* test(#4040): add failing-first regression tests for partial-init routing Red: init.progress/init.resume/init.new-project payloads carry no partial-init discriminator, and progress.md/resume-project.md/ new-project.md route an interrupted bootstrap (.planning/PROJECT.md + config.json only) to Route F / STATE reconstruction / a hard error. * fix(#4040): route partially-created .planning to initialization recovery A bootstrap interrupted after .planning/PROJECT.md (but before REQUIREMENTS.md/ROADMAP.md/STATE.md) was mis-routed three ways: progress.md read it as between-milestones (Route F) or 'no planning structure', resume-project.md offered STATE.md reconstruction, and new-project.md errored 'already initialized' — a routing loop with no recovery exit. Add a shared buildInitCompletenessFields discriminator (planning_exists / requirements_exists / milestones_exists / init_incomplete) to the init.progress, init.resume and init.new-project payloads, and branch on init_incomplete in progress.md, resume-project.md and new-project.md BEFORE the legacy branches. MILESTONES.md presence excludes the archival between-milestones state, so Route F and the STATE-reconstruction path keep working. Emitted-Drift-Ack-Growth: progress.md — deliberate #4040 growth: new init_incomplete recovery branch (routing text + guard on the no-planning and Route F branches) added ahead of the legacy init_context routes. Emitted-Drift-Ack-Growth: resume-project.md — deliberate #4040 growth: new init_incomplete branch routing an interrupted bootstrap to initialization recovery before the STATE.md-reconstruction branch. Emitted-Drift-Ack-Growth: new-project.md — deliberate #4040 growth: project_exists gate split on init_incomplete so a partial bootstrap resumes initialization instead of erroring. * chore(#4040): add changeset fragment * chore(#4040): backfill PR number in changeset fragment --------- Co-authored-by: sim <sim@local>
96 lines
4.5 KiB
JavaScript
96 lines
4.5 KiB
JavaScript
// allow-test-rule: source-text-is-the-product (see #4040)
|
|
// Workflow .md files — their text IS what the runtime loads. Testing text
|
|
// content tests the deployed contract. Per CONTRIBUTING.md exception matrix.
|
|
|
|
/**
|
|
* GSD Tools Tests - #4040 partial-init routing contract
|
|
*
|
|
* Validates that the progress / resume-project / new-project workflows route
|
|
* an interrupted bootstrap (`init_incomplete` from the init.progress /
|
|
* init.resume payload) to initialization RECOVERY, and that this branch runs
|
|
* BEFORE the branches that mis-classified the state pre-fix:
|
|
* - progress.md Route F ("between milestones": PROJECT.md present,
|
|
* ROADMAP.md missing)
|
|
* - progress.md "no planning structure found" (project_exists=false while
|
|
* .planning/ exists)
|
|
* - resume-project.md "reconstruct STATE.md"
|
|
* - new-project.md "project already initialized" error
|
|
*
|
|
* Closes: #4040
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
describe('#4040 partial-init routing', () => {
|
|
const workflowsDir = path.join(__dirname, '..', 'gsd-core', 'workflows');
|
|
const progressMd = () => fs.readFileSync(path.join(workflowsDir, 'progress.md'), 'utf8');
|
|
const resumeMd = () => fs.readFileSync(path.join(workflowsDir, 'resume-project.md'), 'utf8');
|
|
const newProjectMd = () => fs.readFileSync(path.join(workflowsDir, 'new-project.md'), 'utf8');
|
|
|
|
test('progress.md routes init_incomplete to initialization recovery before Route F', () => {
|
|
const content = progressMd();
|
|
const recoveryIdx = content.indexOf('init_incomplete');
|
|
assert.ok(recoveryIdx > -1, 'progress.md must branch on init_incomplete');
|
|
const routeFIdx = content.indexOf('Route F: Between milestones');
|
|
assert.ok(routeFIdx > -1, 'Route F definition must exist');
|
|
assert.ok(
|
|
recoveryIdx < routeFIdx,
|
|
'the init_incomplete recovery branch must appear before the Route F (between milestones) definition'
|
|
);
|
|
const recoverySlice = content.slice(recoveryIdx, recoveryIdx + 1200);
|
|
assert.ok(
|
|
/REQUIREMENTS\.md/.test(recoverySlice) && /ROADMAP\.md/.test(recoverySlice) && /STATE\.md/.test(recoverySlice),
|
|
'the recovery branch must name the missing REQUIREMENTS.md / ROADMAP.md / STATE.md artifacts'
|
|
);
|
|
assert.ok(
|
|
recoverySlice.includes('new-project'),
|
|
'the recovery branch must route to /gsd:new-project to resume initialization'
|
|
);
|
|
});
|
|
|
|
test('progress.md init_context no-planning branch is gated on planning_exists', () => {
|
|
const content = progressMd();
|
|
const noPlanningIdx = content.indexOf('No planning structure found');
|
|
assert.ok(noPlanningIdx > -1, 'no-planning branch must exist');
|
|
const branchSlice = content.slice(Math.max(0, noPlanningIdx - 600), noPlanningIdx);
|
|
assert.ok(
|
|
branchSlice.includes('planning_exists'),
|
|
'the no-planning branch must check planning_exists so an existing-but-partial .planning/ is not reported as absent'
|
|
);
|
|
});
|
|
|
|
test('resume-project.md routes init_incomplete to initialization recovery before STATE reconstruction', () => {
|
|
const content = resumeMd();
|
|
const recoveryIdx = content.indexOf('init_incomplete');
|
|
assert.ok(recoveryIdx > -1, 'resume-project.md must branch on init_incomplete');
|
|
const reconstructIdx = content.indexOf('Offer to reconstruct STATE.md');
|
|
assert.ok(reconstructIdx > -1, 'STATE reconstruction branch must exist');
|
|
assert.ok(
|
|
recoveryIdx < reconstructIdx,
|
|
'the init_incomplete recovery branch must appear before the reconstruct-STATE.md branch'
|
|
);
|
|
const parseIdx = content.indexOf('Parse JSON for');
|
|
const parseSlice = content.slice(parseIdx, parseIdx + 400);
|
|
assert.ok(
|
|
parseSlice.includes('init_incomplete'),
|
|
'the resume parse list must include init_incomplete (payload is the source of truth)'
|
|
);
|
|
});
|
|
|
|
test('new-project.md resumes initialization when init_incomplete instead of erroring', () => {
|
|
const content = newProjectMd();
|
|
const gateIdx = content.indexOf('project_already');
|
|
const gateText = 'If `project_exists` is true';
|
|
const gate = gateIdx > -1 ? gateIdx : content.indexOf(gateText);
|
|
assert.ok(gate > -1, 'the project_exists gate must exist');
|
|
const gateSlice = content.slice(gate, gate + 1500);
|
|
assert.ok(
|
|
gateSlice.includes('init_incomplete'),
|
|
'the project_exists gate must discriminate on init_incomplete (resume a partial bootstrap instead of erroring)'
|
|
);
|
|
});
|
|
});
|