// src/test/cli/packaging.test.ts — packaging and entrypoint hardening tests // Ensures exactly one canonical invocation path and no broken alternatives. import { describe, it } from 'node:test'; import assert from 'node:assert'; import { spawnSync } from 'node:child_process'; import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; const ROOT = new URL('../../../', import.meta.url).pathname; const DIST_CLI = join(ROOT, 'dist/cli/index.js'); const PACKAGE_JSON = join(ROOT, 'package.json'); function run(args: string[], cwd?: string) { const result = spawnSync('node', [DIST_CLI, ...args], { encoding: 'utf8', cwd: cwd || ROOT, timeout: 30000, }); return { stdout: result.stdout || '', stderr: result.stderr || '', status: result.status, signal: result.signal, }; } describe('packaging', () => { it('dist/cli/index.js exists after build', () => { assert(existsSync(DIST_CLI), `Missing ${DIST_CLI} — run npm run build first`); }); it('--help exits 0 and prints expected help text', () => { const { stdout, status } = run(['--help']); assert.strictEqual(status, 0, `Expected exit 0, got ${status}. stderr: ${run(['--help']).stderr}`); assert(stdout.includes('apophis'), 'Help should mention apophis'); assert(stdout.includes('Commands:'), 'Help should list commands'); assert(stdout.includes('init'), 'Help should mention init'); assert(stdout.includes('verify'), 'Help should mention verify'); }); it('--version exits 0 and prints version', () => { const { stdout, status } = run(['--version']); assert.strictEqual(status, 0, `Expected exit 0, got ${status}`); assert.match(stdout, /2\.7\.0/, `Version should include 2.7.0, got: ${stdout}`); }); it('init --help exits 0 and prints init help', () => { const { stdout, status } = run(['init', '--help']); assert.strictEqual(status, 0); assert(stdout.includes('apophis init'), 'Should show init help header'); assert(stdout.includes('--preset'), 'Should mention --preset'); }); it('verify --help exits 0 and prints verify help', () => { const { stdout, status } = run(['verify', '--help']); assert.strictEqual(status, 0); assert(stdout.includes('apophis verify'), 'Should show verify help header'); assert(stdout.includes('--routes'), 'Should mention --routes'); }); it('frobnicate exits 2 with "Unknown command"', () => { const { stdout, stderr, status } = run(['frobnicate']); assert.strictEqual(status, 2, `Expected exit 2, got ${status}`); const combined = stdout + stderr; assert(combined.includes('Unknown command'), `Should report unknown command. Got: ${combined}`); }); it('verify --unknown-flag exits 2 with "Unknown flag"', () => { const { stdout, stderr, status } = run(['verify', '--unknown-flag']); assert.strictEqual(status, 2, `Expected exit 2, got ${status}`); const combined = stdout + stderr; assert(combined.includes('Unknown flag'), `Should report unknown flag. Got: ${combined}`); }); it('doctor --mode verify does not reject --mode as unknown', () => { const { stdout, stderr, status } = run(['doctor', '--mode', 'verify', '--cwd', 'src/cli/__fixtures__/tiny-fastify']); const combined = stdout + stderr; assert.notStrictEqual(status, 3, `Should not crash. Output: ${combined}`); assert(!combined.includes('Unknown flag: --mode'), `Should accept --mode flag. Output: ${combined}`); }); // For each of the 7 commands, verify they do NOT print "Not yet implemented" const commands = ['init', 'verify', 'qualify', 'replay', 'doctor', 'migrate']; for (const cmd of commands) { it(`${cmd} does not print "Not yet implemented"`, () => { // Some commands may fail for config reasons; we just assert they don't say "Not yet implemented" const { stdout, stderr } = run([cmd]); const combined = stdout + stderr; assert( !combined.includes('Not yet implemented'), `Command ${cmd} appears to be a placeholder. Output: ${combined}` ); }); } it('npx apophis --help works via temp package.json bin reference', () => { const tmpDir = join(tmpdir(), `apophis-packaging-test-${Date.now()}`); mkdirSync(tmpDir, { recursive: true }); const pkg = { name: 'test-consumer', version: '1.0.0', dependencies: { '@apophis/fastify': `file:${ROOT}`, }, }; writeFileSync(join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2)); // We don't actually npm install; instead we verify the bin path resolves correctly // by checking the package.json bin field points to dist/cli/index.js const rootPkg = JSON.parse(readFileSync(PACKAGE_JSON, 'utf8')); assert.strictEqual(rootPkg.bin.apophis, 'dist/cli/index.js', 'package.json bin must point to dist/cli/index.js'); assert.strictEqual(rootPkg.main, 'dist/index.js', 'package.json main must point to dist/index.js'); // Verify the file exists at the resolved path const resolvedBin = join(ROOT, rootPkg.bin.apophis); assert(existsSync(resolvedBin), `Resolved bin path does not exist: ${resolvedBin}`); // Clean up temp dir rmSync(tmpDir, { recursive: true, force: true }); }); it('npm pack produces a tarball with the bin entry', () => { const result = spawnSync('npm', ['pack', '--dry-run', '--json'], { cwd: ROOT, encoding: 'utf8', timeout: 30000, }); assert.strictEqual(result.status, 0, `npm pack --dry-run failed: ${result.stderr}`); const packOutput = JSON.parse(result.stdout); const files = packOutput[0]?.files?.map((f: { path: string }) => f.path) || []; assert(files.includes('dist/cli/index.js'), 'Tarball must include dist/cli/index.js'); }); it('npm pack does not contain dist/test files', () => { const result = spawnSync('npm', ['pack', '--dry-run', '--json'], { cwd: ROOT, encoding: 'utf8', timeout: 30000, }); assert.strictEqual(result.status, 0, `npm pack failed: ${result.stderr}`); const packOutput = JSON.parse(result.stdout); const files = packOutput[0]?.files?.map((f: { path: string }) => f.path) || []; const testFiles = files.filter((f: string) => f.includes('dist/test/')); assert.strictEqual(testFiles.length, 0, `Package must not contain dist/test/ files. Found: ${testFiles.join(', ')}`); }); it('real consumer can import the package after install', () => { const tmpDir = join(tmpdir(), `apophis-consumer-${Date.now()}`); mkdirSync(tmpDir, { recursive: true }); try { const pkg = { name: 'test-consumer-import', version: '1.0.0', type: 'module', dependencies: { '@apophis/fastify': `file:${ROOT}`, }, }; writeFileSync(join(tmpDir, 'package.json'), JSON.stringify(pkg)); const installResult = spawnSync('npm', ['install', '--silent', '--install-strategy=nested'], { cwd: tmpDir, encoding: 'utf8', timeout: 120000, }); assert.strictEqual(installResult.status, 0, `npm install failed: ${installResult.stderr}`); const importRootResult = spawnSync('node', ['-e', "import('@apophis/fastify').then(m => console.log('OK:', Object.keys(m))).catch(e => { console.error('FAIL:', e.message); process.exit(1) })"], { cwd: tmpDir, encoding: 'utf8', timeout: 30000, }); assert.strictEqual(importRootResult.status, 0, `Import root failed: ${importRootResult.stderr}`); assert.ok(importRootResult.stdout.includes('OK:'), `Import should print OK, got: ${importRootResult.stdout}`); const importExtResult = spawnSync('node', ['-e', "import('@apophis/fastify/extensions').then(m => console.log('EXT OK:', Object.keys(m))).catch(e => { console.error('EXT FAIL:', e.message); process.exit(1) })"], { cwd: tmpDir, encoding: 'utf8', timeout: 30000, }); assert.strictEqual(importExtResult.status, 0, `Import extensions failed: ${importExtResult.stderr}`); assert.ok(importExtResult.stdout.includes('EXT OK:'), `Extensions import should print OK, got: ${importExtResult.stdout}`); const binResult = spawnSync('node', [join(tmpDir, 'node_modules/.bin/apophis'), '--version'], { cwd: tmpDir, encoding: 'utf8', timeout: 30000, }); assert.strictEqual(binResult.status, 0, `CLI bin failed: ${binResult.stderr}`); assert.ok(binResult.stdout.includes('2.'), `CLI should print version starting with 2., got: ${binResult.stdout}`); } finally { rmSync(tmpDir, { recursive: true, force: true }); } }); it('TypeScript consumer can import and typecheck', () => { const tmpDir = join(tmpdir(), `apophis-ts-consumer-${Date.now()}`); mkdirSync(tmpDir, { recursive: true }); try { writeFileSync(join(tmpDir, 'package.json'), JSON.stringify({ name: 'ts-test', version: '1.0.0', type: 'module', dependencies: { '@apophis/fastify': `file:${ROOT}`, 'fastify': '^5.0.0', '@fastify/swagger': '^9.0.0', }, })); const installResult = spawnSync('npm', ['install', '--silent', '--install-strategy=nested'], { cwd: tmpDir, encoding: 'utf8', timeout: 120000, }); assert.strictEqual(installResult.status, 0, `npm install failed: ${installResult.stderr}`); writeFileSync(join(tmpDir, 'consumer.ts'), ` import Fastify from 'fastify' import apophis, { createAuthExtension } from '@apophis/fastify' import { jwtExtension } from '@apophis/fastify/extensions' import { sseExtension } from '@apophis/fastify/extensions/sse' import { websocketExtension } from '@apophis/fastify/extensions/websocket' import { createSerializerExtension, createSerializerRegistry } from '@apophis/fastify/extensions/serializers' import { createHeaderExtension } from '@apophis/fastify/extension/factories' const app = Fastify() app.register(apophis, { extensions: [jwtExtension, sseExtension, websocketExtension], }) `); writeFileSync(join(tmpDir, 'tsconfig.json'), JSON.stringify({ compilerOptions: { target: 'es2020', module: 'nodenext', moduleResolution: 'nodenext', strict: true, skipLibCheck: true, }, include: ['consumer.ts'], })); const tsc = join(ROOT, 'node_modules/.bin/tsc'); const tscResult = spawnSync(tsc, ['--noEmit', '--project', tmpDir], { cwd: tmpDir, encoding: 'utf8', timeout: 60000, }); if (tscResult.status !== 0) { console.error('TSC errors:', tscResult.stdout + tscResult.stderr); } assert.strictEqual(tscResult.status, 0, `TypeScript typecheck must pass cleanly. Got:\n${tscResult.stdout}${tscResult.stderr}`); } finally { rmSync(tmpDir, { recursive: true, force: true }); } }); it('npx apophis --help works in a temp project after npm install', () => { const tmpDir = join(tmpdir(), `apophis-npx-test-${Date.now()}`); mkdirSync(tmpDir, { recursive: true }); const pkg = { name: 'npx-test', version: '1.0.0', dependencies: { '@apophis/fastify': `file:${ROOT}`, }, }; writeFileSync(join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2)); const installResult = spawnSync('npm', ['install', '--silent'], { cwd: tmpDir, encoding: 'utf8', timeout: 120000, }); assert.strictEqual(installResult.status, 0, `npm install failed: ${installResult.stderr}`); const helpResult = spawnSync('npx', ['apophis', '--help'], { cwd: tmpDir, encoding: 'utf8', timeout: 30000, }); assert.strictEqual(helpResult.status, 0, `npx apophis --help failed: ${helpResult.stderr}`); assert(helpResult.stdout.includes('apophis'), 'Help should mention apophis'); rmSync(tmpDir, { recursive: true, force: true }); }); it('npx apophis doctor works in a temp project after npm install', () => { const tmpDir = join(tmpdir(), `apophis-npx-test-${Date.now()}`); mkdirSync(tmpDir, { recursive: true }); const pkg = { name: 'npx-test', version: '1.0.0', dependencies: { '@apophis/fastify': `file:${ROOT}`, }, }; writeFileSync(join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2)); const installResult = spawnSync('npm', ['install', '--silent'], { cwd: tmpDir, encoding: 'utf8', timeout: 120000, }); assert.strictEqual(installResult.status, 0, `npm install failed: ${installResult.stderr}`); const doctorResult = spawnSync('npx', ['apophis', 'doctor'], { cwd: tmpDir, encoding: 'utf8', timeout: 30000, }); // doctor exits non-zero when peer deps are missing in a bare temp project, // but it should still run and print the header assert(doctorResult.stdout.includes('APOPHIS Doctor'), `Doctor should run and print header. stdout: ${doctorResult.stdout} stderr: ${doctorResult.stderr}`); rmSync(tmpDir, { recursive: true, force: true }); }); it('declares supported Node policy and default confidence test path', () => { const rootPkg = JSON.parse(readFileSync(PACKAGE_JSON, 'utf8')); assert.strictEqual(rootPkg.engines.node, '>=20.18.1 <21 || >=22 <23'); assert.strictEqual(rootPkg.scripts.test, 'npm run build && npm run test:src && npm run test:cli'); }); });