fix(#3749): route migrate-config, project_exists, and health repairs through the project-aware resolver (#3955)

* test(#3749): GSD_PROJECT-scoped migrate-config, project_exists, and repair paths

* fix(#3749): route migrate-config, project_exists, and health repairs through the project-aware resolver

* chore(#3749): changeset fragment (pr number backfilled after PR creation)

* chore(#3749): backfill changeset PR number (3955)

* test(#3749): assert on the POSIX-normalized project_path across platforms

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-27 17:57:05 -04:00
committed by GitHub
parent fa41bfec5c
commit ddeb141bd2
9 changed files with 177 additions and 15 deletions

View File

@@ -4707,3 +4707,38 @@ describe('#3581: init.progress next_phase prefers the roadmap frontier', () => {
assert.equal(out.next_phase, null, 'all-complete milestone: no frontier (completion flow owns the answer)');
});
});
// ─── #3749: project_exists must follow project_path under GSD_PROJECT ───────
describe('init.new-project — GSD_PROJECT scoping (#3749)', () => {
test('project_exists tracks the namespaced PROJECT.md, not the root one', (t) => {
const tmpDir = createTempProject('gsd-3749-init-');
t.after(() => cleanup(tmpDir));
fs.mkdirSync(path.join(tmpDir, '.planning', 'second-product'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, '.planning', 'second-product', 'PROJECT.md'), '# Second Product\n');
const r1 = runGsdTools(['query', 'init.new-project'], tmpDir, { GSD_PROJECT: 'second-product' });
assert.ok(r1.success, r1.error);
const out1 = JSON.parse(r1.output);
assert.equal(out1['project_exists'], true,
`#3749: project_path (${out1['project_path']}) names an existing file — project_exists must be true`);
// project_path is POSIX-normalized by toPosixPath — compare with a literal
// forward-slash path, not path.join (which yields backslashes on Windows).
assert.ok(String(out1['project_path']).includes('.planning/second-product'));
// An unrelated root PROJECT.md must not change the verdict.
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# unrelated\n');
const r2 = runGsdTools(['query', 'init.new-project'], tmpDir, { GSD_PROJECT: 'second-product' });
assert.ok(r2.success, r2.error);
assert.equal(JSON.parse(r2.output)['project_exists'], true,
'#3749: verdict must not flip when an unrelated root file appears');
});
test('without GSD_PROJECT the root PROJECT.md still answers project_exists', (t) => {
const tmpDir = createTempProject('gsd-3749-init2-');
t.after(() => cleanup(tmpDir));
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Root Project\n');
const r = runGsdTools(['query', 'init.new-project'], tmpDir);
assert.ok(r.success, r.error);
assert.equal(JSON.parse(r.output)['project_exists'], true, 'default (unscoped) behavior unchanged');
});
});