* fix(#637): route 3 more workflows through gsd_run launcher (hardcoded $HOME sweep) The hardcoded `node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs"` invocation form fixed in plan-phase.md (#621) survived in three more workflows. Same bug class: on a global/shim-only install with no project-local runtime, the hardcoded path can miss a working install, so the step reports the tool "not found" instead of resolving it via the launcher. #3668 introduced gsd_run resolution; these sites were missed. - plan-review-convergence.md: convert the 3 hardcoded invocations (init, roadmap get-phase, state planned-phase) to gsd_run. File already carried the canonical preamble (first gsd_run is the earlier convergence-enabled check). - ingest-docs.md, spec-phase.md: convert their hardcoded invocations to gsd_run and inject the canonical launcher preamble via `node scripts/sync-runtime-launcher.cjs` (these files previously had no gsd_run and no preamble). The injected preamble is byte-equal to _runtime-launcher.snippet.sh and precedes the first gsd_run call, per runtime-launcher-parity invariant (B). - Add tests/bug-637-workflow-no-hardcoded-home-tool.test.cjs: repo-wide regression guard asserting NO workflow .md invokes gsd-tools via a hardcoded $HOME path. Generalizes the plan-phase-only guard from #621 — the parity test guards retired $GSD_SDK / bare /gsd-tools tokens but not this form, which is how it survived across four files. Fails on the pre-fix files, passes after. runtime-launcher-parity 7/7; full unit suite green (3477 pass / 0 fail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#637): add changeset fragment for PR #642 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(#637): update stale bug-2801 assertion to expect gsd_run bug-2801 pinned ingest-docs.md to the hardcoded node "$HOME/.../gsd-tools.cjs" init form, which #637 replaces with the gsd_run launcher. Flip the assertion to expect gsd_run init ingest-docs; the bare-gsd-tools rejection and CLI-handler tests are unchanged, and bug-637's repo-wide guard now owns the no-hardcoded-$HOME invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
151 lines
6.0 KiB
JavaScript
151 lines
6.0 KiB
JavaScript
/**
|
|
* Regression test for bug #2801
|
|
*
|
|
* `/gsd-ingest-docs` was broken because:
|
|
* 1. `workflows/ingest-docs.md` called `gsd-sdk query init.ingest-docs` but the
|
|
* installed binary is `gsd-tools` (not `gsd-sdk`).
|
|
* 2. `gsd-tools init` had no `ingest-docs` case in its dispatch switch.
|
|
*
|
|
* The fix:
|
|
* - Added `case 'ingest-docs'` to the `init` switch in `gsd-tools.cjs`.
|
|
* - Exported `cmdInitIngestDocs` from `init.cjs`.
|
|
* - Updated `workflows/ingest-docs.md` to call `gsd-tools init ingest-docs`.
|
|
*
|
|
* This test prevents regression of the dispatch omission.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { describe, test, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const childProc = require('node:child_process');
|
|
const { createTempProject, cleanup, TOOLS_PATH } = require('./helpers.cjs');
|
|
|
|
const REPO_ROOT = path.join(__dirname, '..');
|
|
const WORKFLOW_FILE = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'ingest-docs.md');
|
|
|
|
function spawnGsdTools(args, projectDir) {
|
|
let stdout = '';
|
|
let exitCode = 0;
|
|
try {
|
|
stdout = childProc.execFileSync(
|
|
process.execPath,
|
|
[TOOLS_PATH, ...args, '--cwd', projectDir],
|
|
{
|
|
encoding: 'utf-8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: { ...process.env, GSD_SESSION_KEY: '' },
|
|
}
|
|
);
|
|
} catch (err) {
|
|
exitCode = err.status ?? 1;
|
|
stdout = (err.stdout?.toString() ?? '') + (err.stderr?.toString() ?? '');
|
|
}
|
|
return { exitCode, stdout };
|
|
}
|
|
|
|
describe('bug-2801: gsd-tools init ingest-docs handler exists', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject('gsd-test-2801-');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('init ingest-docs exits 0 (not "Unknown init workflow")', () => {
|
|
const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir);
|
|
assert.strictEqual(exitCode, 0, `expected exit 0, got: ${stdout}`);
|
|
});
|
|
|
|
test('init ingest-docs returns JSON with project_exists field', () => {
|
|
const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir);
|
|
assert.strictEqual(exitCode, 0);
|
|
let json;
|
|
try { json = JSON.parse(stdout.trim()); } catch { assert.fail(`non-JSON output: ${stdout}`); }
|
|
assert.ok(Object.prototype.hasOwnProperty.call(json, 'project_exists'), 'project_exists present');
|
|
});
|
|
|
|
test('init ingest-docs returns JSON with planning_exists field', () => {
|
|
const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir);
|
|
assert.strictEqual(exitCode, 0);
|
|
const json = JSON.parse(stdout.trim());
|
|
assert.ok(Object.prototype.hasOwnProperty.call(json, 'planning_exists'), 'planning_exists present');
|
|
});
|
|
|
|
test('init ingest-docs returns JSON with has_git field', () => {
|
|
const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir);
|
|
assert.strictEqual(exitCode, 0);
|
|
const json = JSON.parse(stdout.trim());
|
|
assert.ok(Object.prototype.hasOwnProperty.call(json, 'has_git'), 'has_git present');
|
|
});
|
|
|
|
test('init ingest-docs returns JSON with project_path field', () => {
|
|
const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir);
|
|
assert.strictEqual(exitCode, 0);
|
|
const json = JSON.parse(stdout.trim());
|
|
assert.ok(Object.prototype.hasOwnProperty.call(json, 'project_path'), 'project_path present');
|
|
assert.ok(Object.prototype.hasOwnProperty.call(json, 'commit_docs'), 'commit_docs present');
|
|
});
|
|
|
|
test('planning_exists is true when .planning/ directory exists', () => {
|
|
const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir);
|
|
assert.strictEqual(exitCode, 0);
|
|
const json = JSON.parse(stdout.trim());
|
|
assert.strictEqual(json.planning_exists, true, 'planning_exists should be true (.planning/ created by createTempProject)');
|
|
});
|
|
});
|
|
|
|
describe('bug-2801: ingest-docs.md workflow calls gsd-tools not gsd-sdk', () => {
|
|
test('no bash code block in ingest-docs.md calls gsd-sdk', () => {
|
|
const content = fs.readFileSync(WORKFLOW_FILE, 'utf-8');
|
|
// Extract bash fenced code blocks structurally.
|
|
const bashBlocks = [];
|
|
const codeBlockRe = /```bash\r?\n([\s\S]*?)```/g;
|
|
let m;
|
|
while ((m = codeBlockRe.exec(content)) !== null) {
|
|
bashBlocks.push(m[1]);
|
|
}
|
|
assert.ok(bashBlocks.length > 0, 'expected bash code blocks in workflow');
|
|
|
|
// Check every line in every bash block — not just lines that start with the token,
|
|
// since gsd-sdk can appear in subshell expansions like $(gsd-sdk query ...).
|
|
const sdkCalls = bashBlocks
|
|
.join('\n')
|
|
.split('\n')
|
|
.filter((line) => /\bgsd-sdk\b/.test(line));
|
|
|
|
assert.deepStrictEqual(
|
|
sdkCalls,
|
|
[],
|
|
`workflow bash blocks still reference gsd-sdk (should use gsd-tools): ${sdkCalls.join(', ')}`
|
|
);
|
|
});
|
|
|
|
test('ingest-docs.md init step uses the gsd_run launcher (#637)', () => {
|
|
const content = fs.readFileSync(WORKFLOW_FILE, 'utf-8');
|
|
// Parse fenced bash blocks structurally — do not match raw markdown text.
|
|
const codeBlockRe = /```bash\r?\n([\s\S]*?)```/g;
|
|
const bashLines = [...content.matchAll(codeBlockRe)]
|
|
.flatMap((m) => m[1].split('\n'))
|
|
.filter((l) => !/^\s*#/.test(l));
|
|
// #637 routes ingest-docs through the resolved `gsd_run` launcher instead of
|
|
// the hardcoded `node "$HOME/.../gsd-tools.cjs"` path (which misses global
|
|
// installs). The legacy bare `gsd-tools` form remains the bug and is still
|
|
// rejected by bug-2851's repo-wide guard.
|
|
const initLine = bashLines.find((l) =>
|
|
/\bgsd_run\s+init\s+ingest-docs\b/.test(l)
|
|
);
|
|
assert.ok(initLine, 'workflow must invoke init ingest-docs via the gsd_run launcher (#637)');
|
|
});
|
|
|
|
test('cmdInitIngestDocs is exported from init.cjs', () => {
|
|
const init = require(path.join(REPO_ROOT, 'gsd-core', 'bin', 'lib', 'init.cjs'));
|
|
assert.strictEqual(typeof init.cmdInitIngestDocs, 'function', 'cmdInitIngestDocs must be exported');
|
|
});
|
|
});
|