* feat(#3415): ship local/no-unbounded-quantifier, burn down ReDoS class Phase 4 of epic #3212 (ADR-3212 §5/§7, the final phase). New rule flags an unbounded */+/{n,} quantifier over a broad character class ([\s\S], dotAll ., or a 1-2-unit negated class like [^\n]/[^)\n] — the exact #2128-fixed shape) applied to a regex whose match target is data-flow-traced to readFileSync content. eslint-rules/lib/readfilesync-trace.cjs extracts the data-flow tracer shared with no-crlf-fragile-split (Phase 2) rather than a second copy — no-crlf-fragile-split refactored onto it with zero behavior change, parity-tested. Real triage, not 798 mechanical edits: the ADR's census (2026-08-08) screened every unbounded quantifier in the tree unscoped. Correctly scoped to readFileSync-derived content (matching Phase 2's own G2/G3 scoping), the rule found 162 real hits across two detection waves — the second wave (93) surfaced only after a genuine off-by-one bug in this rule's own first draft was caught while writing its RuleTester tests and fixed (the bug silently missed every directly-quantified [\s\S]* with no gap before the quantifier — exactly the class this rule exists to catch). 3 hits landed in production src/ (commands.cts, milestone.cts, roadmap.cts) and were each empirically timed against adversarial input (matching #2128's own measured-not-assumed precedent) — all confirmed linear-time/benign, left unbounded with a measured-evidence comment rather than mechanically bounded. The remaining 159 are test-file fixture parsing (test-author-controlled, fixed-size content, not adversarial input) — each suppressed with a specific, non-generic reason. Zero functional behavior changed anywhere in this diff. tests/no-pending-3212-markers.test.cjs locks the epic's own closing invariant (ADR §7: "assert zero pending #3212 markers remain") — ground truth confirmed trivially true today (no phase left any such marker behind), now regression-locked going forward. Design: .gsd/phase/chore-3415-prohibition-with-teeth/40-design.md Test matrix: .gsd/phase/chore-3415-prohibition-with-teeth/50-test-matrix.md Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3415): correct rule category mislabel, add CI test-scope entry An orthogonal Standards-axis review found eslint-rules/no-unbounded-quantifier.cjs mistakenly carried meta.docs.category: 'Portability', copied from a sibling rule without realizing what that implied: docs/contributing/cross-platform- portability-rules.md governs an ADR-1703 rule family under a hard "zero escape hatches" contract (tests/portability-rule-disable-ban.test.cjs's PROTECTED_RULES bans eslint-disable for those rules entirely). This rule is not part of that family — it's ADR-3212 (ReDoS/CWE-1333), a different epic — and its eslint-disable-next-line suppressions (159 of them, added earlier this same phase after empirical benign-verification) are an intentional, correct design, not a bypass. Corrected to category: 'Best Practices', matching the actual precedent (no-adhoc-regex-escape.cjs, Phase 1 of the same epic, which is also correctly outside PROTECTED_RULES), and the rule's own docstring now states this explicitly so a future reader doesn't have to re-derive it. Also registers a new scripts/ci-test-scope.cjs bucket so editing this rule or the shared eslint-rules/lib/readfilesync-trace.cjs helper re-runs their own test suites under targeted CI selection — was previously unregistered and invisible to that fast-path (this PR's own gsd-test checkpoint runs the full suite regardless, so this only affects future narrowly-scoped PRs). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3415): bound no-unbounded-quantifier's own scanner (CWE-1333, ironic) Security review found the rule meant to catch algorithmic-complexity bugs had one of its own: hasUnboundedBroadQuantifier's negated-class inner scan walked from each `[^` occurrence to the next `]` (or EOF) with no bound, while the outer loop only ever advanced by one character — O(n²) total work on a pattern with many unclosed `[^` runs. Runs unconditionally inside checkPattern on any `new RegExp('literal string')` argument in any linted file, before the (cheap) readFileSync data-flow gate — so a single crafted string literal, no valid regex syntax required, could make `npm run lint` / CI hang. Empirically confirmed both the bug and the fix: pre-fix, n=4000/8000/ 16000/32000 chars took 30.8/115.6/463.8/1874.3ms (~4x work per 2x n, quadratic); extrapolated, the 300000-char repro from the finding would run ~165s. Post-fix (bail the inner scan once units exceeds the rule's own 1-2-unit scope, rather than continuing to hunt for a closing `]`), the same 300000-char input runs in 8.7ms via the real rule module, independently reconfirmed at 18ms via a fresh Linter.verify() call. New regression row in tests/no-unbounded-quantifier.rule.test.cjs asserts the RuleTester run on a 50000-char adversarial pattern completes and returns a defined result — no wall-clock assertion (CLAUDE.md Clock Seams / local/no-elapsed-assertion). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3415): triage 3 new sites, re-raise ceiling after upstream batch next merged 12 more PRs during this PR's review. Two consequences: - tests/edit-phase.test.cjs (fix #3262, unrelated) added 3 new content.match(/<tag>([\s\S]*?)<\/tag>/) reads of this repo's own workflow .md content — the same Class A pattern as the ~159 sites already triaged elsewhere in this PR. Suppressed with the same established reason. - lint-allow-test-rule-refs' ratchet ceiling needed re-raising again (301 -> 303) for the same reason as the two prior bumps: organic growth from unrelated, already-reviewed PRs landing concurrently, not a defect in this branch's own diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
417 lines
22 KiB
JavaScript
417 lines
22 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// Workflow .md / agent .md / command .md / reference .md files — their text
|
|
// IS what the runtime loads. Testing text content tests the deployed contract.
|
|
// Per CONTRIBUTING.md exception matrix.
|
|
'use strict';
|
|
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
describe('debug session management implementation', () => {
|
|
test('DEBUG.md template contains reasoning_checkpoint field', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'gsd-core/templates/DEBUG.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(content.includes('reasoning_checkpoint'), 'DEBUG.md must contain reasoning_checkpoint field');
|
|
});
|
|
|
|
test('DEBUG.md template contains tdd_checkpoint field', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'gsd-core/templates/DEBUG.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(content.includes('tdd_checkpoint'), 'DEBUG.md must contain tdd_checkpoint field');
|
|
});
|
|
|
|
test('debug command contains list subcommand logic', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'gsd-core/workflows/debug.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(
|
|
content.includes('SUBCMD=list') || content.includes('"list"'),
|
|
'debug.md must contain list subcommand logic'
|
|
);
|
|
});
|
|
|
|
test('debug command contains continue subcommand logic', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'gsd-core/workflows/debug.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(
|
|
content.includes('SUBCMD=continue') || content.includes('"continue"'),
|
|
'debug.md must contain continue subcommand logic'
|
|
);
|
|
});
|
|
|
|
test('debug command contains status subcommand logic', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'gsd-core/workflows/debug.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(
|
|
content.includes('SUBCMD=status') || content.includes('"status"'),
|
|
'debug.md must contain status subcommand logic'
|
|
);
|
|
});
|
|
|
|
test('debug command contains TDD gate logic', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'gsd-core/workflows/debug.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(
|
|
content.includes('TDD_MODE') || content.includes('tdd_mode'),
|
|
'debug.md must contain TDD gate logic'
|
|
);
|
|
});
|
|
|
|
test('debug.md reads tdd_mode via workflow.tdd_mode key (not bare tdd_mode)', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'gsd-core/workflows/debug.md'),
|
|
'utf8'
|
|
);
|
|
// #3149: tdd_mode now arrives on the `init.debug` bundle rather than a
|
|
// `config-get` call in this workflow, so the old literal-call assertion no
|
|
// longer describes reality. The invariant it protected is unchanged and is
|
|
// asserted at its new home: `cmdInitDebug` resolves `config.workflow`'s
|
|
// `tdd_mode`, never a bare top-level key — proven behaviorally in
|
|
// tests/init-debug.test.cjs ('honors workflow.tdd_mode, ignores a bare
|
|
// top-level tdd_mode'). What must remain true HERE is only that this
|
|
// workflow never reintroduces a bare-key read of its own.
|
|
assert.ok(
|
|
!content.includes('config-get tdd_mode'),
|
|
'debug.md must not use bare "tdd_mode" key — use "workflow.tdd_mode" to match every other consumer'
|
|
);
|
|
assert.ok(
|
|
content.includes('tdd_mode'),
|
|
'debug.md must still consume tdd_mode (now from the init.debug bundle)'
|
|
);
|
|
});
|
|
|
|
test('debug command contains security hardening', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'gsd-core/workflows/debug.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(content.includes('DATA_START'), 'debug.md must contain DATA_START injection boundary marker');
|
|
});
|
|
|
|
test('debug command surfaces next_action before spawn', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'gsd-core/workflows/debug.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(
|
|
content.includes('[debug] Next:') || content.includes('next_action'),
|
|
'debug.md must surface next_action before agent spawn'
|
|
);
|
|
});
|
|
|
|
test('gsd-debugger contains structured reasoning checkpoint', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'agents/gsd-debugger.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(content.includes('reasoning_checkpoint'), 'gsd-debugger.md must contain reasoning_checkpoint');
|
|
});
|
|
|
|
test('gsd-debugger contains TDD checkpoint mode', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'agents/gsd-debugger.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(content.includes('tdd_mode'), 'gsd-debugger.md must contain tdd_mode');
|
|
assert.ok(content.includes('TDD CHECKPOINT'), 'gsd-debugger.md must contain TDD CHECKPOINT return format');
|
|
});
|
|
|
|
test('gsd-debugger contains delta debugging technique', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'agents/gsd-debugger.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(content.includes('Delta Debugging'), 'gsd-debugger.md must contain Delta Debugging technique');
|
|
});
|
|
|
|
test('gsd-debugger contains security note about DATA_START', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(process.cwd(), 'agents/gsd-debugger.md'),
|
|
'utf8'
|
|
);
|
|
assert.ok(content.includes('DATA_START'), 'gsd-debugger.md must contain DATA_START security reference');
|
|
});
|
|
});
|
|
|
|
// Tests for #2148 and #2151
|
|
describe('debug skill dispatch and sub-orchestrator (#2148, #2151)', () => {
|
|
test('gsd-debugger ROOT CAUSE FOUND format includes specialist_hint field', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'agents', 'gsd-debugger.md'), 'utf8');
|
|
assert.ok(content.includes('specialist_hint'), 'gsd-debugger missing specialist_hint in ROOT CAUSE FOUND');
|
|
assert.ok(content.includes('swift_concurrency'), 'gsd-debugger missing specialist_hint derivation guidance');
|
|
});
|
|
|
|
test('debug.md orchestrator has specialist skill dispatch step', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'gsd-core/workflows/debug.md'), 'utf8');
|
|
assert.ok(content.includes('specialist_hint'), 'debug.md missing specialist dispatch logic');
|
|
assert.ok(content.includes('typescript-expert'), 'debug.md missing skill dispatch mapping');
|
|
});
|
|
|
|
test('debug.md specialist dispatch prompt uses DATA_START/DATA_END boundaries', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'gsd-core/workflows/debug.md'), 'utf8');
|
|
assert.ok(content.includes('DATA_START') && content.includes('DATA_END'),
|
|
'debug.md specialist dispatch prompt missing security boundaries');
|
|
});
|
|
|
|
test('gsd-debug-session-manager agent exists with correct tools', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'agents', 'gsd-debug-session-manager.md'), 'utf8');
|
|
assert.ok(content.includes('Agent'), 'gsd-debug-session-manager missing Agent tool');
|
|
assert.ok(content.includes('AskUserQuestion'), 'gsd-debug-session-manager missing AskUserQuestion tool');
|
|
});
|
|
|
|
test('gsd-debug-session-manager spawns debugger with Agent() dispatcher', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'agents', 'gsd-debug-session-manager.md'), 'utf8');
|
|
assert.ok(content.includes('\nAgent('), 'session manager must dispatch debugger with Agent(');
|
|
});
|
|
|
|
test('gsd-debug-session-manager uses DATA_START/DATA_END for checkpoint responses', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'agents', 'gsd-debug-session-manager.md'), 'utf8');
|
|
assert.ok(content.includes('DATA_START') && content.includes('DATA_END'),
|
|
'gsd-debug-session-manager missing security boundaries on checkpoint responses');
|
|
});
|
|
|
|
test('gsd-debug-session-manager has compact summary output format', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'agents', 'gsd-debug-session-manager.md'), 'utf8');
|
|
assert.ok(content.includes('DEBUG SESSION COMPLETE'), 'session manager missing compact summary format');
|
|
});
|
|
|
|
test('gsd-debug-session-manager includes anti-heredoc rule', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'agents', 'gsd-debug-session-manager.md'), 'utf8');
|
|
assert.ok(content.includes('heredoc'), 'session manager missing anti-heredoc rule');
|
|
});
|
|
|
|
test('debug.md delegates to gsd-debug-session-manager', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'gsd-core', 'workflows', 'debug.md'), 'utf8');
|
|
assert.ok(content.includes('gsd-debug-session-manager'),
|
|
'debug.md does not delegate to session manager');
|
|
});
|
|
|
|
test('DEBUG.md reasoning_checkpoint field-count claim matches gsd-debugger.md YAML keys (parity)', () => {
|
|
// Parallel-surface drift guard (DEFECT.GENERATIVE-FIX): the field-count
|
|
// claim in the DEBUG.md section_rules prose must equal the number of keys
|
|
// enumerated in the gsd-debugger.md reasoning_checkpoint YAML block.
|
|
// CRLF-safe regex/split per DEFECT.WINDOWS-TEST-PORTABILITY. Accepts either
|
|
// digit ("7-field") or word ("seven-field") count form.
|
|
const NUMWORDS = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 };
|
|
const debugContent = fs.readFileSync(path.join(process.cwd(), 'gsd-core', 'templates', 'DEBUG.md'), 'utf8');
|
|
const agentContent = fs.readFileSync(path.join(process.cwd(), 'agents', 'gsd-debugger.md'), 'utf8');
|
|
const debugMatch = debugContent.match(/reasoning_checkpoint[^.\r\n]*?(\d+|one|two|three|four|five|six|seven|eight|nine|ten)-field structured reasoning record/i);
|
|
assert.ok(debugMatch, 'DEBUG.md must state a "N-field structured reasoning record" claim for reasoning_checkpoint');
|
|
const claimedCount = /^\d+$/.test(debugMatch[1]) ? parseInt(debugMatch[1], 10) : NUMWORDS[debugMatch[1].toLowerCase()];
|
|
assert.ok(typeof claimedCount === 'number', `unrecognized field-count token: ${debugMatch[1]}`);
|
|
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md content, fixed-size author-controlled content
|
|
const yamlBlock = agentContent.match(/reasoning_checkpoint:\s*\r?\n([\s\S]*?)```/);
|
|
assert.ok(yamlBlock, 'gsd-debugger.md must define a fenced reasoning_checkpoint YAML block');
|
|
const keys = new Set();
|
|
for (const line of yamlBlock[1].split(/\r?\n/)) {
|
|
// Match 2-space-indented YAML keys (with OR without an inline value —
|
|
// array-valued keys like confirming_evidence: have no trailing space).
|
|
const m = line.match(/^ {2}([a-z_]+):/);
|
|
if (m) keys.add(m[1]);
|
|
}
|
|
assert.strictEqual(
|
|
keys.size,
|
|
claimedCount,
|
|
`parity drift: DEBUG.md claims ${claimedCount}-field but gsd-debugger.md enumerates ${keys.size} keys (${[...keys].join(', ')}). Update BOTH surfaces together.`
|
|
);
|
|
});
|
|
|
|
// #2376: debug_file_path handed to the spawned gsd-debug-session-manager
|
|
// must resolve regardless of that subagent's own cwd, which may differ
|
|
// from the orchestrator's — debug.md must reference the absolute
|
|
// {debug_dir} field from `state load`, not a bare .planning/debug/...
|
|
// literal, in both the `continue` and new-session Agent() spawns.
|
|
test('debug.md session_params reference {debug_dir}/{slug}.md, not bare .planning/debug/... literals (#2376)', () => {
|
|
const content = fs.readFileSync(path.join(process.cwd(), 'gsd-core/workflows/debug.md'), 'utf8');
|
|
assert.ok(content.includes('debug_file_path: {debug_dir}/{SLUG}.md'),
|
|
'debug.md continue-subcommand spawn must reference {debug_dir}/{SLUG}.md, not a bare .planning/debug/{SLUG}.md literal');
|
|
assert.ok(content.includes('debug_file_path: {debug_dir}/{slug}.md'),
|
|
'debug.md new-session spawn must reference {debug_dir}/{slug}.md, not a bare .planning/debug/{slug}.md literal');
|
|
assert.ok(!content.includes('debug_file_path: .planning/debug/'),
|
|
'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`);
|
|
}
|
|
});
|
|
});
|