test(#2974): migrate 8 test files to typed-IR assertions (#3016)

* test(#2974): migrate 8 test files to typed-IR assertions

Replaces raw stdout/stderr substring matching with structured-field
assertions per CONTRIBUTING.md "Prohibited: Raw Text Matching on Test
Outputs". Adds shared infrastructure for typed error emission so this
pattern is the easy path going forward.

Shared infrastructure:
- core.cjs: ERROR_REASON frozen enum + setJsonErrorMode/getJsonErrorMode
- gsd-tools.cjs: --json-errors CLI flag, parsed before subcommand dispatch
- config.cjs: typed reasons at all 7 error sites
- graphify.cjs: GRAPHIFY_REASON enum + reason/timeout_ms in execGraphify result
- bin/install.js: pure buildSdkFailFastReport() IR builder + renderer
- hooks/gsd-session-state.sh, gsd-phase-boundary.sh: emit Claude Code
  hookSpecificOutput JSON envelope with typed state_present/config_mode/
  planning_modified/file_path fields (no-op when hooks.community is off)

Test migrations (all pass, 171 tests across the 8 files):
- bug-2649-sdk-fail-fast: assert on ir.reason / ir.context / ir.fix_command
- bug-2687-config-read-warning-parity: assert.equal stderr === ''
- bug-2796-arg-parsing-regression: assert on result.json.updated/.phase
- bug-2838-summary-rescue: parse rescue footer, assert mtime invariant
- bug-2943-config-get-context-window: parse JSON, assert ERROR_REASON.CONFIG_KEY_NOT_FOUND
- graphify: assert reason === GRAPHIFY_REASON.ENOENT/TIMEOUT
- hooks-opt-in: parse hookSpecificOutput, assert typed fields
- security-scan: reclassified as source-text-is-the-product (scan label
  output and CI workflow YAML ARE the deployed contract)

Verification: lint-no-source-grep clean (0 violations), full suite
6741/6741 pass.

Closes #2974

* test(#2974): address CR feedback — typed code field, robust idempotency

Two CodeRabbit findings on #3016 addressed:

1. tests/hooks-opt-in.test.cjs:355 (Minor, inline) —
   parsed.reason.includes('Conventional Commits') was still substring
   matching after the typed-IR migration. Fixed at the source: the
   gsd-validate-commit hook now emits a typed `code` field
   ('CONVENTIONAL_COMMITS_VIOLATION', 'COMMIT_SUBJECT_TOO_LONG')
   alongside the human-readable `reason`. Test asserts strictEqual
   on the code; the prose copy is no longer part of the test contract.

2. tests/bug-2838-summary-rescue-gitignored-planning.test.cjs:224-250
   (Outside-diff) — mtimeMs alone can stay unchanged on coarse-grained
   filesystems (HFS+, FAT) when two rewrites land within the same
   timestamp tick, falsely passing the idempotency assertion.
   Replaced with a full snapshot (mtimeMs, ctimeMs, size, ino, sha256
   of contents) compared via assert.deepStrictEqual — the hash
   catches any rewrite the timestamp would miss.

Verification: 30/30 pass on the two affected files; lint-no-source-grep
clean (0 violations across 368 test files).
This commit is contained in:
Tom Boucher
2026-05-02 09:27:23 -04:00
committed by GitHub
parent de25400b70
commit f55069ecbf
17 changed files with 470 additions and 178 deletions

View File

@@ -8944,6 +8944,85 @@ function classifySdkInstall(sdkDir) {
return { mode, npxCache, readOnly };
}
/**
* #2974: pure builder for the SDK fail-fast report. Returns a structured IR
* with everything the renderer needs PLUS everything tests need to assert
* on. Tests can call `buildSdkFailFastReport(sdkDir, sdkCliPath)` directly
* and assert on `report.reason`, `report.context`, `report.fix_command`
* etc. without intercepting console.error or matching against rendered
* text.
*
* Shape (frozen contract — extending requires a new test):
* {
* ok: false,
* reason: 'sdk_fail_fast', // ERROR_REASON.SDK_FAIL_FAST
* context: 'npx-cache' | 'tarball' | 'dev-clone',
* missing_path: '<path>/sdk/dist/cli.js',
* missing_artifact: 'sdk/dist',
* fix_command: 'npm install -g get-shit-done-cc@latest' | 'cd sdk && npm install && npm run build',
* attempted_nested_install: false, // contract: never true
* }
*/
function buildSdkFailFastReport(sdkDir, sdkCliPath) {
const ctx = classifySdkInstall(sdkDir);
let context, fix_command;
if (ctx.mode === 'tarball') {
context = ctx.npxCache ? 'npx-cache' : 'tarball';
fix_command = 'npm install -g get-shit-done-cc@latest';
} else {
context = 'dev-clone';
fix_command = 'cd sdk && npm install && npm run build';
}
return {
ok: false,
reason: 'sdk_fail_fast',
context,
missing_path: sdkCliPath,
missing_artifact: 'sdk/dist',
fix_command,
attempted_nested_install: false,
};
}
/**
* Renderer for the structured fail-fast report. Text formatting only —
* tests never call this. Splits the IR fields back into the same human-
* readable lines the previous shape produced.
*/
function renderSdkFailFastReport(ir) {
const bar = '━'.repeat(72);
const redBold = `${red}${bold}`;
console.error('');
console.error(`${redBold}${bar}${reset}`);
console.error(`${redBold} ✗ GSD SDK dist not found — /gsd-* commands will not work${reset}`);
console.error(`${redBold}${bar}${reset}`);
console.error(` ${red}Reason:${reset} ${ir.missing_artifact}/cli.js not found at ${ir.missing_path}`);
console.error('');
if (ir.context === 'npx-cache') {
console.error(` Detected read-only npx cache install (${dim}${path.dirname(ir.missing_path).replace(/\/dist$/, '')}${reset}).`);
console.error(` The installer will ${bold}not${reset} attempt \`npm install\` inside the npx cache.`);
console.error('');
console.error(` Fix: install a version that ships sdk/dist/ globally:`);
console.error(` ${cyan}${ir.fix_command}${reset}`);
console.error(` Or, if you prefer a one-shot run, clear the npx cache first:`);
console.error(` ${cyan}npx --yes get-shit-done-cc@latest${reset}`);
console.error(` Or build from source (git clone):`);
console.error(` ${cyan}git clone https://github.com/gsd-build/get-shit-done && cd get-shit-done/sdk && npm install && npm run build${reset}`);
} else if (ir.context === 'tarball') {
console.error(` The published tarball appears to be missing sdk/dist/ (see #2647).`);
console.error('');
console.error(` Fix: install a version that ships sdk/dist/ globally:`);
console.error(` ${cyan}${ir.fix_command}${reset}`);
console.error(` Or build from source (git clone):`);
console.error(` ${cyan}git clone https://github.com/gsd-build/get-shit-done && cd get-shit-done/sdk && npm install && npm run build${reset}`);
} else {
console.error(` Running from a git clone — build the SDK first:`);
console.error(` ${cyan}${ir.fix_command}${reset}`);
}
console.error(`${redBold}${bar}${reset}`);
console.error('');
}
function installSdkIfNeeded(opts) {
opts = opts || {};
if (hasNoSdk && !opts.sdkDir) {
@@ -8971,44 +9050,8 @@ function installSdkIfNeeded(opts) {
}
if (!fs.existsSync(sdkCliPath)) {
const ctx = classifySdkInstall(sdkDir);
const bar = '━'.repeat(72);
const redBold = `${red}${bold}`;
console.error('');
console.error(`${redBold}${bar}${reset}`);
console.error(`${redBold} ✗ GSD SDK dist not found — /gsd-* commands will not work${reset}`);
console.error(`${redBold}${bar}${reset}`);
console.error(` ${red}Reason:${reset} sdk/dist/cli.js not found at ${sdkCliPath}`);
console.error('');
if (ctx.mode === 'tarball') {
// User install (including `npx get-shit-done-cc@latest`, which stages
// a read-only tarball under the npx cache). The sdk/dist/ artifact
// should ship in the published tarball. If it's missing, the only
// sane fix from the user's side is a fresh global install of a
// version that includes dist/. Do NOT attempt a nested `npm install`
// inside the (read-only) npx cache — that's the #2649 failure mode.
if (ctx.npxCache) {
console.error(` Detected read-only npx cache install (${dim}${sdkDir}${reset}).`);
console.error(` The installer will ${bold}not${reset} attempt \`npm install\` inside the npx cache.`);
console.error('');
} else {
console.error(` The published tarball appears to be missing sdk/dist/ (see #2647).`);
console.error('');
}
console.error(` Fix: install a version that ships sdk/dist/ globally:`);
console.error(` ${cyan}npm install -g get-shit-done-cc@latest${reset}`);
console.error(` Or, if you prefer a one-shot run, clear the npx cache first:`);
console.error(` ${cyan}npx --yes get-shit-done-cc@latest${reset}`);
console.error(` Or build from source (git clone):`);
console.error(` ${cyan}git clone https://github.com/gsd-build/get-shit-done && cd get-shit-done/sdk && npm install && npm run build${reset}`);
} else {
// Dev clone: keep the existing build-from-source hint.
console.error(` Running from a git clone — build the SDK first:`);
console.error(` ${cyan}cd sdk && npm install && npm run build${reset}`);
}
console.error(`${redBold}${bar}${reset}`);
console.error('');
const ir = buildSdkFailFastReport(sdkDir, sdkCliPath);
renderSdkFailFastReport(ir);
process.exit(1);
}
@@ -9370,6 +9413,8 @@ if (process.env.GSD_TEST_MODE) {
install,
uninstall,
installSdkIfNeeded,
buildSdkFailFastReport,
renderSdkFailFastReport,
classifySdkInstall,
convertClaudeCommandToCodexSkill,
convertClaudeToOpencodeFrontmatter,