fix(#1529): emit runtime-native instruction file from new-project
This commit is contained in:
111
tests/project-instruction-file-parity.test.cjs
Normal file
111
tests/project-instruction-file-parity.test.cjs
Normal file
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Bug #1529 parity / drift guard.
|
||||
*
|
||||
* The runtime → project-instruction-file mapping is shared between two
|
||||
* parallel surfaces:
|
||||
* (A) the Node surface — `getProjectInstructionFile` in runtime-name-policy.cjs,
|
||||
* consumed by profile-output.cjs (the generate-claude-md handler).
|
||||
* (B) the bash surface — `gsd-tools query project-instruction-file --runtime <r>`,
|
||||
* consumed by gsd-core/workflows/new-project.md to set $INSTRUCTION_FILE.
|
||||
*
|
||||
* Per DEFECT.GENERATIVE-FIX, any shared mapping between two surfaces MUST
|
||||
* carry a parity assertion that fails when they diverge. This test is that
|
||||
* guard: it asserts (A) and (B) return the same filename for every runtime,
|
||||
* AND that the new-project.md workflow derives $INSTRUCTION_FILE from the
|
||||
* shared query rather than a hardcoded codex-only branch (the original bug).
|
||||
*
|
||||
* Boundary coverage (per RULESET.TESTS.boundary-coverage): claude (the
|
||||
* kept-as-is case) and an unknown runtime (the AGENTS.md default) are both
|
||||
* exercised alongside every runtime family in the mapping table.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const RUNTIME_NAME_POLICY_PATH = path.join(
|
||||
ROOT,
|
||||
'gsd-core',
|
||||
'bin',
|
||||
'lib',
|
||||
'runtime-name-policy.cjs',
|
||||
);
|
||||
const GSD_TOOLS_PATH = path.join(ROOT, 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
const NEW_PROJECT_WORKFLOW_PATH = path.join(
|
||||
ROOT,
|
||||
'gsd-core',
|
||||
'workflows',
|
||||
'new-project.md',
|
||||
);
|
||||
|
||||
const { getProjectInstructionFile } = require(RUNTIME_NAME_POLICY_PATH);
|
||||
|
||||
const RUNTIMES = [
|
||||
'claude',
|
||||
'codex',
|
||||
'opencode',
|
||||
'kilo',
|
||||
'kimi',
|
||||
'copilot',
|
||||
'antigravity',
|
||||
'gemini',
|
||||
'future-runtime-xyz',
|
||||
'',
|
||||
];
|
||||
|
||||
function queryInstructionFile(runtime) {
|
||||
const args = [
|
||||
GSD_TOOLS_PATH,
|
||||
'query',
|
||||
'project-instruction-file',
|
||||
'--runtime',
|
||||
runtime,
|
||||
];
|
||||
return execFileSync('node', args, {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, GSD_RUNTIME: '' },
|
||||
}).trim();
|
||||
}
|
||||
|
||||
describe('bug #1529: getProjectInstructionFile ↔ gsd-tools query parity', () => {
|
||||
for (const runtime of RUNTIMES) {
|
||||
const label = runtime === '' ? '<empty>' : runtime;
|
||||
test(`Node function and CLI query agree for runtime=${label}`, () => {
|
||||
const fromFunction = getProjectInstructionFile(runtime);
|
||||
const fromQuery = queryInstructionFile(runtime);
|
||||
assert.strictEqual(
|
||||
fromQuery,
|
||||
fromFunction,
|
||||
`gsd-tools query project-instruction-file --runtime ${label} returned "${fromQuery}" but getProjectInstructionFile() returned "${fromFunction}"; the two surfaces drifted.`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('bug #1529: new-project.md workflow uses the shared policy query', () => {
|
||||
// allow-test-rule: structural drift guard for #1529 — the workflow's bash block MUST invoke the
|
||||
// shared `gsd_run query project-instruction-file` query rather than a hardcoded
|
||||
// codex-only `if/else` branch; there is no typed IR for "this bash block calls a
|
||||
// specific gsd-tools query instead of a hardcoded mapping".
|
||||
const workflow = fs.readFileSync(NEW_PROJECT_WORKFLOW_PATH, 'utf8');
|
||||
|
||||
test('workflow derives INSTRUCTION_FILE from the shared query', () => {
|
||||
assert.ok(
|
||||
/INSTRUCTION_FILE=\$\(gsd_run query project-instruction-file --runtime "\$RUNTIME"\)/.test(workflow),
|
||||
'new-project.md must derive INSTRUCTION_FILE via `gsd_run query project-instruction-file --runtime "$RUNTIME"` (the shared policy adapter)',
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow no longer hardcodes the codex-only branch', () => {
|
||||
assert.ok(
|
||||
!/if \[ "\$RUNTIME" = "codex" \]; then INSTRUCTION_FILE="AGENTS\.md"; else INSTRUCTION_FILE="\.claude\/CLAUDE\.md"; fi/.test(workflow),
|
||||
'new-project.md must not contain the retired codex-only `if [ "$RUNTIME" = "codex" ]; then INSTRUCTION_FILE="AGENTS.md"; else INSTRUCTION_FILE=".claude/CLAUDE.md"; fi` branch (#1529 regression guard)',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ const ROOT = path.join(__dirname, '..');
|
||||
const {
|
||||
canonicalizeRuntimeName,
|
||||
resolveRuntimeNameFromCandidates,
|
||||
getProjectInstructionFile,
|
||||
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-name-policy.cjs'));
|
||||
|
||||
describe('runtime-name-policy canonical runtime ids', () => {
|
||||
@@ -71,3 +72,55 @@ describe('runtime-name-policy windsurf alias parity — manifest vs FALLBACK_ALI
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runtime-name-policy getProjectInstructionFile (#1529)', () => {
|
||||
test('claude maps to .claude/CLAUDE.md (kept-as-is boundary case)', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('claude'), '.claude/CLAUDE.md');
|
||||
});
|
||||
|
||||
test('codex maps to AGENTS.md', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('codex'), 'AGENTS.md');
|
||||
});
|
||||
|
||||
test('opencode maps to AGENTS.md (the #1529 bug surface)', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('opencode'), 'AGENTS.md');
|
||||
});
|
||||
|
||||
test('kilo maps to AGENTS.md', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('kilo'), 'AGENTS.md');
|
||||
});
|
||||
|
||||
test('kimi maps to AGENTS.md', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('kimi'), 'AGENTS.md');
|
||||
});
|
||||
|
||||
test('copilot maps to copilot-instructions.md', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('copilot'), 'copilot-instructions.md');
|
||||
});
|
||||
|
||||
test('gemini maps to GEMINI.md', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('gemini'), 'GEMINI.md');
|
||||
});
|
||||
|
||||
test('antigravity maps to GEMINI.md', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('antigravity'), 'GEMINI.md');
|
||||
});
|
||||
|
||||
test('unknown runtime maps to AGENTS.md (safe cross-agent default, boundary case)', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('future-runtime-xyz'), 'AGENTS.md');
|
||||
assert.strictEqual(getProjectInstructionFile(''), 'AGENTS.md');
|
||||
assert.strictEqual(getProjectInstructionFile(null), 'AGENTS.md');
|
||||
assert.strictEqual(getProjectInstructionFile(undefined), 'AGENTS.md');
|
||||
});
|
||||
|
||||
test('aliases normalize via canonicalizeRuntimeName before mapping', () => {
|
||||
// codex-cli is an alias for codex; it must resolve to the codex mapping.
|
||||
assert.strictEqual(getProjectInstructionFile('codex-cli'), 'AGENTS.md');
|
||||
// opencode-cli is an alias for opencode.
|
||||
assert.strictEqual(getProjectInstructionFile('opencode-cli'), 'AGENTS.md');
|
||||
// gemini-cli is an alias for gemini.
|
||||
assert.strictEqual(getProjectInstructionFile('gemini-cli'), 'GEMINI.md');
|
||||
// github-copilot is an alias for copilot.
|
||||
assert.strictEqual(getProjectInstructionFile('github-copilot'), 'copilot-instructions.md');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"milestone-summary.md": 11774,
|
||||
"mvp-phase.md": 13582,
|
||||
"new-milestone.md": 32422,
|
||||
"new-project.md": 61802,
|
||||
"new-project.md": 62308,
|
||||
"new-workspace.md": 11254,
|
||||
"next.md": 20094,
|
||||
"node-repair.md": 4173,
|
||||
|
||||
Reference in New Issue
Block a user