Folds 15 tests/fix-*.test.cjs regression files (191 test() blocks) into their module's main suite, per the wave decomposition of #3315 (H3 of epic #3053). 187 blocks land in 8 existing suites (4 exact-duplicate cases dropped, documented inline); 4 blocks move via git mv into 2 new suite files with no prior coverage to merge into. Zero production behavior change. Also tightens two H1 (#3313) ratchets that the fold's own file-count reduction moved past their grace window, per the ratchets' documented dual failure mode (a stale/too-loose baseline fails exactly like a novel violation): - lint-test-file-count.allowlist.json: removes the stale "audit" entry (folding fix-2766 into tests/uat.test.cjs drops that module back to its 2-file cap). - lint-allow-test-rule-refs.ceiling.json: lowers maxFiles 314 -> 309, the real post-fold high-water mark (gsd-test's own repo-baseline test caught this — CI, not a human, found it). Two orthogonal review passes (Standards+Spec code-review, isolated security-review) found and this commit fixes two issues before push: a genuinely-distinct #2287 test case (file-absent vs. file-present- resolved) that a prior fold pass had wrongly dropped as a duplicate — restored verbatim into tests/uat.test.cjs; and a missing same-line allow-test-rule citation on the #2196 block in tests/debug-session-management.test.cjs, added for consistency with its sibling #2257 block. lint-removed-but-needed also caught two stale doc references to the now-folded-away fix-2285-claude-orchestration-wiring.test.cjs filename (docs/adr/1143-claude-orchestration-capability.md, gsd-core/references/execute-phase-response-language.md) — updated both to point at tests/claude-orchestration.test.cjs, its new home. Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -244,3 +244,172 @@ describe('debug skill dispatch and sub-orchestrator (#2148, #2151)', () => {
|
||||
'debug.md session_params must not hardcode .planning/debug/... as debug_file_path (#2376)');
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for #2196 (folded from tests/fix-2196-debug-agent-handoff.test.cjs):
|
||||
// the /gsd-debug orchestrator misused the foreground session-manager Agent()
|
||||
// spawn as a background task, then queried the returned agent ID via
|
||||
// TaskOutput (which expects a task ID) — yielding "No task found with ID"
|
||||
// and leaving the workflow waiting on a handoff that was never queryable,
|
||||
// with no recovery. The fix makes debug.md state explicitly that the spawn
|
||||
// is foreground/blocking, that an agent ID must never be passed to
|
||||
// TaskOutput, and that a lost handoff must be recovered (preserve
|
||||
// checkpoint + resume).
|
||||
describe('#2196 debug.md session-manager spawn contract', () => {
|
||||
// allow-test-rule: source-text-is-the-product (#2196)
|
||||
const DEBUG_MD_2196 = path.join(__dirname, '..', 'gsd-core', 'workflows', 'debug.md');
|
||||
const content = fs.readFileSync(DEBUG_MD_2196, 'utf-8');
|
||||
const sectionStart = content.indexOf('Session Management');
|
||||
const section = sectionStart !== -1 ? content.slice(sectionStart) : '';
|
||||
|
||||
test('debug.md has the Session Management section', () => {
|
||||
assert.notEqual(sectionStart, -1, 'debug.md must contain the Session Management section');
|
||||
});
|
||||
|
||||
test('the session-manager spawn is declared foreground/blocking (not backgrounded)', () => {
|
||||
assert.ok(/foreground/i.test(section) && /blocking/i.test(section),
|
||||
'the Agent() spawn must be declared foreground and blocking so it is not polled');
|
||||
});
|
||||
|
||||
test('an agent ID must not be passed to TaskOutput', () => {
|
||||
assert.ok(/TaskOutput/.test(section),
|
||||
'the contract must mention TaskOutput by name');
|
||||
assert.ok(/agent ID is NOT a task ID|agent ID is not a task ID/i.test(section),
|
||||
'the contract must state an agent ID is not a task ID');
|
||||
});
|
||||
|
||||
test('a lost handoff has a recovery path (preserve checkpoint + resume)', () => {
|
||||
// Pin the CANONICAL colon form — the retired /gsd-debug hyphen syntax is
|
||||
// rejected by the slash-command-namespace guard, so this must be /gsd:debug.
|
||||
assert.ok(/\/gsd:debug continue \{slug\}/.test(section),
|
||||
'the contract must point to /gsd:debug continue {slug} (canonical colon form) as the resume path');
|
||||
assert.ok(/do not claim|do NOT claim/i.test(section),
|
||||
'the contract must forbid claiming a lost-handoff session is still running');
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for #2257 (folded from tests/fix-2257-debug-nonterminal-resume.test.cjs):
|
||||
// the /gsd-debug orchestrator had no contract for a foreground
|
||||
// gsd-debug-session-manager return that is usable but non-terminal. Section 4
|
||||
// "Session Management" (and the `continue` subcommand's return handling in
|
||||
// Section 1c) recognized only two literal-string returns — `DEBUG SESSION
|
||||
// COMPLETE` and `ABANDONED` — with no else branch. Any other return (e.g. a
|
||||
// mid-investigation progress summary emitted when the manager's own
|
||||
// turn/context budget runs out) matched neither and fell through to the user
|
||||
// as if the debug session were complete, silently abandoning the
|
||||
// investigation mid-flight.
|
||||
//
|
||||
// The fix defines an explicit non-terminal marker, `CONTINUE_REQUIRED`, that
|
||||
// the session manager emits when it must stop before reaching a terminal
|
||||
// state (distinct from the two terminal returns and from a genuine
|
||||
// user-input/approval `CHECKPOINT REACHED`, which already correctly pauses
|
||||
// via AskUserQuestion). The orchestrator treats anything that is not one of
|
||||
// the two terminal markers as non-terminal and auto-resumes by re-spawning
|
||||
// the session manager from the on-disk checkpoint, bounded by an anti-loop
|
||||
// guard.
|
||||
//
|
||||
// Correction (orthogonal review): the first cut of the anti-loop guard
|
||||
// required BOTH `next_action` AND `updated` to be unchanged across two
|
||||
// resumes to detect no-progress — but `agents/gsd-debugger.md` overwrites
|
||||
// `updated` on every checkpoint write ("Update the file BEFORE taking
|
||||
// action"), so `updated` changes every cycle and the AND-condition could
|
||||
// never be true, making the guard dead (unbounded auto-resume / DoS
|
||||
// regression). The corrected guard keys no-progress detection off
|
||||
// `next_action` ALONE and adds an absolute, content-independent hard cap of
|
||||
// 3 total auto-resumes per slug per `/gsd:debug` invocation as the real
|
||||
// termination bound.
|
||||
describe('#2257 debug non-terminal session-manager return contract', () => {
|
||||
const DEBUG_MD_2257 = path.join(__dirname, '..', 'gsd-core', 'workflows', 'debug.md');
|
||||
const SESSION_MANAGER_MD = path.join(__dirname, '..', 'agents', 'gsd-debug-session-manager.md');
|
||||
|
||||
// allow-test-rule: source-text-is-the-product (#2257)
|
||||
// workflow/agent prose IS the runtime contract under test
|
||||
const debugContent = fs.readFileSync(DEBUG_MD_2257, 'utf-8');
|
||||
// allow-test-rule: source-text-is-the-product (#2257)
|
||||
// workflow/agent prose IS the runtime contract under test
|
||||
const managerContent = fs.readFileSync(SESSION_MANAGER_MD, 'utf-8');
|
||||
|
||||
const section4Start = debugContent.indexOf('## 4. Session Management');
|
||||
const section4 = section4Start !== -1 ? debugContent.slice(section4Start) : '';
|
||||
|
||||
const section1cStart = debugContent.indexOf('## 1c. CONTINUE subcommand');
|
||||
const section1dStart = debugContent.indexOf('## 1d. Check Active Sessions');
|
||||
const section1c =
|
||||
section1cStart !== -1 && section1dStart !== -1
|
||||
? debugContent.slice(section1cStart, section1dStart)
|
||||
: '';
|
||||
|
||||
test('debug.md has Section 4 (Session Management) and Section 1c (CONTINUE subcommand)', () => {
|
||||
assert.notEqual(section4Start, -1, 'debug.md must contain Section 4 Session Management');
|
||||
assert.notEqual(section1cStart, -1, 'debug.md must contain Section 1c CONTINUE subcommand');
|
||||
});
|
||||
|
||||
test('Section 4 has an exhaustive non-terminal branch that auto-resumes from the checkpoint', () => {
|
||||
assert.ok(/CONTINUE_REQUIRED/.test(section4),
|
||||
'Section 4 must reference the CONTINUE_REQUIRED non-terminal marker');
|
||||
assert.ok(/ANYTHING ELSE/i.test(section4),
|
||||
'Section 4 must exhaustively catch any return that is not one of the two terminal markers');
|
||||
assert.ok(/AUTO-RESUME/i.test(section4) && /re-spawning/i.test(section4),
|
||||
'Section 4 must auto-resume by re-spawning the session manager, not return control to the user');
|
||||
assert.ok(/same.{0,20}slug/i.test(section4),
|
||||
'Section 4 auto-resume must use the SAME slug/checkpoint as the original spawn');
|
||||
});
|
||||
|
||||
test('Section 1c has the same exhaustive non-terminal auto-resume branch (not just the two literals)', () => {
|
||||
assert.ok(/CONTINUE_REQUIRED/.test(section1c),
|
||||
'Section 1c must reference the CONTINUE_REQUIRED non-terminal marker');
|
||||
assert.ok(/ANYTHING ELSE/i.test(section1c),
|
||||
'Section 1c must exhaustively catch any return that is not one of the two terminal markers');
|
||||
assert.ok(/AUTO-RESUME/i.test(section1c) && /re-spawning/i.test(section1c),
|
||||
'Section 1c must auto-resume by re-spawning the session manager, not return control to the user');
|
||||
assert.ok(/same.{0,20}slug/i.test(section1c),
|
||||
'Section 1c auto-resume must use the SAME slug/checkpoint as the original spawn (symmetric with Section 4)');
|
||||
});
|
||||
|
||||
test('gsd-debug-session-manager.md defines CONTINUE_REQUIRED distinct from the two terminal formats', () => {
|
||||
assert.ok(/## CONTINUE_REQUIRED/.test(managerContent),
|
||||
'the agent must define an explicit ## CONTINUE_REQUIRED return heading');
|
||||
assert.ok(/## DEBUG SESSION COMPLETE/.test(managerContent),
|
||||
'the terminal DEBUG SESSION COMPLETE format must still be present');
|
||||
assert.ok(/ABANDONED/.test(managerContent),
|
||||
'the terminal ABANDONED format must still be present');
|
||||
assert.ok(/non-terminal/i.test(managerContent),
|
||||
'the agent must characterize CONTINUE_REQUIRED as non-terminal');
|
||||
assert.ok(/CHECKPOINT REACHED/.test(managerContent) && /distinct from/i.test(managerContent),
|
||||
'CONTINUE_REQUIRED must be explicitly distinguished from the genuine user-input CHECKPOINT REACHED shape');
|
||||
assert.ok(/\.planning\/debug\/\{slug\}\.md/.test(managerContent),
|
||||
'CONTINUE_REQUIRED must reference the on-disk checkpoint path');
|
||||
assert.ok(/next_action/.test(managerContent) && /status/.test(managerContent),
|
||||
'CONTINUE_REQUIRED must reference the checkpoint status/next_action fields');
|
||||
});
|
||||
|
||||
test('an anti-loop bound exists so repeated no-progress auto-resumes do not loop indefinitely', () => {
|
||||
assert.ok(/anti-loop guard/i.test(section4),
|
||||
'Section 4 must name an anti-loop guard');
|
||||
assert.ok(/blocker report/i.test(section4),
|
||||
'Section 4 must emit a blocker report to the user once the bound is exceeded, instead of looping forever');
|
||||
|
||||
assert.ok(/anti-loop guard/i.test(section1c),
|
||||
'Section 1c must name an anti-loop guard');
|
||||
});
|
||||
|
||||
test('the anti-loop guard has an absolute hard cap independent of no-progress detection (#2257 correction)', () => {
|
||||
for (const [label, section] of [['Section 4', section4], ['Section 1c', section1c]]) {
|
||||
assert.ok(/hard cap/i.test(section), `${label} must name an absolute hard cap`);
|
||||
assert.ok(/\b3\b/.test(section) && /total auto-resumes/i.test(section),
|
||||
`${label} must encode a concrete numeric cap of 3 total auto-resumes`);
|
||||
assert.ok(/regardless/i.test(section),
|
||||
`${label} hard cap must trip regardless of whether next_action changed (content-independent)`);
|
||||
}
|
||||
});
|
||||
|
||||
test('no-progress detection keys off next_action alone, never the always-changing updated timestamp (#2257 correction)', () => {
|
||||
for (const [label, section] of [['Section 4', section4], ['Section 1c', section1c]]) {
|
||||
assert.ok(/next_action/.test(section),
|
||||
`${label} no-progress heuristic must reference next_action`);
|
||||
assert.ok(/(do not|never).{0,40}updated/i.test(section),
|
||||
`${label} must explicitly forbid keying no-progress detection off updated`);
|
||||
assert.ok(/changes every cycle/i.test(section),
|
||||
`${label} must state WHY updated cannot be used: it is overwritten/changes every checkpoint cycle`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user