96 lines
4.5 KiB
TypeScript
96 lines
4.5 KiB
TypeScript
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
// Every @conusai-sdk/* package the template imports must be pinned in the
|
|
// shipped package.json at the version the SDK source tree carries at the same
|
|
// commit (showcase plan §2.7 "Pin versions", D19). An import the manifest does
|
|
// not declare builds nowhere: `npm install` cannot know it and `next build`
|
|
// fails with "Module not found" (KI-55; stage deployment #5, 2026-10-09).
|
|
//
|
|
// The pin comparison needs the monorepo: CONUSAI_REPO_ROOT when set
|
|
// (test-template.sh exports it), else the nearest ancestor holding
|
|
// sdks/node/packages. A standalone project created from the template has
|
|
// neither — no source tree exists there to compare against, so only the
|
|
// presence check runs.
|
|
const templateRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
const thisFile = fileURLToPath(import.meta.url)
|
|
|
|
const IMPORT_PATTERN = /@conusai-sdk\/[a-z0-9-]+/g
|
|
const SOURCE_PATTERN = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/
|
|
const SKIP_DIRS = new Set(['node_modules', '.next'])
|
|
|
|
function collectSdkImports(dir: string, found = new Set<string>()): Set<string> {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const path = join(dir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
if (!SKIP_DIRS.has(entry.name)) collectSdkImports(path, found)
|
|
} else if (SOURCE_PATTERN.test(entry.name) && path !== thisFile) {
|
|
for (const match of readFileSync(path, 'utf8').matchAll(IMPORT_PATTERN)) found.add(match[0])
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
const sdkImports = [...collectSdkImports(templateRoot)].sort()
|
|
const manifest = JSON.parse(readFileSync(join(templateRoot, 'package.json'), 'utf8')) as {
|
|
dependencies?: Record<string, string>
|
|
devDependencies?: Record<string, string>
|
|
}
|
|
const declared: Record<string, string> = { ...manifest.dependencies, ...manifest.devDependencies }
|
|
|
|
function monorepoRoot(): string | undefined {
|
|
const candidates = process.env.CONUSAI_REPO_ROOT ? [process.env.CONUSAI_REPO_ROOT] : []
|
|
let dir = templateRoot
|
|
for (let depth = 0; depth <= 4; depth += 1) {
|
|
candidates.push(dir)
|
|
dir = join(dir, '..')
|
|
}
|
|
return candidates.find((candidate) => existsSync(join(candidate, 'sdks/node/packages')))
|
|
}
|
|
|
|
function sdkSourceVersions(root: string): Map<string, string> {
|
|
const versions = new Map<string, string>()
|
|
for (const group of ['sdks/node/packages', 'packages']) {
|
|
for (const entry of readdirSync(join(root, group), { withFileTypes: true })) {
|
|
const manifestPath = join(root, group, entry.name, 'package.json')
|
|
if (!entry.isDirectory() || !existsSync(manifestPath)) continue
|
|
const source = JSON.parse(readFileSync(manifestPath, 'utf8')) as { name?: string; version?: string }
|
|
if (source.name?.startsWith('@conusai-sdk/') && source.version) versions.set(source.name, source.version)
|
|
}
|
|
}
|
|
return versions
|
|
}
|
|
|
|
// A dev vendor bundle rewrites every package's own version and the template's
|
|
// @conusai-sdk/* pins with the same `-dev.<yyyymmdd>.<sha8>` suffix, inside the
|
|
// bundle only (scripts/vendor/build-bundle.sh, scripts/vendor/rewrite-version.py;
|
|
// showcase plan D29). test-template.sh now builds this file's package.json from
|
|
// the bundle's own rewritten template archive, so a dev build's declared pin
|
|
// carries that suffix; strip it before comparing against this commit's plain
|
|
// SDK source version. A release build's pin carries no suffix, so the strip is
|
|
// a no-op there.
|
|
const DEV_SUFFIX = /[-.]dev\.\d{8}\.[0-9a-f]+$/
|
|
|
|
describe('the shipped package.json pins every imported @conusai-sdk package (D19)', () => {
|
|
it('finds @conusai-sdk imports in the template (negative control)', () => {
|
|
expect(sdkImports.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it.each(sdkImports)('declares %s', (name) => {
|
|
expect(declared, `${name} is imported but missing from package.json`).toHaveProperty(name)
|
|
})
|
|
|
|
it('pins each at the version the SDK source tree carries at this commit (plain, or dev-bundle-suffixed)', () => {
|
|
const root = monorepoRoot()
|
|
if (!root) return // standalone checkout: no SDK source tree to compare against
|
|
const versions = sdkSourceVersions(root)
|
|
for (const name of sdkImports) {
|
|
expect(versions.has(name), `${name} has no source package under sdks/node/packages or packages`).toBe(true)
|
|
const pinnedBase = (declared[name] ?? '').replace(DEV_SUFFIX, '')
|
|
expect(pinnedBase, `${name} pin differs from the SDK source version at this commit`).toBe(versions.get(name))
|
|
}
|
|
})
|
|
})
|