From 67c5533565a7c2efdb7dc40c909186db24c2fff0 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Tue, 7 Apr 2026 19:37:48 -0300 Subject: [PATCH 1/2] test: add bin-entries regression guard Parameterised test (tests/bin-entries.test.ts) that iterates over every pkg.bin entry and asserts: - target file exists on disk - target starts with a node shebang - target lives under a directory declared in pkg.files This prevents the regression class that shipped in @forgespace/core v1.4.0-v1.14.0, where bin.forge-patterns pointed at a non-existent dist/cli.js for over a year. See Forge-Space/core#202 for the original incident. Mutation-tested locally: reverting bin.forge-ai-init to point at a non-existent path makes the test fail on the existence assertion, proving the guard catches real breakage. --- tests/bin-entries.test.ts | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/bin-entries.test.ts diff --git a/tests/bin-entries.test.ts b/tests/bin-entries.test.ts new file mode 100644 index 0000000..3288267 --- /dev/null +++ b/tests/bin-entries.test.ts @@ -0,0 +1,49 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const repoRoot = process.cwd(); +const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf-8')) as { + name: string; + bin?: string | Record; + files?: string[]; +}; + +function normalizeBin(bin: typeof pkg.bin, name: string): Array<[string, string]> { + if (typeof bin === 'string') return [[name, bin]]; + if (bin && typeof bin === 'object') return Object.entries(bin); + return []; +} + +function pkgFilesCoversPath(files: string[], relPath: string): boolean { + const clean = relPath.replace(/^\.\//, ''); + const top = clean.split('/')[0]; + return files.some((f) => { + const c = f.replace(/\/$/, '').replace(/^\.\//, ''); + return c === top; + }); +} + +describe('package.json bin entries', () => { + const entries = normalizeBin(pkg.bin, pkg.name); + const files: string[] = pkg.files ?? []; + + it('declares at least one bin entry', () => { + expect(entries.length).toBeGreaterThan(0); + }); + + it.each(entries)('bin "%s" -> "%s" exists on disk', (_name, rel) => { + const abs = join(repoRoot, rel.replace(/^\.\//, '')); + expect(existsSync(abs)).toBe(true); + }); + + it.each(entries)('bin "%s" -> "%s" starts with a node shebang', (_name, rel) => { + const abs = join(repoRoot, rel.replace(/^\.\//, '')); + if (!existsSync(abs)) return; + const firstLine = readFileSync(abs, 'utf-8').split('\n', 1)[0] ?? ''; + expect(firstLine).toMatch(/^#!.*\bnode\b/); + }); + + it.each(entries)('bin "%s" -> "%s" lives under a directory in pkg.files', (_name, rel) => { + expect(pkgFilesCoversPath(files, rel)).toBe(true); + }); +}); From e4eff0d8307d540c743ef0b34471e485fa63bafd Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Wed, 8 Apr 2026 14:05:59 -0300 Subject: [PATCH 2/2] ci: build dist before jest so bin-entries test can resolve files Without pretest=build, bin-entries.test.ts asserts against dist/index.js that does not exist yet in CI (jest runs before the build step), producing a false-positive failure. A pretest hook guarantees dist/ exists in every invocation path (CI, local, validate). --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 772e836..c8c13bd 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "scripts": { "build": "tsup", "dev": "tsx src/index.ts", + "pretest": "npm run build", "test": "node --experimental-vm-modules node_modules/.bin/jest", "test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage", "lint": "eslint src/",