test(#1976): consolidate 17 agent regression tests into agent-contract suites
Fold 17 issue-named agent-markdown regression files into their canonical agent suites: agent-frontmatter (shared agent-body-contract catch-all: read-loop guards, retired-slash-ref bans, sycophancy hardening, doc-writer/code-fixer/review-fix/mapper contracts, write-truncation), planner-language-regression (planner directive/grep/phase contract), executor-mvp-tdd-section, verifier-behavior-unverified, intel, and research-agent-profiles. Verbatim block-scoped describe wrappers; 98 subtests conserved 1:1. All unit (markdown-content) tests — no CLI/env surface. No new files. Regenerates regression-name allowlist (222->207), ratchets file-count allowlist (intel entry removed, phase 4->3), makes 13 relocated allow-test-rule exemptions issue-ref- compliant (ADR-456; prunes stale ids). No CONTEXT.md/ADR refs named the moved files. lint:ci green. Part of epic #1969. Closes #1976. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,28 +17,17 @@
|
||||
"tests/bug-131-release-tarball-smoke-explicit-home.test.cjs :: integration-test-input",
|
||||
"tests/bug-211-launcher-home-fallback.test.cjs :: structural/behavioral regression for the ~/.claude fallback arm in",
|
||||
"tests/bug-2136-sh-hook-version.test.cjs :: structural-regression-guard",
|
||||
"tests/bug-214-phase-researcher-write-truncation-contract.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-214-writer-agents-write-truncation-contract.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-2346-agent-read-loop-guards.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-2419-project-researcher-agent.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-2421-planner-grep-gate-hygiene.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-2543-gsd-slash-namespace.test.cjs :: structural-regression-guard",
|
||||
"tests/bug-2559-stale-search-year.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-2686-review-fix-worktree.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-2772-gitmodules-path-intersection.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-2808-skill-hyphen-name.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-2839-review-fix-transactional-cleanup.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-2990-code-fixer-worktree-branch.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3097-3099-executor-worktree-path-safety.test.cjs :: reads markdown product files (gsd-executor.md, worktree-path-safety.md) to verify structural protocol — not source-grep",
|
||||
"tests/bug-3290-intel-updater-layout-block.test.cjs :: source-text-is-the-product — agents/gsd-intel-updater.md IS",
|
||||
"tests/bug-33-settings-model-profile-adaptive.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3384-secondary-defects.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3430-planner-phase-contract.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3446-resume-continue-here-discovery.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3491-nested-git-worktree.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3523-cjs-loadconfig-branching-strategy-warning.test.cjs :: validates runtime CLI stdout/stderr warning behavior, not source grep",
|
||||
"tests/bug-3542-executor-git-stash-prohibition.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3605-stale-research-insert-phase-agent-refs.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3677-agent-colon-namespace-leak.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3678-executor-commit-docs-respect.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3683-command-colon-namespace-leak.test.cjs :: source-text-is-the-product",
|
||||
@@ -46,7 +35,6 @@
|
||||
"tests/bug-3689-resume-glob-nomatch.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-3810-no-gsd-sdk-runtime-refs.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-444-resolver-local-claude-install.test.cjs :: structural/behavioral regression for the repo-local .claude/ install",
|
||||
"tests/bug-571-doc-writer-fix-mode-edit-only.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-619-codebase-drift-gate-shim.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-622-graphify-optional-graph-html.test.cjs :: source-text-is-the-product",
|
||||
"tests/bug-630-wave-cleanup-orchestrator-root.test.cjs :: source-text-is-the-product",
|
||||
@@ -91,7 +79,6 @@
|
||||
"tests/edge-probe-spec-phase-contract.test.cjs :: runtime-contract-is-the-product — spec-phase.md Step 5.5 is the deployed workflow runtime contract under assertion",
|
||||
"tests/edit-phase.test.cjs :: source-text-is-the-product",
|
||||
"tests/enh-2380-sync-skills.test.cjs :: source-text-is-the-product",
|
||||
"tests/enh-2500-codebase-mapper-arch-rich-format.test.cjs :: source-text-is-the-product",
|
||||
"tests/enh-2789-description-budget.test.cjs :: source-text-is-the-product",
|
||||
"tests/enh-2790-skill-consolidation.test.cjs :: source-text-is-the-product",
|
||||
"tests/enh-48-cwd-drift-guard-e2e.test.cjs :: integration-test-input",
|
||||
|
||||
@@ -6,37 +6,24 @@
|
||||
"bug-21-state-md-template-frontmatter.test.cjs",
|
||||
"bug-211-launcher-home-fallback.test.cjs",
|
||||
"bug-2136-sh-hook-version.test.cjs",
|
||||
"bug-214-phase-researcher-write-truncation-contract.test.cjs",
|
||||
"bug-214-writer-agents-write-truncation-contract.test.cjs",
|
||||
"bug-2344-read-guard-claudecode-env.test.cjs",
|
||||
"bug-2346-agent-read-loop-guards.test.cjs",
|
||||
"bug-2351-intel-kilo-layout.test.cjs",
|
||||
"bug-2419-project-researcher-agent.test.cjs",
|
||||
"bug-2421-planner-grep-gate-hygiene.test.cjs",
|
||||
"bug-2451-context-monitor-over-report.test.cjs",
|
||||
"bug-2520-read-guard-hook-subprocess-env.test.cjs",
|
||||
"bug-2543-gsd-slash-namespace.test.cjs",
|
||||
"bug-2559-stale-search-year.test.cjs",
|
||||
"bug-260-worktree-path-guard.test.cjs",
|
||||
"bug-261-worktree-force-add-guard.test.cjs",
|
||||
"bug-2686-review-fix-worktree.test.cjs",
|
||||
"bug-2772-gitmodules-path-intersection.test.cjs",
|
||||
"bug-2808-skill-hyphen-name.test.cjs",
|
||||
"bug-2839-review-fix-transactional-cleanup.test.cjs",
|
||||
"bug-2866-codex-strip-no-trailing-newline.test.cjs",
|
||||
"bug-2876-skill-frontmatter-quote.test.cjs",
|
||||
"bug-2916-handle-branching-default-base.test.cjs",
|
||||
"bug-2990-code-fixer-worktree-branch.test.cjs",
|
||||
"bug-2995-post-install-script-paths.test.cjs",
|
||||
"bug-3019-help-passthrough.test.cjs",
|
||||
"bug-3054-stale-gsd-next-references.test.cjs",
|
||||
"bug-3087-planner-directive-language.test.cjs",
|
||||
"bug-3097-3099-executor-worktree-path-safety.test.cjs",
|
||||
"bug-3290-intel-updater-layout-block.test.cjs",
|
||||
"bug-33-settings-model-profile-adaptive.test.cjs",
|
||||
"bug-3321-verifier-runs-probes.test.cjs",
|
||||
"bug-3384-secondary-defects.test.cjs",
|
||||
"bug-3430-planner-phase-contract.test.cjs",
|
||||
"bug-3442-shim-projection-drift-guard.test.cjs",
|
||||
"bug-3446-resume-continue-here-discovery.test.cjs",
|
||||
"bug-3491-nested-git-worktree.test.cjs",
|
||||
@@ -44,7 +31,6 @@
|
||||
"bug-3523-cjs-loadconfig-branching-strategy-warning.test.cjs",
|
||||
"bug-3542-executor-git-stash-prohibition.test.cjs",
|
||||
"bug-3588-npm-audit-clean.test.cjs",
|
||||
"bug-3605-stale-research-insert-phase-agent-refs.test.cjs",
|
||||
"bug-3668-workflow-runtime-resolution.test.cjs",
|
||||
"bug-3677-agent-colon-namespace-leak.test.cjs",
|
||||
"bug-3678-executor-commit-docs-respect.test.cjs",
|
||||
@@ -53,7 +39,6 @@
|
||||
"bug-3689-resume-glob-nomatch.test.cjs",
|
||||
"bug-3810-no-gsd-sdk-runtime-refs.test.cjs",
|
||||
"bug-444-resolver-local-claude-install.test.cjs",
|
||||
"bug-571-doc-writer-fix-mode-edit-only.test.cjs",
|
||||
"bug-619-codebase-drift-gate-shim.test.cjs",
|
||||
"bug-622-graphify-optional-graph-html.test.cjs",
|
||||
"bug-630-wave-cleanup-orchestrator-root.test.cjs",
|
||||
|
||||
@@ -21,15 +21,6 @@
|
||||
],
|
||||
"issue": "622"
|
||||
},
|
||||
"intel": {
|
||||
"files": [
|
||||
"bug-2351-intel-kilo-layout.test.cjs",
|
||||
"bug-3290-intel-updater-layout-block.test.cjs",
|
||||
"intel-command-cutover.test.cjs",
|
||||
"intel.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
},
|
||||
"milestone": {
|
||||
"files": [
|
||||
"milestone-archive.test.cjs",
|
||||
@@ -40,14 +31,6 @@
|
||||
],
|
||||
"issue": "TBD"
|
||||
},
|
||||
"phase": {
|
||||
"files": [
|
||||
"bug-214-phase-researcher-write-truncation-contract.test.cjs",
|
||||
"phase-dependency-levels.test.cjs",
|
||||
"phase.test.cjs"
|
||||
],
|
||||
"issue": "597"
|
||||
},
|
||||
"roadmap": {
|
||||
"files": [
|
||||
"roadmap-mode-field.test.cjs",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,61 +0,0 @@
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const RESEARCHER_PATH = path.join(REPO_ROOT, 'agents', 'gsd-phase-researcher.md');
|
||||
|
||||
function readResearcherPrompt() {
|
||||
return fs.readFileSync(RESEARCHER_PATH, 'utf8');
|
||||
}
|
||||
|
||||
describe('bug #214: phase researcher must survive OpenCode write-tool truncation', () => {
|
||||
test('Step 6 documents the large-file / truncation fallback write contract', () => {
|
||||
const prompt = readResearcherPrompt();
|
||||
|
||||
assert.match(
|
||||
prompt,
|
||||
/truncat/i,
|
||||
'Step 6 must name the truncation failure mode.'
|
||||
);
|
||||
assert.match(
|
||||
prompt,
|
||||
/incrementa/i,
|
||||
'Step 6 must instruct incremental construction on large files.'
|
||||
);
|
||||
assert.match(
|
||||
prompt,
|
||||
/<!-- gsd:write-continue -->/,
|
||||
'Step 6 must define the continuation sentinel for incremental writes.'
|
||||
);
|
||||
assert.match(
|
||||
prompt,
|
||||
/do NOT retry the same oversized call/i,
|
||||
'Step 6 must forbid identical retry of the oversized write (doom-loop guard).'
|
||||
);
|
||||
assert.match(
|
||||
prompt,
|
||||
/do NOT silently fall back to returning content/i,
|
||||
'Step 6 must forbid silent fallback to returning content.'
|
||||
);
|
||||
assert.match(
|
||||
prompt,
|
||||
/`Read` the file, then `Edit`/i,
|
||||
'Step 6 must require Read before Edit (OpenCode edit requires a prior Read).'
|
||||
);
|
||||
assert.match(
|
||||
prompt,
|
||||
/no trailing sentinel/i,
|
||||
'Step 6 must instruct removing the sentinel on the final section.'
|
||||
);
|
||||
assert.match(
|
||||
prompt,
|
||||
/write the whole file in a single `Write` call/i,
|
||||
'Step 6 must keep single-Write as the default path (no regression for non-truncating runtimes).'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
// Every agent that writes a large file in a single Write call must carry the
|
||||
// same truncation-resilient write contract added for bug #214. OpenCode shares
|
||||
// OUTPUT_TOKEN_MAX=32000 with the thinking budget (upstream opencode#18108), so
|
||||
// an oversized single `write` tool call is truncated mid-payload, yielding
|
||||
// `JSON Parse error: Expected '}'`, and OpenCode then doom-loops. gsd-phase-researcher
|
||||
// is locked by its own bug-214 test; this locks the other large-file writers.
|
||||
const WRITER_AGENTS = [
|
||||
'gsd-research-synthesizer',
|
||||
'gsd-planner',
|
||||
'gsd-executor',
|
||||
'gsd-domain-researcher',
|
||||
'gsd-project-researcher',
|
||||
'gsd-ui-researcher',
|
||||
];
|
||||
|
||||
function readAgent(name) {
|
||||
return fs.readFileSync(path.join(REPO_ROOT, 'agents', `${name}.md`), 'utf8');
|
||||
}
|
||||
|
||||
describe('bug #214: large-file writer agents must survive write-tool truncation', () => {
|
||||
for (const name of WRITER_AGENTS) {
|
||||
describe(name, () => {
|
||||
const prompt = readAgent(name);
|
||||
|
||||
test('keeps single-Write as the default path', () => {
|
||||
assert.match(
|
||||
prompt,
|
||||
/in a single `Write` call/i,
|
||||
`${name}: must keep single-Write as the default (no regression for non-truncating runtimes).`
|
||||
);
|
||||
});
|
||||
|
||||
test('names the truncation failure mode', () => {
|
||||
assert.match(prompt, /truncat/i, `${name}: must name the truncation failure mode.`);
|
||||
});
|
||||
|
||||
test('instructs incremental construction on large files', () => {
|
||||
assert.match(
|
||||
prompt,
|
||||
/incrementa/i,
|
||||
`${name}: must instruct incremental construction on large files.`
|
||||
);
|
||||
});
|
||||
|
||||
test('defines the continuation sentinel', () => {
|
||||
assert.match(
|
||||
prompt,
|
||||
/<!-- gsd:write-continue -->/,
|
||||
`${name}: must define the continuation sentinel for incremental writes.`
|
||||
);
|
||||
});
|
||||
|
||||
test('forbids identical retry of the oversized write (doom-loop guard)', () => {
|
||||
assert.match(
|
||||
prompt,
|
||||
/do NOT retry the same oversized call/i,
|
||||
`${name}: must forbid identical retry of the oversized write.`
|
||||
);
|
||||
});
|
||||
|
||||
test('requires Read before Edit', () => {
|
||||
assert.match(
|
||||
prompt,
|
||||
/`Read` the file, then `Edit`/i,
|
||||
`${name}: must require Read before Edit (OpenCode edit requires a prior Read).`
|
||||
);
|
||||
});
|
||||
|
||||
test('instructs removing the sentinel on the final section', () => {
|
||||
assert.match(
|
||||
prompt,
|
||||
/no trailing sentinel/i,
|
||||
`${name}: must instruct removing the sentinel on the final section.`
|
||||
);
|
||||
});
|
||||
|
||||
test('forbids silent fallback to returning content', () => {
|
||||
assert.match(
|
||||
prompt,
|
||||
/do NOT silently fall back to returning content/i,
|
||||
`${name}: must forbid silent fallback to returning content.`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
/**
|
||||
* Regression tests for bug #2346
|
||||
*
|
||||
* Multiple GSD agents (gsd-ui-checker, gsd-planner) entered unbounded Read
|
||||
* loops — re-reading the same file hundreds of times in a single run. Root
|
||||
* cause: no explicit no-re-read rule or tool-budget cap in the agent prompts.
|
||||
* gsd-pattern-mapper was fixed in #2312; this covers the remaining agents.
|
||||
*
|
||||
* Fix: add <critical_rules> block to each affected agent with:
|
||||
* 1. No-re-read constraint
|
||||
* 2. Large-file strategy (Grep first, then targeted offset/limit Read)
|
||||
* 3. Stop-on-sufficient-evidence rule (where applicable)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// The <critical_rules> block in agent .md files IS the fix — it is the AI instruction that
|
||||
// prevents unbounded Read loops. There is no behavioral equivalent without a live LLM run.
|
||||
describe('bug #2346: agent read loop guards', () => {
|
||||
|
||||
describe('gsd-ui-checker', () => {
|
||||
const agentPath = path.join(AGENTS_DIR, 'gsd-ui-checker.md');
|
||||
const content = fs.readFileSync(agentPath, 'utf-8');
|
||||
|
||||
test('agent file exists', () => {
|
||||
assert.ok(fs.existsSync(agentPath), 'agents/gsd-ui-checker.md must exist');
|
||||
});
|
||||
|
||||
test('has <critical_rules> block', () => {
|
||||
assert.ok(
|
||||
content.includes('<critical_rules>'),
|
||||
'gsd-ui-checker.md must have a <critical_rules> block to prevent unbounded read loops (#2346)'
|
||||
);
|
||||
});
|
||||
|
||||
test('critical_rules contains no-re-read constraint', () => {
|
||||
const rulesStart = content.indexOf('<critical_rules>');
|
||||
const rulesEnd = content.indexOf('</critical_rules>', rulesStart);
|
||||
assert.ok(rulesStart !== -1 && rulesEnd !== -1, '<critical_rules> block must be complete');
|
||||
const rulesBlock = content.slice(rulesStart, rulesEnd);
|
||||
assert.ok(
|
||||
rulesBlock.includes('re-read') || rulesBlock.includes('re read'),
|
||||
'critical_rules must include a no-re-read rule'
|
||||
);
|
||||
});
|
||||
|
||||
test('critical_rules appears before success_criteria', () => {
|
||||
const rulesIdx = content.indexOf('<critical_rules>');
|
||||
const successIdx = content.indexOf('<success_criteria>');
|
||||
assert.ok(rulesIdx !== -1 && successIdx !== -1, 'both sections must exist');
|
||||
assert.ok(
|
||||
rulesIdx < successIdx,
|
||||
'<critical_rules> must appear before <success_criteria>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-planner', () => {
|
||||
const agentPath = path.join(AGENTS_DIR, 'gsd-planner.md');
|
||||
const content = fs.readFileSync(agentPath, 'utf-8');
|
||||
|
||||
test('agent file exists', () => {
|
||||
assert.ok(fs.existsSync(agentPath), 'agents/gsd-planner.md must exist');
|
||||
});
|
||||
|
||||
test('has <critical_rules> block', () => {
|
||||
assert.ok(
|
||||
content.includes('<critical_rules>'),
|
||||
'gsd-planner.md must have a <critical_rules> block to prevent unbounded read loops (#2346)'
|
||||
);
|
||||
});
|
||||
|
||||
test('critical_rules contains no-re-read constraint', () => {
|
||||
const rulesStart = content.indexOf('<critical_rules>');
|
||||
const rulesEnd = content.indexOf('</critical_rules>', rulesStart);
|
||||
assert.ok(rulesStart !== -1 && rulesEnd !== -1, '<critical_rules> block must be complete');
|
||||
const rulesBlock = content.slice(rulesStart, rulesEnd);
|
||||
assert.ok(
|
||||
rulesBlock.includes('re-read') || rulesBlock.includes('re read'),
|
||||
'critical_rules must include a no-re-read rule'
|
||||
);
|
||||
});
|
||||
|
||||
test('critical_rules appears before success_criteria', () => {
|
||||
const rulesIdx = content.indexOf('<critical_rules>');
|
||||
const successIdx = content.lastIndexOf('<success_criteria>');
|
||||
assert.ok(rulesIdx !== -1 && successIdx !== -1, 'both sections must exist');
|
||||
assert.ok(
|
||||
rulesIdx < successIdx,
|
||||
'<critical_rules> must appear before <success_criteria>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Regression test for bug #2351
|
||||
*
|
||||
* gsd-intel-updater used hardcoded canonical paths (`agents/*.md`,
|
||||
* `commands/gsd/*.md`, `hooks/*.js`, etc.) that assumed the standard
|
||||
* `.claude/` runtime layout. Under a `.kilo` install, the runtime root is
|
||||
* `.kilo/`, and the command directory is `command/` (not `commands/gsd/`).
|
||||
* Globs against the old paths returned no results, producing semantically
|
||||
* empty intel files (`"entries": {}`).
|
||||
*
|
||||
* Fix: add runtime layout detection and a mapping table so the agent
|
||||
* resolves paths against the correct root.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const AGENT_PATH = path.join(__dirname, '..', 'agents', 'gsd-intel-updater.md');
|
||||
|
||||
describe('bug #2351: intel updater kilo layout support', () => {
|
||||
let content;
|
||||
|
||||
test('agent file exists', () => {
|
||||
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-intel-updater.md must exist');
|
||||
content = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
});
|
||||
|
||||
test('scope section includes layout detection step', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
const hasDetection =
|
||||
content.includes('ls -d .kilo') ||
|
||||
content.includes('Runtime layout detection') ||
|
||||
content.includes('detected layout') ||
|
||||
content.includes('layout detection');
|
||||
assert.ok(
|
||||
hasDetection,
|
||||
'gsd-intel-updater.md must instruct the agent to detect the runtime layout ' +
|
||||
'(.kilo vs .claude) before resolving canonical paths (#2351)'
|
||||
);
|
||||
});
|
||||
|
||||
test('scope section maps .kilo/agents path', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('.kilo/agents'),
|
||||
'scope section must include the .kilo/agents/*.md path so agent count is correct under kilo layout'
|
||||
);
|
||||
});
|
||||
|
||||
test('scope section maps .kilo/command path (not commands/gsd)', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('.kilo/command'),
|
||||
'scope section must include .kilo/command path — kilo uses "command/" not "commands/gsd/"'
|
||||
);
|
||||
});
|
||||
|
||||
test('scope section maps .kilo/hooks path', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('.kilo/hooks'),
|
||||
'scope section must include .kilo/hooks path for hook file counts'
|
||||
);
|
||||
});
|
||||
|
||||
test('scope section retains standard layout paths for .claude installs', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents/*.md') || content.includes('Standard `.claude` layout'),
|
||||
'scope section must still document the standard .claude layout paths for non-kilo installs'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// Reads .md/.json/.yml product files whose deployed text IS what the
|
||||
// runtime loads — testing text content tests the deployed contract.
|
||||
|
||||
/**
|
||||
* Bug #2419: gsd-project-researcher agent type not found
|
||||
*
|
||||
* When gsd-new-project spawns gsd-project-researcher subagents, it fails with
|
||||
* "agent type not found" if the user has a local-only install (agents in
|
||||
* .claude/agents/ of a different project, not the global ~/.claude/agents/).
|
||||
*
|
||||
* Fix: new-project.md and new-milestone.md must parse agents_installed from
|
||||
* the init JSON and warn the user (rather than silently failing) when agents
|
||||
* are missing.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const NEW_PROJECT_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'new-project.md');
|
||||
const NEW_MILESTONE_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'new-milestone.md');
|
||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
|
||||
describe('gsd-project-researcher agent registration (#2419)', () => {
|
||||
test('gsd-project-researcher.md exists in agents source dir', () => {
|
||||
const agentFile = path.join(AGENTS_DIR, 'gsd-project-researcher.md');
|
||||
assert.ok(
|
||||
fs.existsSync(agentFile),
|
||||
'agents/gsd-project-researcher.md must exist in the source agents directory'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-project-researcher.md has correct name in frontmatter', () => {
|
||||
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-project-researcher.md'), 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('name: gsd-project-researcher'),
|
||||
'agents/gsd-project-researcher.md must have name: gsd-project-researcher in frontmatter'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project.md parses agents_installed from init JSON', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents_installed'),
|
||||
'new-project.md must parse agents_installed from the init JSON to detect missing agents'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project.md warns user when agents_installed is false', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents_installed') && content.includes('agent type not found') ||
|
||||
content.includes('agents_installed') && content.includes('missing') ||
|
||||
content.includes('agents_installed') && content.includes('not installed'),
|
||||
'new-project.md must warn the user when agents are not installed (agents_installed is false)'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project.md reports required-agent and skill-payload diagnostics separately', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
assert.ok(content.includes('required_agents_installed'),
|
||||
'new-project.md must parse required_agents_installed from init JSON');
|
||||
assert.ok(content.includes('missing_required_agents'),
|
||||
'new-project.md must report missing required new-project agents separately');
|
||||
assert.ok(content.includes('agent_skill_payloads_available'),
|
||||
'new-project.md must distinguish skill payload availability from agent definitions');
|
||||
assert.ok(content.includes('agents_dir'),
|
||||
'new-project.md must show which agents directory was checked');
|
||||
});
|
||||
|
||||
test('new-milestone.md parses agents_installed from init JSON', () => {
|
||||
const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents_installed'),
|
||||
'new-milestone.md must parse agents_installed from the init JSON to detect missing agents'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-milestone.md warns user when agents_installed is false', () => {
|
||||
const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents_installed') && (
|
||||
content.includes('agent type not found') ||
|
||||
content.includes('missing') ||
|
||||
content.includes('not installed')
|
||||
),
|
||||
'new-milestone.md must warn the user when agents are not installed (agents_installed is false)'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project.md lists gsd-project-researcher in available_agent_types', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
const agentTypesMatch = content.match(/<available_agent_types>([\s\S]*?)<\/available_agent_types>/);
|
||||
assert.ok(agentTypesMatch, 'new-project.md must have <available_agent_types> section');
|
||||
assert.ok(
|
||||
agentTypesMatch[1].includes('gsd-project-researcher'),
|
||||
'new-project.md <available_agent_types> must list gsd-project-researcher'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-milestone.md lists gsd-project-researcher in available_agent_types', () => {
|
||||
const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8');
|
||||
const agentTypesMatch = content.match(/<available_agent_types>([\s\S]*?)<\/available_agent_types>/);
|
||||
assert.ok(agentTypesMatch, 'new-milestone.md must have <available_agent_types> section');
|
||||
assert.ok(
|
||||
agentTypesMatch[1].includes('gsd-project-researcher'),
|
||||
'new-milestone.md <available_agent_types> must list gsd-project-researcher'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Bug #2421: gsd-planner emits grep-count acceptance gates that count comment text
|
||||
*
|
||||
* The planner must instruct agents to use comment-aware grep patterns in
|
||||
* <automated> verify blocks. Without this, descriptive comments in file
|
||||
* headers count against the gate and force authors to reword them — the
|
||||
* "self-invalidating grep gate" anti-pattern.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PLANNER_PATH = path.join(__dirname, '..', 'agents', 'gsd-planner.md');
|
||||
|
||||
describe('gsd-planner grep gate hygiene (#2421)', () => {
|
||||
test('gsd-planner.md exists in agents source dir', () => {
|
||||
assert.ok(fs.existsSync(PLANNER_PATH), 'agents/gsd-planner.md must exist');
|
||||
});
|
||||
|
||||
test('gsd-planner.md contains Grep gate hygiene rule', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('Grep gate hygiene') || content.includes('grep gate hygiene'),
|
||||
'gsd-planner.md must contain a "Grep gate hygiene" rule to prevent self-invalidating grep gates'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-planner.md explains self-invalidating grep gate anti-pattern', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('self-invalidating'),
|
||||
'gsd-planner.md must describe the "self-invalidating" grep gate anti-pattern'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-planner.md provides comment-stripping grep example', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
// Must show a pattern that excludes comment lines (grep -v or grep -vE)
|
||||
assert.ok(
|
||||
content.includes('grep -v') || content.includes('grep -vE') || content.includes('-v '),
|
||||
'gsd-planner.md must provide a comment-stripping grep example (grep -v or grep -vE)'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-planner.md warns against bare zero-count grep gates on whole files', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('== 0') || content.includes('zero-count') || content.includes('zero count'),
|
||||
'gsd-planner.md must warn against bare zero-count grep gates without comment exclusion'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-planner.md grep gate hygiene rule appears after Nyquist Rule', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
const nyquistIdx = content.indexOf('Nyquist Rule');
|
||||
const grepGateIdx = content.indexOf('grep gate hygiene') !== -1
|
||||
? content.indexOf('grep gate hygiene')
|
||||
: content.indexOf('Grep gate hygiene');
|
||||
|
||||
assert.ok(nyquistIdx !== -1, 'Nyquist Rule must be present in gsd-planner.md');
|
||||
assert.ok(grepGateIdx !== -1, 'Grep gate hygiene must be present in gsd-planner.md');
|
||||
assert.ok(
|
||||
grepGateIdx > nyquistIdx,
|
||||
`Grep gate hygiene rule (at ${grepGateIdx}) must appear after Nyquist Rule (at ${nyquistIdx})`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Regression test for bug #2686
|
||||
*
|
||||
* The gsd-code-fixer agent (spawned by /gsd-code-review-fix) operated directly
|
||||
* against the main working tree. When it ran concurrently with a foreground
|
||||
* session both processes raced for HEAD, the index, and on-disk files. The
|
||||
* foreground session's next commit could land on the wrong branch (whichever
|
||||
* branch the agent last checked out).
|
||||
*
|
||||
* Fix: the agent's working instructions must include `git worktree add` as the
|
||||
* FIRST git operation, run ALL subsequent git operations inside that worktree
|
||||
* path, and call `git worktree remove` for cleanup when done.
|
||||
*
|
||||
* This mirrors the pattern already used by every other per-issue GSD agent at
|
||||
* /private/tmp/sv-<n>.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// The gsd-code-fixer agent's working instructions ARE the product — Claude
|
||||
// executes them literally at runtime. Testing the text content tests the
|
||||
// deployed contract: if the instruction is absent, the isolation guarantee
|
||||
// is absent.
|
||||
|
||||
const { describe, test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('bug-2686: review-fix agent worktree isolation', () => {
|
||||
let agentContent;
|
||||
|
||||
before(() => {
|
||||
const agentPath = path.join(__dirname, '..', 'agents', 'gsd-code-fixer.md');
|
||||
assert.ok(fs.existsSync(agentPath), 'agents/gsd-code-fixer.md must exist');
|
||||
agentContent = fs.readFileSync(agentPath, 'utf-8');
|
||||
});
|
||||
|
||||
test('agent instructions include git worktree add before any branch-switching checkout or commit', () => {
|
||||
const worktreePos = agentContent.indexOf('git worktree add');
|
||||
|
||||
assert.ok(
|
||||
worktreePos !== -1,
|
||||
'gsd-code-fixer.md must include a "git worktree add" instruction to isolate operations from the main working tree (#2686)'
|
||||
);
|
||||
|
||||
// `git checkout -- {file}` is a file-restore within the worktree — safe, not a branch switch.
|
||||
// The dangerous operation is `git checkout <branch>` (no leading --).
|
||||
// Find the first branch-switching checkout (pattern: "git checkout " NOT followed by "--").
|
||||
const branchCheckoutMatch = /git checkout (?!--)/.exec(agentContent);
|
||||
if (branchCheckoutMatch) {
|
||||
const branchCheckoutPos = branchCheckoutMatch.index;
|
||||
assert.ok(
|
||||
worktreePos < branchCheckoutPos,
|
||||
'git worktree add must appear before any branch-switching git checkout in the agent instructions'
|
||||
);
|
||||
}
|
||||
|
||||
// commit command must come after worktree setup — the fixer may use
|
||||
// either `git commit` directly or `gsd-sdk query commit`
|
||||
const commitMatch = /(?:git commit|gsd-sdk query commit)/.exec(agentContent);
|
||||
if (commitMatch) {
|
||||
const commitPos = commitMatch.index;
|
||||
assert.ok(
|
||||
worktreePos < commitPos,
|
||||
'git worktree add must appear before any commit command in the agent instructions'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('agent instructions include worktree cleanup after completion', () => {
|
||||
assert.ok(
|
||||
agentContent.includes('git worktree remove') || agentContent.includes('worktree remove'),
|
||||
'gsd-code-fixer.md must include worktree cleanup (git worktree remove) to avoid leaking tmp directories (#2686)'
|
||||
);
|
||||
});
|
||||
|
||||
test('agent instructions use a /tmp path for the worktree', () => {
|
||||
// Require either a literal /tmp/sv- path or a variable assignment to /tmp/sv-
|
||||
// (e.g. `wt=$(mktemp -d "/tmp/sv-..."`). Bare `$wt` or `wt=` references
|
||||
// without a /tmp/sv- assignment are not sufficient.
|
||||
const hasTmpWorktreePath =
|
||||
/\/tmp\/sv-/.test(agentContent) ||
|
||||
/\bwt\s*=\s*["']?\/tmp\/sv-/.test(agentContent);
|
||||
assert.ok(
|
||||
hasTmpWorktreePath,
|
||||
'gsd-code-fixer.md must define a worktree variable at a /tmp/sv-... path, consistent with other GSD agents (#2686)'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,208 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// agents/gsd-code-fixer.md is the deployed agent definition the runtime
|
||||
// loads. Parsing its bash code blocks into structured invocation records
|
||||
// (extractCleanupGitInvocations + the recovery-block parsers below) IS
|
||||
// testing the runtime contract — what command sequence the agent
|
||||
// actually documents and executes. The .match() calls extract typed
|
||||
// fields from a known-shape product file, then assertions go against
|
||||
// those typed fields, not against the raw markdown text.
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
/**
|
||||
* Bug #2990: gsd-code-fixer worktree setup fails when current branch
|
||||
* is already checked out in the main repo.
|
||||
*
|
||||
* The original agent definition called `git worktree add "$wt" "$branch"`,
|
||||
* where `$branch` was the user's currently-checked-out branch. Git refuses
|
||||
* to check out the same branch in two worktrees by default, so the setup
|
||||
* failed before the agent could do any work.
|
||||
*
|
||||
* Fix: create a NEW branch `gsd-reviewfix/${padded_phase}-$$` and attach
|
||||
* the worktree to it via `git worktree add -b "$reviewfix_branch" "$wt"
|
||||
* "$branch"`. The cleanup tail then fast-forwards `$branch` to
|
||||
* `$reviewfix_branch` so the user's branch captures the agent's commits.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const AGENT_PATH = path.join(__dirname, '..', 'agents', 'gsd-code-fixer.md');
|
||||
|
||||
function parseWorktreeAddInvocations(markdown) {
|
||||
// Pull `git worktree add ...` calls and classify each into structured
|
||||
// records: hasNewBranchFlag (uses -b $reviewfix_branch) vs attachesToBareBranch
|
||||
// ($wt $branch). Skip occurrences inside markdown inline code (backticks)
|
||||
// or bash comments -- those are documentation citations of the OLD broken
|
||||
// pattern, not executable instructions.
|
||||
const invocations = [];
|
||||
const lines = markdown.split('\n');
|
||||
for (const line of lines) {
|
||||
const idx = line.indexOf('git worktree add');
|
||||
if (idx === -1) continue;
|
||||
// Skip if inside backticks: the substring up to the match has an odd
|
||||
// number of backticks, the call is inside an inline code span.
|
||||
const before = line.slice(0, idx);
|
||||
const backticksBefore = (before.match(/`/g) || []).length;
|
||||
if (backticksBefore % 2 === 1) continue;
|
||||
// Skip if the line is a bash comment (after stripping leading whitespace).
|
||||
if (line.trimStart().startsWith('#')) continue;
|
||||
const argstr = line.slice(idx + 'git worktree add'.length).trim();
|
||||
invocations.push({
|
||||
raw: argstr,
|
||||
hasNewBranchFlag: /(?:^|\s)-b\s+["']?\$reviewfix_branch["']?/.test(argstr),
|
||||
attachesToBareBranch: /^["']?\$wt["']?\s+["']?\$branch["']?\b/.test(argstr),
|
||||
});
|
||||
}
|
||||
return invocations;
|
||||
}
|
||||
|
||||
describe('Bug #2990: gsd-code-fixer worktree attaches to a NEW branch, not the user-checked-out one', () => {
|
||||
const md = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
const invocations = parseWorktreeAddInvocations(md);
|
||||
|
||||
test('sanity: at least one git-worktree-add invocation exists in the agent definition', () => {
|
||||
assert.ok(invocations.length > 0,
|
||||
'expected gsd-code-fixer.md to document at least one git worktree add invocation');
|
||||
});
|
||||
|
||||
test('every git-worktree-add invocation uses -b $reviewfix_branch (not bare $branch)', () => {
|
||||
const violations = invocations.filter(inv => inv.attachesToBareBranch);
|
||||
assert.deepEqual(
|
||||
violations.map(v => v.raw),
|
||||
[],
|
||||
`worktree-add invocations attaching to bare $branch (#2990): ${JSON.stringify(violations.map(v => v.raw), null, 2)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('the canonical setup invocation uses -b "$reviewfix_branch" "$wt" "$branch"', () => {
|
||||
const setupInvocations = invocations.filter(inv => inv.hasNewBranchFlag);
|
||||
assert.ok(setupInvocations.length >= 1,
|
||||
`expected at least one git-worktree-add invocation with -b "$reviewfix_branch" -- found: ${JSON.stringify(invocations.map(i => i.raw), null, 2)}`);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract the cleanup-tail bash block from the agent .md, then parse it into
|
||||
* an ordered array of `git ...` invocation records. Per-record assertions go
|
||||
* against the structured records, not the raw markdown text. Anchor on the
|
||||
* "Cleanup tail" header to scope to the right block (the file has multiple
|
||||
* fenced bash blocks; we only want the cleanup one).
|
||||
*/
|
||||
function extractCleanupGitInvocations(markdown) {
|
||||
// Find the cleanup tail header and the fenced bash block that follows.
|
||||
const headerIdx = markdown.indexOf('**Cleanup tail (transactional');
|
||||
if (headerIdx === -1) return null;
|
||||
const fenceStart = markdown.indexOf('```bash', headerIdx);
|
||||
if (fenceStart === -1) return null;
|
||||
const fenceEnd = markdown.indexOf('```', fenceStart + '```bash'.length);
|
||||
if (fenceEnd === -1) return null;
|
||||
const block = markdown.slice(fenceStart + '```bash'.length, fenceEnd);
|
||||
|
||||
// Tokenize each non-comment, non-blank line into structured records.
|
||||
const lines = block.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
||||
const records = [];
|
||||
for (const line of lines) {
|
||||
// Skip occurrences inside backticks (these would be inline-code
|
||||
// citations of the OLD pattern, not executable). The cleanup fenced
|
||||
// block is bash, but inline backticks can still appear inside echo
|
||||
// strings — guard anyway.
|
||||
const ticksBefore = (line.match(/`/g) || []).length;
|
||||
if (ticksBefore && ticksBefore % 2 === 1) continue;
|
||||
if (!line.includes('git ') && !line.startsWith('git ')) continue;
|
||||
records.push({
|
||||
raw: line,
|
||||
// Strip leading `git -C "..."`/`git -C $main_repo` so the verb-only
|
||||
// form stays comparable across direct and -C invocations.
|
||||
verb: (() => {
|
||||
const m = line.match(/^git\s+(?:-C\s+\S+\s+)?(\S+)/);
|
||||
return m ? m[1] : null;
|
||||
})(),
|
||||
// Did this line target the temp reviewfix branch by variable name?
|
||||
targetsReviewfixBranch: /\$reviewfix_branch\b/.test(line) || /"\$reviewfix_branch"/.test(line),
|
||||
// Is this the merge step? Captures the flag too.
|
||||
isMergeFfOnly: /\bmerge\s+--ff-only\b/.test(line),
|
||||
// Is this the branch-delete step?
|
||||
isBranchDelete: /\bbranch\s+-D\b/.test(line),
|
||||
});
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
describe('Bug #2990: cleanup tail fast-forwards $branch and deletes the temp branch on success', () => {
|
||||
const md = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
const records = extractCleanupGitInvocations(md);
|
||||
|
||||
test('cleanup tail bash block exists and is parseable', () => {
|
||||
assert.notEqual(records, null, 'expected to find a "Cleanup tail" bash block in agents/gsd-code-fixer.md');
|
||||
assert.ok(records.length > 0, 'expected at least one git invocation in the cleanup tail');
|
||||
});
|
||||
|
||||
test('cleanup contains exactly one merge --ff-only against $reviewfix_branch', () => {
|
||||
const merges = records.filter(r => r.isMergeFfOnly);
|
||||
assert.equal(merges.length, 1, `expected exactly 1 ff-only merge, got ${merges.length}: ${JSON.stringify(merges, null, 2)}`);
|
||||
assert.equal(merges[0].targetsReviewfixBranch, true, 'merge --ff-only must target $reviewfix_branch');
|
||||
});
|
||||
|
||||
test('cleanup contains exactly one git branch -D for $reviewfix_branch', () => {
|
||||
const deletes = records.filter(r => r.isBranchDelete);
|
||||
assert.equal(deletes.length, 1, `expected exactly 1 branch -D, got ${deletes.length}`);
|
||||
assert.equal(deletes[0].targetsReviewfixBranch, true, 'branch -D must target $reviewfix_branch');
|
||||
});
|
||||
|
||||
test('merge --ff-only precedes branch -D in the cleanup ordering', () => {
|
||||
const mergeIdx = records.findIndex(r => r.isMergeFfOnly);
|
||||
const deleteIdx = records.findIndex(r => r.isBranchDelete);
|
||||
assert.ok(mergeIdx >= 0 && deleteIdx >= 0);
|
||||
assert.ok(mergeIdx < deleteIdx,
|
||||
`merge must run before branch delete (merge=${mergeIdx}, delete=${deleteIdx}); otherwise commits could be lost on merge failure`);
|
||||
});
|
||||
|
||||
test('recovery sentinel JSON shape records reviewfix_branch alongside worktree_path', () => {
|
||||
// Find the writeFileSync call that constructs the sentinel JSON.
|
||||
// Parse the JSON.stringify argument list to extract the field names.
|
||||
const match = md.match(/fs\.writeFileSync\(sentinelPath,\s*JSON\.stringify\(\{([^}]+)\}/);
|
||||
assert.notEqual(match, null, 'expected JSON.stringify({...}) inside the sentinel write');
|
||||
const fields = match[1].split(',').map(s => s.trim().split(':')[0].trim()).filter(Boolean);
|
||||
assert.ok(fields.includes('reviewfix_branch'),
|
||||
`recovery sentinel must record reviewfix_branch alongside worktree_path; fields=${JSON.stringify(fields)}`);
|
||||
assert.ok(fields.includes('worktree_path'),
|
||||
`recovery sentinel must record worktree_path; fields=${JSON.stringify(fields)}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bug #2990 (#3001 CR): recovery code reads reviewfix_branch from sentinel and deletes the orphan branch', () => {
|
||||
const md = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
|
||||
test('recovery node script extracts reviewfix_branch from parsed sentinel', () => {
|
||||
// Find the recovery `node -e '...'` block (NOT the sentinel-write one).
|
||||
// Anchor on "recovery sentinel from a prior interrupted run".
|
||||
const headerIdx = md.indexOf('Detected pre-existing recovery sentinel');
|
||||
assert.notEqual(headerIdx, -1);
|
||||
const nodeStart = md.indexOf("node -e '", headerIdx);
|
||||
assert.notEqual(nodeStart, -1);
|
||||
const nodeEnd = md.indexOf("' \"$sentinel\"", nodeStart);
|
||||
assert.notEqual(nodeEnd, -1);
|
||||
const nodeBlock = md.slice(nodeStart, nodeEnd);
|
||||
// Both fields must be referenced by parsed.<field>.
|
||||
assert.ok(nodeBlock.includes('parsed.reviewfix_branch'),
|
||||
'recovery node script must extract parsed.reviewfix_branch from the sentinel');
|
||||
assert.ok(nodeBlock.includes('parsed.worktree_path'),
|
||||
'recovery node script must extract parsed.worktree_path from the sentinel');
|
||||
});
|
||||
|
||||
test('recovery shell deletes the orphan reviewfix branch when present', () => {
|
||||
// The recovery block (between sentinel detection and `rm -f "$sentinel"`)
|
||||
// must call `git branch -D "$prior_branch"` (best-effort, with || true).
|
||||
const sentinelIdx = md.indexOf('Detected pre-existing recovery sentinel');
|
||||
const rmIdx = md.indexOf('rm -f "$sentinel"', sentinelIdx);
|
||||
assert.notEqual(rmIdx, -1);
|
||||
const recoveryBlock = md.slice(sentinelIdx, rmIdx);
|
||||
assert.ok(/git\s+branch\s+-D\s+"\$prior_branch"/.test(recoveryBlock),
|
||||
`recovery block must contain \`git branch -D "$prior_branch"\`; got: ${recoveryBlock.slice(0, 500)}`);
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// Regression guard for bug #3087.
|
||||
//
|
||||
// Between v1.38.3 and v1.38.4, agents/gsd-planner.md had 10 instances of
|
||||
// CRITICAL/MANDATORY/ALWAYS/MUST directive emphasis systematically removed.
|
||||
// The change was undocumented and conflicts with the stated intent of PR #2489
|
||||
// (the sycophancy-hardening pass that shipped in the same release). This test
|
||||
// enforces the restored directive language so the demotion cannot recur silently.
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
let src;
|
||||
try {
|
||||
src = fs.readFileSync(path.join(ROOT, 'agents', 'gsd-planner.md'), 'utf8');
|
||||
} catch (err) {
|
||||
throw new Error(`agents/gsd-planner.md not found — was the file renamed? (${err.message})`);
|
||||
}
|
||||
|
||||
const directives = [
|
||||
{ desc: 'User Decision Fidelity heading is CRITICAL', pattern: /## CRITICAL: User Decision Fidelity/ },
|
||||
{ desc: 'Never Simplify heading is CRITICAL', pattern: /## CRITICAL: Never Simplify User Decisions/ },
|
||||
{ desc: 'Multi-Source Audit heading is MANDATORY', pattern: /## Multi-Source Coverage Audit \(MANDATORY in every plan set\)/ },
|
||||
{ desc: 'Source audit uses "Audit ALL" imperative', pattern: /Audit ALL four source types before finalizing/ },
|
||||
{ desc: 'Discovery is MANDATORY', pattern: /Discovery is MANDATORY unless/ },
|
||||
{ desc: 'Split signals use ALWAYS', pattern: /\*\*ALWAYS split if:\*\*/ },
|
||||
{ desc: 'requirements field doc uses MUST', pattern: /\*\*MUST\*\* list requirement IDs from ROADMAP/ },
|
||||
{ desc: 'Step 0 has CRITICAL requirement ID directive', pattern: /\*\*CRITICAL:\*\* Every requirement ID MUST appear/ },
|
||||
{ desc: 'Write tool directive uses ALWAYS', pattern: /\*\*ALWAYS use the Write tool to create files\*\*/ },
|
||||
{ desc: 'File naming convention heading is CRITICAL', pattern: /\*\*CRITICAL — File naming convention \(enforced\):\*\*/ },
|
||||
];
|
||||
|
||||
for (const { desc, pattern } of directives) {
|
||||
test(`gsd-planner.md: ${desc}`, () => {
|
||||
assert.ok(
|
||||
pattern.test(src),
|
||||
`Directive enforcement missing from gsd-planner.md: "${desc}" — pattern ${pattern} not found. ` +
|
||||
`This language was demoted in v1.38.4 (PR #2489) without documentation, conflicting with ` +
|
||||
`the sycophancy-hardening intent of that release. See bug #3087.`,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
'use strict';
|
||||
// allow-test-rule: reads markdown product files (gsd-executor.md, worktree-path-safety.md) to verify structural protocol — not source-grep
|
||||
|
||||
// Regression guards for bug #3097 and #3099.
|
||||
//
|
||||
// #3097: gsd-executor's worktree HEAD guard used `if [ -f .git ]` to detect
|
||||
// worktree mode. After a Bash `cd` out of the worktree into the main repo,
|
||||
// `.git` is a DIRECTORY (not a file), so the test is false and the entire
|
||||
// HEAD safety block is silently skipped. Commits then land on whatever branch
|
||||
// the main repo has checked out — not the per-agent worktree branch.
|
||||
//
|
||||
// #3099: Executor agents construct absolute paths from `pwd` captured in the
|
||||
// orchestrator context (main repo root). Edit/Write calls using these paths
|
||||
// resolve to the main repo, not the worktree. git commit from the worktree
|
||||
// sees a clean tree; the work is silently lost or leaks to main.
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const executorSrc = fs.readFileSync(
|
||||
path.join(ROOT, 'agents', 'gsd-executor.md'), 'utf8',
|
||||
);
|
||||
const executePhaseSrc = fs.readFileSync(
|
||||
path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase.md'), 'utf8',
|
||||
);
|
||||
|
||||
describe('bug #3097: cwd-drift sentinel in gsd-executor.md', () => {
|
||||
test('task_commit_protocol has cwd-drift assertion step (0a)', () => {
|
||||
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
|
||||
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
||||
assert.ok(protocolIdx !== -1 && protocolEnd !== -1, 'task_commit_protocol block not found');
|
||||
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
|
||||
assert.ok(
|
||||
protocol.includes('cwd') || protocol.includes('drift') || protocol.includes('gsd-spawn-toplevel'),
|
||||
'task_commit_protocol missing cwd-drift assertion step — #3097 fix not applied',
|
||||
);
|
||||
});
|
||||
|
||||
test('sentinel uses git rev-parse --git-dir to detect worktree', () => {
|
||||
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
|
||||
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
||||
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
|
||||
assert.ok(
|
||||
protocol.includes('rev-parse --git-dir') || protocol.includes('worktrees/'),
|
||||
'cwd-drift detection does not use git rev-parse --git-dir or .git/worktrees/ pattern',
|
||||
);
|
||||
});
|
||||
|
||||
test('cwd-drift check precedes HEAD assertion', () => {
|
||||
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
|
||||
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
||||
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
|
||||
const driftIdx = protocol.search(/cwd.drift|gsd-spawn-toplevel|drift.*assertion/i);
|
||||
const headIdx = protocol.indexOf('Pre-commit HEAD safety assertion');
|
||||
assert.ok(driftIdx !== -1, 'cwd-drift assertion not found');
|
||||
assert.ok(headIdx !== -1, 'HEAD assertion not found');
|
||||
assert.ok(driftIdx < headIdx, 'cwd-drift assertion must precede HEAD assertion (step 0a before step 0)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bug #3099: absolute-path safety guidance in gsd-executor.md', () => {
|
||||
test('task_commit_protocol documents absolute-path safety', () => {
|
||||
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
|
||||
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
||||
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
|
||||
assert.ok(
|
||||
(protocol.includes('absolute') || protocol.includes('absolute-path')) &&
|
||||
(protocol.includes('worktree') || protocol.includes('WT_ROOT')),
|
||||
'task_commit_protocol missing absolute-path safety guidance — #3099 fix not applied',
|
||||
);
|
||||
});
|
||||
|
||||
test('execute-phase.md parallel_execution block references path safety', () => {
|
||||
const parallelIdx = executePhaseSrc.indexOf('<parallel_execution>');
|
||||
assert.ok(parallelIdx !== -1, 'parallel_execution block not found in execute-phase.md');
|
||||
// Verify the worktree-path-safety.md reference is present in the execution_context
|
||||
// (loaded via @ reference rather than inlined — the safe extract pattern)
|
||||
assert.ok(
|
||||
executePhaseSrc.includes('worktree-path-safety.md'),
|
||||
'execute-phase.md does not reference worktree-path-safety.md in execution_context',
|
||||
);
|
||||
});
|
||||
|
||||
test('execute-phase prompt anchors subagent file paths to project_root before files_to_read (#280)', () => {
|
||||
const filesIdx = executePhaseSrc.indexOf('<files_to_read>');
|
||||
assert.ok(filesIdx !== -1, 'files_to_read block not found in execute-phase.md');
|
||||
const dispatchSnippet = executePhaseSrc.slice(filesIdx, filesIdx + 1800);
|
||||
assert.ok(
|
||||
dispatchSnippet.includes('PROJECT_ROOT=$(git rev-parse --show-toplevel'),
|
||||
'executor dispatch must compute PROJECT_ROOT in the prompt before file reads',
|
||||
);
|
||||
assert.ok(
|
||||
dispatchSnippet.includes('${PROJECT_ROOT}/'),
|
||||
'executor files_to_read paths must be anchored to ${PROJECT_ROOT}/',
|
||||
);
|
||||
});
|
||||
|
||||
test('worktree-path-safety.md reference file exists', () => {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md')),
|
||||
'gsd-core/references/worktree-path-safety.md does not exist',
|
||||
);
|
||||
});
|
||||
|
||||
test('worktree-path-safety.md contains cwd-drift and absolute-path guards', () => {
|
||||
const safetySrc = fs.readFileSync(
|
||||
path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md'), 'utf8',
|
||||
);
|
||||
assert.ok(safetySrc.includes('gsd-spawn-toplevel') || safetySrc.includes('cwd-drift'),
|
||||
'worktree-path-safety.md missing cwd-drift sentinel content');
|
||||
assert.ok(safetySrc.includes('WT_ROOT') || safetySrc.includes('absolute'),
|
||||
'worktree-path-safety.md missing absolute-path guard content');
|
||||
});
|
||||
});
|
||||
@@ -1,187 +0,0 @@
|
||||
// allow-test-rule: source-text-is-the-product — agents/gsd-intel-updater.md IS
|
||||
// the deployed agent instruction set. Asserting its text content tests the
|
||||
// deployed behaviour contract, not internal implementation.
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Regression tests for bug #3290.
|
||||
*
|
||||
* The "Runtime layout detection" block in gsd-intel-updater.md ran
|
||||
* unconditionally on every project analysed, emitting:
|
||||
*
|
||||
* Layout detection returned "unknown" — this project is not a GSD-system
|
||||
* installation (no `.claude/gsd-core/` or `.kilo/` runtime root).
|
||||
*
|
||||
* for every ordinary (non-GSD-framework) user project. The verdict was already
|
||||
* ignored by Steps 2-6 on non-GSD projects. The block was dead-but-noisy.
|
||||
*
|
||||
* Fix: gate the runtime bash detection on a positive "is-this-the-framework-
|
||||
* repo" check (package.json name === "@opengsd/gsd-core") so it runs ONLY when
|
||||
* analysing the GSD framework's own repo, OR remove the block entirely if no
|
||||
* downstream consumers exist.
|
||||
*
|
||||
* Group A — gating contract:
|
||||
* The unconditional bash detection invocation must be absent OR wrapped in a
|
||||
* framework-repo guard. A bare `ls -d .kilo ... || echo "unknown"` with no
|
||||
* surrounding gate is the defect signature.
|
||||
*
|
||||
* Group B — no orphan consumers:
|
||||
* Confirm no other agent, command, or workflow file reads/consumes the layout-
|
||||
* detection verdict emitted by this block.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const AGENT_PATH = path.join(ROOT, 'agents', 'gsd-intel-updater.md');
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Walk a directory recursively and return absolute paths of all .md files. */
|
||||
function walkMd(dir) {
|
||||
const results = [];
|
||||
if (!fs.existsSync(dir)) return results;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const abs = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...walkMd(abs));
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
results.push(abs);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ─── Group A — gating contract ───────────────────────────────────────────────
|
||||
|
||||
describe('bug #3290 — Group A: layout-detection block must be gated or absent', () => {
|
||||
let content;
|
||||
|
||||
test('agent file exists', () => {
|
||||
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-intel-updater.md must exist');
|
||||
content = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
});
|
||||
|
||||
test(
|
||||
'bare unconditional detection invocation is absent — ' +
|
||||
'the "ls -d .kilo ... || echo unknown" must not appear outside a framework-repo gate',
|
||||
() => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
|
||||
// The defect signature: the bash block runs unconditionally.
|
||||
// We look for the exact shell one-liner that emits the verdict.
|
||||
const bareDetectionPattern =
|
||||
/ls -d \.kilo\b.*\|\|.*echo "?unknown"?/;
|
||||
|
||||
const hasBareDetection = bareDetectionPattern.test(content);
|
||||
|
||||
if (!hasBareDetection) {
|
||||
// Block is fully removed — option B — pass.
|
||||
return;
|
||||
}
|
||||
|
||||
// Block is still present. Verify it is surrounded by a framework-repo gate.
|
||||
// A valid gate checks package.json name or an equivalent positive signal
|
||||
// that the current project IS the GSD framework's own repo.
|
||||
const hasFrameworkGate =
|
||||
content.includes('@opengsd/gsd-core') ||
|
||||
content.includes('is-this-the-framework') ||
|
||||
content.includes('framework repo') ||
|
||||
content.includes('Only run') ||
|
||||
/if.*package\.json.*gsd-core/i.test(content) ||
|
||||
/Only.*layout detection.*GSD framework/i.test(content) ||
|
||||
/Only.*layout detection.*framework/i.test(content);
|
||||
|
||||
assert.ok(
|
||||
hasFrameworkGate,
|
||||
'agents/gsd-intel-updater.md contains a bare unconditional layout-detection ' +
|
||||
'bash block (`ls -d .kilo ... || echo unknown`) with no surrounding ' +
|
||||
'framework-repo gate (#3290). ' +
|
||||
'Either remove the block entirely, or wrap it in a check like:\n' +
|
||||
' if [[ "$(jq -r \'.name // ""\' package.json 2>/dev/null)" == "@opengsd/gsd-core" ]]; then\n' +
|
||||
' # ... detection block ...\n' +
|
||||
' fi'
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Group B — no orphan downstream consumers ────────────────────────────────
|
||||
|
||||
describe('bug #3290 — Group B: layout-detection verdict has no downstream consumers', () => {
|
||||
const SOURCE_DIRS = [
|
||||
path.join(ROOT, 'agents'),
|
||||
path.join(ROOT, 'commands', 'gsd'),
|
||||
path.join(ROOT, 'gsd-core', 'workflows'),
|
||||
];
|
||||
|
||||
/**
|
||||
* Lines that reference the three possible verdict values emitted by the
|
||||
* detection block: "claude", "kilo", "unknown" — ONLY as the verdict output
|
||||
* of the gsd-intel-updater layout detection (not general runtime references).
|
||||
*
|
||||
* We look for the specific phrase "Layout detection returned" which is the
|
||||
* sentinel the noisy output line uses.
|
||||
*/
|
||||
test('no file contains "Layout detection returned" (the noisy verdict phrase)', () => {
|
||||
const matches = [];
|
||||
|
||||
for (const dir of SOURCE_DIRS) {
|
||||
const files = walkMd(dir);
|
||||
for (const file of files) {
|
||||
const rel = path.relative(ROOT, file);
|
||||
const src = fs.readFileSync(file, 'utf-8');
|
||||
if (src.includes('Layout detection returned')) {
|
||||
// Collect matching lines for the error message
|
||||
const lines = src.split(/\r?\n/)
|
||||
.map((l, i) => ({ line: l, n: i + 1 }))
|
||||
.filter(({ line }) => line.includes('Layout detection returned'));
|
||||
matches.push({ rel, lines });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
matches.length,
|
||||
0,
|
||||
'Expected zero files to contain "Layout detection returned" (the noisy verdict ' +
|
||||
'phrase from the gsd-intel-updater layout-detection block). Found:\n' +
|
||||
matches.map(({ rel, lines }) =>
|
||||
` ${rel}:\n${lines.map(({ n, line }) => ` L${n}: ${line.trim()}`).join('\n')}`
|
||||
).join('\n')
|
||||
);
|
||||
});
|
||||
|
||||
test('no agent or workflow instructs reading the layout-detection verdict output', () => {
|
||||
// The verdict was: echo "kilo" | echo "claude" | echo "unknown"
|
||||
// If any file references "Layout detection returned unknown" as an instruction
|
||||
// to consume, that would be a consumer. We verify none exist outside of
|
||||
// the producing file (gsd-intel-updater.md).
|
||||
const verdictConsumerPattern = /Layout detection returned.*(unknown|claude|kilo)/i;
|
||||
const consumers = [];
|
||||
|
||||
for (const dir of SOURCE_DIRS) {
|
||||
const files = walkMd(dir);
|
||||
for (const file of files) {
|
||||
// Exclude the producer itself — it defines the message, not consumes it
|
||||
if (path.basename(file) === 'gsd-intel-updater.md') continue;
|
||||
const src = fs.readFileSync(file, 'utf-8');
|
||||
if (verdictConsumerPattern.test(src)) {
|
||||
consumers.push(path.relative(ROOT, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(
|
||||
consumers,
|
||||
[],
|
||||
'Expected no downstream consumer of the layout-detection verdict. Found:\n' +
|
||||
consumers.map((f) => ` ${f}`).join('\n') +
|
||||
'\nIf a consumer exists, use option A (gate) not option B (remove).'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const VERIFIER_AGENT = path.join(REPO_ROOT, 'agents', 'gsd-verifier.md');
|
||||
|
||||
function verifierProbeContract(content) {
|
||||
const sectionStart = content.indexOf('## Step 7c: Probe Execution');
|
||||
const sectionEnd = content.indexOf('## Step 8:', sectionStart);
|
||||
assert.notEqual(sectionStart, -1, 'verifier must define Step 7c');
|
||||
assert.notEqual(sectionEnd, -1, 'verifier must close Step 7c before Step 8');
|
||||
|
||||
const section = content.slice(sectionStart, sectionEnd);
|
||||
const codeBlocks = [...section.matchAll(/```bash\r?\n([\s\S]*?)\r?\n```/g)].map((match) => match[1].split(/\r?\n/).join('\n'));
|
||||
const executionSteps = [...section.matchAll(/^\d+\.\s+(.+)$/gm)].map((match) => match[1]);
|
||||
return {
|
||||
title: 'Step 7c: Probe Execution',
|
||||
conventionalDiscoveryCommand: codeBlocks[0]?.split('\n').find((line) => line.startsWith('find scripts')) || null,
|
||||
declaredDiscoveryCommand: codeBlocks[0]?.split('\n').find((line) => line.startsWith('grep -R')) || null,
|
||||
executionCommand: codeBlocks[1] || '',
|
||||
executionSteps,
|
||||
statusRows: [...section.matchAll(/^\|\s*`([^`]+)`\s*\|\s*`([^`]+)`\s*\|[^|]+\|\s*([^|]+)\|$/gm)]
|
||||
.map((match) => ({ probe: match[1], command: match[2], statuses: match[3].trim() })),
|
||||
summaryClaimsRejected: section.includes('SUMMARY.md probe pass claims are not evidence'),
|
||||
};
|
||||
}
|
||||
|
||||
describe('bug #3321: gsd-verifier runs probes instead of trusting SUMMARY claims', () => {
|
||||
test('verifier prompt requires direct probe discovery and execution', () => {
|
||||
const content = fs.readFileSync(VERIFIER_AGENT, 'utf8');
|
||||
const contract = verifierProbeContract(content);
|
||||
|
||||
assert.equal(contract.title, 'Step 7c: Probe Execution');
|
||||
assert.equal(contract.conventionalDiscoveryCommand, "find scripts -path '*/tests/probe-*.sh' -type f 2>/dev/null | sort");
|
||||
assert.equal(
|
||||
contract.declaredDiscoveryCommand,
|
||||
"grep -R -n -E 'probe-[^[:space:]]+\\.sh|scripts/.*/tests/probe-.*\\.sh' \"$PHASE_DIR\"/*-PLAN.md \"$PHASE_DIR\"/*-SUMMARY.md 2>/dev/null",
|
||||
);
|
||||
assert.deepEqual(contract.executionSteps, [
|
||||
'Build the `PROBES` list from explicit PLAN declarations first; include conventional `scripts/*/tests/probe-*.sh` when the phase is a migration/tooling phase or the success criteria mention probes.',
|
||||
'For every documented probe path, if the file is missing or unreadable, mark `MISSING_PROBE` and set `status: gaps_found`. Do not require the executable bit because probes run through `bash "$probe"`.',
|
||||
'Run each probe from the built `PROBES` list (declared + conventional) from the repository root:',
|
||||
'Exit code 0 is PASS. Any non-zero exit is FAILED and must include stdout/stderr evidence in VERIFICATION.md.',
|
||||
'Do not substitute executor narration, SUMMARY.md PASS-marker counts, or a different dry-run driver command for the probe result.',
|
||||
]);
|
||||
assert.equal(contract.executionCommand, 'for probe in "${PROBES[@]}"; do\n timeout 30s bash "$probe"\ndone');
|
||||
assert.deepEqual(contract.statusRows, [{
|
||||
probe: 'scripts/.../probe-name.sh',
|
||||
command: 'bash "$probe"',
|
||||
statuses: 'PASS / FAILED / MISSING_PROBE',
|
||||
}]);
|
||||
assert.equal(contract.summaryClaimsRejected, true);
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// Planner markdown is the deployed planning contract; these checks lock the
|
||||
// exact canonical forms that downstream phase-plan-index accepts.
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const PLANNER_PATH = path.join(__dirname, '..', 'agents', 'gsd-planner.md');
|
||||
|
||||
function readPlanner() {
|
||||
return fs.readFileSync(PLANNER_PATH, 'utf8');
|
||||
}
|
||||
|
||||
test('#3430: planner SUMMARY instruction uses canonical padded phase/plan form', () => {
|
||||
const content = readPlanner();
|
||||
assert.match(
|
||||
content,
|
||||
/Create `\.planning\/phases\/XX-name\/\{padded_phase\}-\{plan\}-SUMMARY\.md` when done/,
|
||||
'planner must instruct executors to write SUMMARY files in canonical padded-phase form'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
content,
|
||||
/After completion, create `\.planning\/phases\/XX-name\/\{phase\}-\{plan\}-SUMMARY\.md`/,
|
||||
'planner must not instruct the broken {phase}-{plan}-SUMMARY.md form'
|
||||
);
|
||||
});
|
||||
|
||||
test('#3430: planner depends_on docs show canonical in-phase plan ids', () => {
|
||||
const content = readPlanner();
|
||||
assert.match(
|
||||
content,
|
||||
/depends_on:[^\n]*Use `01-01`\/`01-01-auth-hardening`/,
|
||||
'planner must document canonical depends_on examples that phase-plan-index resolves'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
content,
|
||||
/depends_on:[^\n]*01-trust\/01/,
|
||||
'planner must not document phase-slug/plan-number depends_on examples as canonical'
|
||||
);
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// agents/*.md text IS the deployed contract — Claude Code, Codex, etc. load these
|
||||
// files at runtime and surface their content to users. Testing for retired slash
|
||||
// commands in this text is testing what real users will see.
|
||||
|
||||
/**
|
||||
* Bug #3605: Stale slash command references in 5 agent files
|
||||
*
|
||||
* After #3042 deleted /gsd-research-phase (replaced by
|
||||
* /gsd-plan-phase --research-phase <N>) and v1.40.0 consolidated /gsd-insert-phase
|
||||
* into /gsd-phase insert, six occurrences survived in agents/*.md because none of
|
||||
* the consolidation passes (#3029, #3044, #3131) included agents/ in their per-name
|
||||
* scrub scope. scripts/fix-slash-commands.cjs lists agents/ in SEARCH_DIRS but only
|
||||
* runs the /gsd- → /gsd: namespace transform, not retired-name replacement.
|
||||
*
|
||||
* This guard fails when any retired command name reappears in agents/*.md.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
|
||||
const RETIRED_COMMANDS = [
|
||||
'/gsd-research-phase',
|
||||
'/gsd-insert-phase',
|
||||
'/gsd-add-phase',
|
||||
'/gsd-remove-phase',
|
||||
'/gsd-analyze-dependencies',
|
||||
];
|
||||
|
||||
// Not the shared listAgentFiles() helper: this returns ABSOLUTE paths (consumed
|
||||
// by scanForRetired below as readFileSync targets), not stripped basenames.
|
||||
function listAgentFiles() {
|
||||
return fs
|
||||
.readdirSync(AGENTS_DIR)
|
||||
.filter((name) => name.endsWith('.md'))
|
||||
.map((name) => path.join(AGENTS_DIR, name));
|
||||
}
|
||||
|
||||
function scanForRetired(filePath) {
|
||||
const text = fs.readFileSync(filePath, 'utf-8');
|
||||
const lines = text.split(/\r?\n/);
|
||||
const hits = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
for (const cmd of RETIRED_COMMANDS) {
|
||||
const idx = lines[i].indexOf(cmd);
|
||||
if (idx === -1) continue;
|
||||
const next = lines[i].charCodeAt(idx + cmd.length);
|
||||
// Only count if the match is a real invocation, not a prefix of a longer name.
|
||||
// The next char must be a non-name char (anything outside [A-Za-z0-9-_]).
|
||||
const isWordBoundary =
|
||||
Number.isNaN(next) ||
|
||||
!((next >= 48 && next <= 57) || // 0-9
|
||||
(next >= 65 && next <= 90) || // A-Z
|
||||
(next >= 97 && next <= 122) || // a-z
|
||||
next === 45 || // -
|
||||
next === 95); // _
|
||||
if (!isWordBoundary) continue;
|
||||
hits.push({ line: i + 1, cmd, text: lines[i].trim() });
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
describe('bug #3605: agent contracts must not reference retired slash commands', () => {
|
||||
const agentFiles = listAgentFiles();
|
||||
|
||||
test('at least one agent file is scanned (smoke)', () => {
|
||||
assert.ok(agentFiles.length > 0, 'expected agents/*.md to exist');
|
||||
});
|
||||
|
||||
for (const file of agentFiles) {
|
||||
const rel = path.relative(path.join(__dirname, '..'), file);
|
||||
test(`${rel} contains no retired slash commands`, () => {
|
||||
const hits = scanForRetired(file);
|
||||
assert.deepEqual(
|
||||
hits,
|
||||
[],
|
||||
`${rel} contains retired command references:\n` +
|
||||
hits.map((h) => ` line ${h.line}: ${h.cmd} — ${h.text}`).join('\n'),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,200 +0,0 @@
|
||||
/**
|
||||
* Regression tests for bug #571
|
||||
*
|
||||
* gsd-doc-writer in fix mode used the Write tool (whole-file replace) instead
|
||||
* of the Edit tool (surgical replacement) when correcting specific failing
|
||||
* claims. When the target doc was generated but not yet committed, Write could
|
||||
* truncate the file to a single line with no git recovery path.
|
||||
*
|
||||
* Fix 1 (agent): Add Edit to the tools frontmatter and rewrite fix_mode
|
||||
* instructions to mandate Edit and explicitly forbid Write on existing files.
|
||||
* Fix 2 (workflow): Add a post-fix line-count guard in fix_loop that detects
|
||||
* >90% shrinkage and restores the file from existing_content.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// Agent .md files are the installed AI agents — their frontmatter and body IS
|
||||
// what the runtime loads. Checking text content IS checking the deployed contract.
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
|
||||
|
||||
const AGENT_PATH = path.join(AGENTS_DIR, 'gsd-doc-writer.md');
|
||||
const WORKFLOW_PATH = path.join(WORKFLOWS_DIR, 'docs-update.md');
|
||||
|
||||
// ─── Agent fix: Edit in tools frontmatter ────────────────────────────────────
|
||||
|
||||
describe('bug #571: gsd-doc-writer agent', () => {
|
||||
const content = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
|
||||
test('agent file exists', () => {
|
||||
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-doc-writer.md must exist');
|
||||
});
|
||||
|
||||
test('tools frontmatter includes Edit', () => {
|
||||
const toolsMatch = content.match(/^tools:\s*(.+)$/m);
|
||||
assert.ok(toolsMatch, 'gsd-doc-writer.md must have a tools: frontmatter line');
|
||||
assert.ok(
|
||||
toolsMatch[1].includes('Edit'),
|
||||
'tools: frontmatter must include Edit so fix mode can make surgical replacements (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
// ─── fix_mode instructions ────────────────────────────────────────────────
|
||||
|
||||
describe('fix_mode block', () => {
|
||||
const fixStart = content.indexOf('<fix_mode>');
|
||||
const fixEnd = content.indexOf('</fix_mode>', fixStart);
|
||||
assert.ok(fixStart !== -1 && fixEnd !== -1, '<fix_mode> block must be present and complete');
|
||||
const fixBlock = content.slice(fixStart, fixEnd);
|
||||
|
||||
test('fix_mode mandates Edit for corrections', () => {
|
||||
assert.ok(
|
||||
fixBlock.includes('Edit'),
|
||||
'fix_mode must instruct the agent to use the Edit tool for surgical corrections (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('fix_mode explicitly forbids Write on existing files', () => {
|
||||
assert.ok(
|
||||
fixBlock.includes('NEVER use the Write tool') || fixBlock.includes('NEVER call Write'),
|
||||
'fix_mode must explicitly forbid Write on existing files — Write replaces the whole file (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('fix_mode mentions unrecoverable data loss risk of Write', () => {
|
||||
assert.ok(
|
||||
fixBlock.includes('untracked') || fixBlock.includes('context window') || fixBlock.includes('permanently destroyed'),
|
||||
'fix_mode must explain WHY Write is forbidden — unrecoverable data loss for untracked files (#571)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── critical_rules ───────────────────────────────────────────────────────
|
||||
|
||||
describe('critical_rules block', () => {
|
||||
const rulesStart = content.indexOf('<critical_rules>');
|
||||
const rulesEnd = content.indexOf('</critical_rules>', rulesStart);
|
||||
assert.ok(rulesStart !== -1 && rulesEnd !== -1, '<critical_rules> block must be present and complete');
|
||||
const rulesBlock = content.slice(rulesStart, rulesEnd);
|
||||
|
||||
test('critical_rules forbids Write in fix mode', () => {
|
||||
assert.ok(
|
||||
rulesBlock.includes('fix mode') && (rulesBlock.includes('NEVER call Write') || rulesBlock.includes('NEVER use the Write')),
|
||||
'critical_rules must explicitly forbid Write in fix mode (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('critical_rules Edit rule appears before success_criteria', () => {
|
||||
const rulesIdx = content.indexOf('<critical_rules>');
|
||||
const successIdx = content.indexOf('<success_criteria>');
|
||||
assert.ok(rulesIdx !== -1 && successIdx !== -1, 'both <critical_rules> and <success_criteria> must exist');
|
||||
assert.ok(
|
||||
rulesIdx < successIdx,
|
||||
'<critical_rules> must appear before <success_criteria> (#571)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Workflow fix: post-fix truncation guard in fix_loop ─────────────────────
|
||||
|
||||
describe('bug #571: docs-update workflow fix_loop', () => {
|
||||
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
|
||||
|
||||
test('workflow file exists', () => {
|
||||
assert.ok(fs.existsSync(WORKFLOW_PATH), 'gsd-core/workflows/docs-update.md must exist');
|
||||
});
|
||||
|
||||
describe('fix_loop step', () => {
|
||||
const loopStart = content.indexOf('<step name="fix_loop">');
|
||||
const loopEnd = content.indexOf('</step>', loopStart);
|
||||
assert.ok(loopStart !== -1 && loopEnd !== -1, 'fix_loop step must be present and complete');
|
||||
const loopBlock = content.slice(loopStart, loopEnd);
|
||||
|
||||
test('fix_loop captures pre-fix line count', () => {
|
||||
assert.ok(
|
||||
loopBlock.includes('PRE_FIX_LINES') || loopBlock.includes('pre-fix line'),
|
||||
'fix_loop must capture the pre-fix line count to detect truncation (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('fix_loop checks post-fix line count', () => {
|
||||
assert.ok(
|
||||
loopBlock.includes('POST_FIX_LINES') || loopBlock.includes('post-fix line'),
|
||||
'fix_loop must check the post-fix line count to detect truncation (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('fix_loop restores file on truncation detection', () => {
|
||||
assert.ok(
|
||||
loopBlock.includes('Restore') || loopBlock.includes('restore'),
|
||||
'fix_loop must restore the file from existing_content when truncation is detected (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('fix_loop truncation threshold is >90% shrinkage', () => {
|
||||
assert.ok(
|
||||
loopBlock.includes('90%') || loopBlock.includes('10%'),
|
||||
'fix_loop must use a >90% shrinkage threshold (10% of original) to detect truncation (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('fix_loop logs a WARNING on truncation', () => {
|
||||
assert.ok(
|
||||
loopBlock.includes('WARNING') || loopBlock.includes('corrupted'),
|
||||
'fix_loop must log a WARNING when truncation is detected and restored (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
// Structural ordering: PRE check → fix agent runs → POST check → restore
|
||||
// These ensure the guard is wired in the right sequence, not just present.
|
||||
test('PRE_FIX_LINES is captured before POST_FIX_LINES (correct ordering)', () => {
|
||||
const preIdx = loopBlock.indexOf('PRE_FIX_LINES');
|
||||
const postIdx = loopBlock.indexOf('POST_FIX_LINES');
|
||||
assert.ok(preIdx !== -1 && postIdx !== -1, 'both PRE_FIX_LINES and POST_FIX_LINES must be present (#571)');
|
||||
assert.ok(
|
||||
preIdx < postIdx,
|
||||
'PRE_FIX_LINES must appear before POST_FIX_LINES — pre-capture must happen before post-check (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('restore instruction appears after POST_FIX_LINES check (correct ordering)', () => {
|
||||
const postIdx = loopBlock.indexOf('POST_FIX_LINES');
|
||||
// Find the restore instruction — it follows the threshold comparison
|
||||
const restoreIdx = loopBlock.indexOf('existing_content', postIdx);
|
||||
assert.ok(
|
||||
restoreIdx !== -1 && restoreIdx > postIdx,
|
||||
'restore-from-existing_content instruction must appear after the POST_FIX_LINES check (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('fix_loop doc path is quoted in shell snippets', () => {
|
||||
// Unquoted paths break on filenames with spaces or shell metacharacters.
|
||||
// Verify the bash snippets use quoted "{doc_path}" not bare {doc_path}.
|
||||
assert.ok(
|
||||
loopBlock.includes('< "{doc_path}"') || loopBlock.includes("<\"{doc_path}\""),
|
||||
'shell redirections must quote {doc_path} to handle paths with spaces (#571)'
|
||||
);
|
||||
});
|
||||
|
||||
test('corrupted doc is still re-verified (not silently skipped)', () => {
|
||||
// The restored doc must be included in step 2 re-verification so its
|
||||
// failures are counted and reported. It should only be excluded from
|
||||
// receiving another fix attempt, not from verification.
|
||||
const restoreIdx = loopBlock.indexOf('existing_content', loopBlock.indexOf('POST_FIX_LINES'));
|
||||
const reVerifyIdx = loopBlock.indexOf('re-verify', restoreIdx);
|
||||
assert.ok(
|
||||
reVerifyIdx !== -1,
|
||||
'fix_loop must include re-verification after truncation restore (corrupted docs still have failures) (#571)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Tests for #2427 — prompt-level sycophancy hardening of audit-class agents.
|
||||
* Verifies the four required changes are present in each agent file:
|
||||
* 1. Third-person framing (no "You are a GSD X" opening in <role>)
|
||||
* 2. FORCE adversarial stance block
|
||||
* 3. Explicit failure modes list
|
||||
* 4. BLOCKER/WARNING classification requirement
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const AGENTS_DIR = path.join(__dirname, '../agents');
|
||||
|
||||
const AUDIT_AGENTS = [
|
||||
'gsd-plan-checker.md',
|
||||
'gsd-code-reviewer.md',
|
||||
'gsd-security-auditor.md',
|
||||
'gsd-verifier.md',
|
||||
'gsd-eval-auditor.md',
|
||||
'gsd-nyquist-auditor.md',
|
||||
'gsd-ui-auditor.md',
|
||||
'gsd-integration-checker.md',
|
||||
'gsd-doc-verifier.md',
|
||||
];
|
||||
|
||||
function readAgent(agentsDir, filename) {
|
||||
return fs.readFileSync(path.join(agentsDir, filename), 'utf-8');
|
||||
}
|
||||
|
||||
function extractRole(content) {
|
||||
const match = content.match(/<role>([\s\S]*?)<\/role>/);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
describe('enh-2427 — sycophancy hardening: audit-class agents', () => {
|
||||
|
||||
for (const filename of AUDIT_AGENTS) {
|
||||
const label = filename.replace('.md', '');
|
||||
|
||||
describe(label, () => {
|
||||
let content;
|
||||
let role;
|
||||
|
||||
test('file is readable', () => {
|
||||
content = readAgent(AGENTS_DIR, filename);
|
||||
role = extractRole(content);
|
||||
assert.ok(content.length > 0, `${filename} should not be empty`);
|
||||
});
|
||||
|
||||
test('(1) third-person framing — <role> does not open with "You are a GSD"', () => {
|
||||
content = content || readAgent(AGENTS_DIR, filename);
|
||||
role = role || extractRole(content);
|
||||
const firstSentence = role.trim().slice(0, 80);
|
||||
assert.ok(
|
||||
!firstSentence.startsWith('You are a GSD'),
|
||||
`${filename}: <role> must not open with "You are a GSD" — use third-person submission framing. Got: "${firstSentence}"`
|
||||
);
|
||||
});
|
||||
|
||||
test('(2) FORCE adversarial stance — <adversarial_stance> block present', () => {
|
||||
content = content || readAgent(AGENTS_DIR, filename);
|
||||
assert.ok(
|
||||
content.includes('<adversarial_stance>'),
|
||||
`${filename}: must contain <adversarial_stance> block`
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('FORCE stance'),
|
||||
`${filename}: <adversarial_stance> must contain "FORCE stance"`
|
||||
);
|
||||
});
|
||||
|
||||
test('(3) explicit failure modes list present', () => {
|
||||
content = content || readAgent(AGENTS_DIR, filename);
|
||||
assert.ok(
|
||||
content.includes('failure modes'),
|
||||
`${filename}: must contain "failure modes" section in <adversarial_stance>`
|
||||
);
|
||||
});
|
||||
|
||||
test('(4) BLOCKER/WARNING classification requirement present', () => {
|
||||
content = content || readAgent(AGENTS_DIR, filename);
|
||||
assert.ok(
|
||||
content.includes('**BLOCKER**'),
|
||||
`${filename}: must define BLOCKER classification in <adversarial_stance>`
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('**WARNING**'),
|
||||
`${filename}: must define WARNING classification in <adversarial_stance>`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
// sdk/prompts/agents/ was removed in 377a6d2 — SDK now loads installed agents directly.
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* Enhancement #2500: gsd-codebase-mapper (arch focus) rich architecture output
|
||||
*
|
||||
* The codebase/ARCHITECTURE.md produced by gsd-codebase-mapper was a sparse
|
||||
* structural inventory — file listings and module relationships. After a major
|
||||
* refactor, research/ARCHITECTURE.md (created at /gsd-new-project) goes stale
|
||||
* with no refresh command. This enhancement enriches the codebase mapper's
|
||||
* arch-focus template to match the richness of the research version:
|
||||
* - ASCII system overview diagram
|
||||
* - Data flow traces with numbered steps and code references
|
||||
* - Component responsibility table (component → responsibility → file)
|
||||
* - Critical architectural constraints
|
||||
* - Anti-patterns specific to the codebase
|
||||
* - <!-- refreshed: {date} --> marker at top (maintainer request)
|
||||
*
|
||||
* The agent's template text IS what the runtime executes, so testing
|
||||
* the template content directly tests the deployed contract.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// The gsd-codebase-mapper ARCHITECTURE.md template is the instruction set
|
||||
// executed by the LLM at runtime. Testing its text content tests whether the
|
||||
// deployed agent will produce rich architecture docs as required by #2500.
|
||||
|
||||
const { describe, test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const AGENT_PATH = path.join(__dirname, '..', 'agents', 'gsd-codebase-mapper.md');
|
||||
|
||||
describe('enh-2500: gsd-codebase-mapper arch focus — rich architecture output', () => {
|
||||
let agentContent;
|
||||
let archTemplate;
|
||||
|
||||
before(() => {
|
||||
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-codebase-mapper.md must exist');
|
||||
agentContent = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
|
||||
// Isolate the ARCHITECTURE.md template section from the agent file.
|
||||
// End boundary is the STRUCTURE.md Template heading that immediately follows it.
|
||||
const archStart = agentContent.indexOf('## ARCHITECTURE.md Template (arch focus)');
|
||||
assert.ok(archStart !== -1, 'agent must contain an ARCHITECTURE.md Template (arch focus) section');
|
||||
|
||||
const archEnd = agentContent.indexOf('## STRUCTURE.md Template (arch focus)', archStart + 1);
|
||||
archTemplate = archEnd !== -1
|
||||
? agentContent.slice(archStart, archEnd)
|
||||
: agentContent.slice(archStart);
|
||||
});
|
||||
|
||||
test('template includes a refreshed date marker', () => {
|
||||
assert.ok(
|
||||
archTemplate.includes('<!-- refreshed:') || archTemplate.includes('refreshed:'),
|
||||
'ARCHITECTURE.md template must include a <!-- refreshed: {date} --> marker so users can see when the doc was last generated (#2500 maintainer requirement)'
|
||||
);
|
||||
});
|
||||
|
||||
test('template includes an ASCII system overview diagram', () => {
|
||||
// ASCII diagrams use box-drawing characters or at minimum ┌/└/│/─ or +/|/-
|
||||
const hasAsciiDiagram =
|
||||
archTemplate.includes('┌') ||
|
||||
archTemplate.includes('└') ||
|
||||
archTemplate.includes('│') ||
|
||||
archTemplate.includes('+--') ||
|
||||
archTemplate.includes('+-') ||
|
||||
archTemplate.includes('→') ||
|
||||
archTemplate.includes('↓') ||
|
||||
archTemplate.includes('↑');
|
||||
|
||||
assert.ok(
|
||||
hasAsciiDiagram,
|
||||
'ARCHITECTURE.md template must include an ASCII system overview diagram (box-drawing characters or flow arrows) as required by #2500'
|
||||
);
|
||||
});
|
||||
|
||||
test('template includes System Overview section header', () => {
|
||||
assert.ok(
|
||||
archTemplate.includes('System Overview') || archTemplate.includes('system overview'),
|
||||
'ARCHITECTURE.md template must include a "System Overview" section for the ASCII diagram (#2500)'
|
||||
);
|
||||
});
|
||||
|
||||
test('template includes a component responsibility table with required columns', () => {
|
||||
// Must have a markdown table with component, responsibility, and file columns
|
||||
const hasComponentCol =
|
||||
archTemplate.includes('Component') || archTemplate.includes('component');
|
||||
const hasResponsibilityCol =
|
||||
archTemplate.includes('Responsibility') || archTemplate.includes('responsibility');
|
||||
const hasFileCol =
|
||||
archTemplate.includes('File') || archTemplate.includes('file');
|
||||
|
||||
assert.ok(
|
||||
hasComponentCol && hasResponsibilityCol && hasFileCol,
|
||||
'ARCHITECTURE.md template must include a component responsibility table with Component, Responsibility, and File columns (#2500)'
|
||||
);
|
||||
});
|
||||
|
||||
test('template includes data flow traces with numbered steps', () => {
|
||||
const hasPrimaryRequestPath = /###\s+Primary Request Path/i.test(archTemplate);
|
||||
// [^\n]+ + \r?\n is CRLF-tolerant: .+ doesn't match \r in JS regex by
|
||||
// default, so \r before the literal \n in CRLF content kills the match.
|
||||
const hasThreeNumberedSteps = /^\s*1\.[^\n]+\r?\n\s*2\.[^\n]+\r?\n\s*3\./m.test(archTemplate);
|
||||
const hasFileLineRefs = /\(`\[.*:(?:line|\d+)\]`\)/.test(archTemplate);
|
||||
|
||||
assert.ok(
|
||||
hasPrimaryRequestPath && hasThreeNumberedSteps && hasFileLineRefs,
|
||||
'ARCHITECTURE.md template must include a "Primary Request Path" section with numbered steps and file:line references (#2500)'
|
||||
);
|
||||
});
|
||||
|
||||
test('template includes architectural constraints section', () => {
|
||||
const hasConstraints =
|
||||
/##\s+Architectural Constraints/i.test(archTemplate) &&
|
||||
/\bThreading\b/.test(archTemplate) &&
|
||||
/\bGlobal state\b/i.test(archTemplate) &&
|
||||
/\bCircular imports\b/i.test(archTemplate);
|
||||
|
||||
assert.ok(
|
||||
hasConstraints,
|
||||
'ARCHITECTURE.md template must include an "Architectural Constraints" section with Threading, Global state, and Circular imports categories (#2500)'
|
||||
);
|
||||
});
|
||||
|
||||
test('template includes anti-patterns section', () => {
|
||||
assert.ok(
|
||||
archTemplate.includes('Anti-pattern') ||
|
||||
archTemplate.includes('Anti-Pattern') ||
|
||||
archTemplate.includes('anti-pattern'),
|
||||
'ARCHITECTURE.md template must include an anti-patterns section specific to the codebase (#2500)'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -96,3 +96,130 @@ describe('gsd-executor — state.* calls use the named-only router form (#1863 r
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/bug-3097-3099-executor-worktree-path-safety.test.cjs — consolidation epic #1969 (B7 #1976)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:bug-3097-3099-executor-worktree-path-safety (consolidation epic #1969 B7 #1976)", () => {
|
||||
'use strict';
|
||||
// allow-test-rule: reads markdown product files (gsd-executor.md, worktree-path-safety.md) to verify structural protocol — not source-grep (see #3097)
|
||||
|
||||
// Regression guards for bug #3097 and #3099.
|
||||
//
|
||||
// #3097: gsd-executor's worktree HEAD guard used `if [ -f .git ]` to detect
|
||||
// worktree mode. After a Bash `cd` out of the worktree into the main repo,
|
||||
// `.git` is a DIRECTORY (not a file), so the test is false and the entire
|
||||
// HEAD safety block is silently skipped. Commits then land on whatever branch
|
||||
// the main repo has checked out — not the per-agent worktree branch.
|
||||
//
|
||||
// #3099: Executor agents construct absolute paths from `pwd` captured in the
|
||||
// orchestrator context (main repo root). Edit/Write calls using these paths
|
||||
// resolve to the main repo, not the worktree. git commit from the worktree
|
||||
// sees a clean tree; the work is silently lost or leaks to main.
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const executorSrc = fs.readFileSync(
|
||||
path.join(ROOT, 'agents', 'gsd-executor.md'), 'utf8',
|
||||
);
|
||||
const executePhaseSrc = fs.readFileSync(
|
||||
path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase.md'), 'utf8',
|
||||
);
|
||||
|
||||
describe('bug #3097: cwd-drift sentinel in gsd-executor.md', () => {
|
||||
test('task_commit_protocol has cwd-drift assertion step (0a)', () => {
|
||||
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
|
||||
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
||||
assert.ok(protocolIdx !== -1 && protocolEnd !== -1, 'task_commit_protocol block not found');
|
||||
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
|
||||
assert.ok(
|
||||
protocol.includes('cwd') || protocol.includes('drift') || protocol.includes('gsd-spawn-toplevel'),
|
||||
'task_commit_protocol missing cwd-drift assertion step — #3097 fix not applied',
|
||||
);
|
||||
});
|
||||
|
||||
test('sentinel uses git rev-parse --git-dir to detect worktree', () => {
|
||||
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
|
||||
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
||||
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
|
||||
assert.ok(
|
||||
protocol.includes('rev-parse --git-dir') || protocol.includes('worktrees/'),
|
||||
'cwd-drift detection does not use git rev-parse --git-dir or .git/worktrees/ pattern',
|
||||
);
|
||||
});
|
||||
|
||||
test('cwd-drift check precedes HEAD assertion', () => {
|
||||
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
|
||||
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
||||
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
|
||||
const driftIdx = protocol.search(/cwd.drift|gsd-spawn-toplevel|drift.*assertion/i);
|
||||
const headIdx = protocol.indexOf('Pre-commit HEAD safety assertion');
|
||||
assert.ok(driftIdx !== -1, 'cwd-drift assertion not found');
|
||||
assert.ok(headIdx !== -1, 'HEAD assertion not found');
|
||||
assert.ok(driftIdx < headIdx, 'cwd-drift assertion must precede HEAD assertion (step 0a before step 0)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bug #3099: absolute-path safety guidance in gsd-executor.md', () => {
|
||||
test('task_commit_protocol documents absolute-path safety', () => {
|
||||
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
|
||||
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
||||
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
|
||||
assert.ok(
|
||||
(protocol.includes('absolute') || protocol.includes('absolute-path')) &&
|
||||
(protocol.includes('worktree') || protocol.includes('WT_ROOT')),
|
||||
'task_commit_protocol missing absolute-path safety guidance — #3099 fix not applied',
|
||||
);
|
||||
});
|
||||
|
||||
test('execute-phase.md parallel_execution block references path safety', () => {
|
||||
const parallelIdx = executePhaseSrc.indexOf('<parallel_execution>');
|
||||
assert.ok(parallelIdx !== -1, 'parallel_execution block not found in execute-phase.md');
|
||||
// Verify the worktree-path-safety.md reference is present in the execution_context
|
||||
// (loaded via @ reference rather than inlined — the safe extract pattern)
|
||||
assert.ok(
|
||||
executePhaseSrc.includes('worktree-path-safety.md'),
|
||||
'execute-phase.md does not reference worktree-path-safety.md in execution_context',
|
||||
);
|
||||
});
|
||||
|
||||
test('execute-phase prompt anchors subagent file paths to project_root before files_to_read (#280)', () => {
|
||||
const filesIdx = executePhaseSrc.indexOf('<files_to_read>');
|
||||
assert.ok(filesIdx !== -1, 'files_to_read block not found in execute-phase.md');
|
||||
const dispatchSnippet = executePhaseSrc.slice(filesIdx, filesIdx + 1800);
|
||||
assert.ok(
|
||||
dispatchSnippet.includes('PROJECT_ROOT=$(git rev-parse --show-toplevel'),
|
||||
'executor dispatch must compute PROJECT_ROOT in the prompt before file reads',
|
||||
);
|
||||
assert.ok(
|
||||
dispatchSnippet.includes('${PROJECT_ROOT}/'),
|
||||
'executor files_to_read paths must be anchored to ${PROJECT_ROOT}/',
|
||||
);
|
||||
});
|
||||
|
||||
test('worktree-path-safety.md reference file exists', () => {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md')),
|
||||
'gsd-core/references/worktree-path-safety.md does not exist',
|
||||
);
|
||||
});
|
||||
|
||||
test('worktree-path-safety.md contains cwd-drift and absolute-path guards', () => {
|
||||
const safetySrc = fs.readFileSync(
|
||||
path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md'), 'utf8',
|
||||
);
|
||||
assert.ok(safetySrc.includes('gsd-spawn-toplevel') || safetySrc.includes('cwd-drift'),
|
||||
'worktree-path-safety.md missing cwd-drift sentinel content');
|
||||
assert.ok(safetySrc.includes('WT_ROOT') || safetySrc.includes('absolute'),
|
||||
'worktree-path-safety.md missing absolute-path guard content');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1263,3 +1263,286 @@ describe('#3258: no stale /gsd-intel slash-command references in product source
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/bug-2351-intel-kilo-layout.test.cjs — consolidation epic #1969 (B7 #1976)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:bug-2351-intel-kilo-layout (consolidation epic #1969 B7 #1976)", () => {
|
||||
/**
|
||||
* Regression test for bug #2351
|
||||
*
|
||||
* gsd-intel-updater used hardcoded canonical paths (`agents/*.md`,
|
||||
* `commands/gsd/*.md`, `hooks/*.js`, etc.) that assumed the standard
|
||||
* `.claude/` runtime layout. Under a `.kilo` install, the runtime root is
|
||||
* `.kilo/`, and the command directory is `command/` (not `commands/gsd/`).
|
||||
* Globs against the old paths returned no results, producing semantically
|
||||
* empty intel files (`"entries": {}`).
|
||||
*
|
||||
* Fix: add runtime layout detection and a mapping table so the agent
|
||||
* resolves paths against the correct root.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const AGENT_PATH = path.join(__dirname, '..', 'agents', 'gsd-intel-updater.md');
|
||||
|
||||
describe('bug #2351: intel updater kilo layout support', () => {
|
||||
let content;
|
||||
|
||||
test('agent file exists', () => {
|
||||
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-intel-updater.md must exist');
|
||||
content = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
});
|
||||
|
||||
test('scope section includes layout detection step', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
const hasDetection =
|
||||
content.includes('ls -d .kilo') ||
|
||||
content.includes('Runtime layout detection') ||
|
||||
content.includes('detected layout') ||
|
||||
content.includes('layout detection');
|
||||
assert.ok(
|
||||
hasDetection,
|
||||
'gsd-intel-updater.md must instruct the agent to detect the runtime layout ' +
|
||||
'(.kilo vs .claude) before resolving canonical paths (#2351)'
|
||||
);
|
||||
});
|
||||
|
||||
test('scope section maps .kilo/agents path', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('.kilo/agents'),
|
||||
'scope section must include the .kilo/agents/*.md path so agent count is correct under kilo layout'
|
||||
);
|
||||
});
|
||||
|
||||
test('scope section maps .kilo/command path (not commands/gsd)', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('.kilo/command'),
|
||||
'scope section must include .kilo/command path — kilo uses "command/" not "commands/gsd/"'
|
||||
);
|
||||
});
|
||||
|
||||
test('scope section maps .kilo/hooks path', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('.kilo/hooks'),
|
||||
'scope section must include .kilo/hooks path for hook file counts'
|
||||
);
|
||||
});
|
||||
|
||||
test('scope section retains standard layout paths for .claude installs', () => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents/*.md') || content.includes('Standard `.claude` layout'),
|
||||
'scope section must still document the standard .claude layout paths for non-kilo installs'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/bug-3290-intel-updater-layout-block.test.cjs — consolidation epic #1969 (B7 #1976)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:bug-3290-intel-updater-layout-block (consolidation epic #1969 B7 #1976)", () => {
|
||||
// allow-test-rule: source-text-is-the-product — agents/gsd-intel-updater.md IS (see #3290)
|
||||
// the deployed agent instruction set. Asserting its text content tests the
|
||||
// deployed behaviour contract, not internal implementation.
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Regression tests for bug #3290.
|
||||
*
|
||||
* The "Runtime layout detection" block in gsd-intel-updater.md ran
|
||||
* unconditionally on every project analysed, emitting:
|
||||
*
|
||||
* Layout detection returned "unknown" — this project is not a GSD-system
|
||||
* installation (no `.claude/gsd-core/` or `.kilo/` runtime root).
|
||||
*
|
||||
* for every ordinary (non-GSD-framework) user project. The verdict was already
|
||||
* ignored by Steps 2-6 on non-GSD projects. The block was dead-but-noisy.
|
||||
*
|
||||
* Fix: gate the runtime bash detection on a positive "is-this-the-framework-
|
||||
* repo" check (package.json name === "@opengsd/gsd-core") so it runs ONLY when
|
||||
* analysing the GSD framework's own repo, OR remove the block entirely if no
|
||||
* downstream consumers exist.
|
||||
*
|
||||
* Group A — gating contract:
|
||||
* The unconditional bash detection invocation must be absent OR wrapped in a
|
||||
* framework-repo guard. A bare `ls -d .kilo ... || echo "unknown"` with no
|
||||
* surrounding gate is the defect signature.
|
||||
*
|
||||
* Group B — no orphan consumers:
|
||||
* Confirm no other agent, command, or workflow file reads/consumes the layout-
|
||||
* detection verdict emitted by this block.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const AGENT_PATH = path.join(ROOT, 'agents', 'gsd-intel-updater.md');
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Walk a directory recursively and return absolute paths of all .md files. */
|
||||
function walkMd(dir) {
|
||||
const results = [];
|
||||
if (!fs.existsSync(dir)) return results;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const abs = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...walkMd(abs));
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
results.push(abs);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ─── Group A — gating contract ───────────────────────────────────────────────
|
||||
|
||||
describe('bug #3290 — Group A: layout-detection block must be gated or absent', () => {
|
||||
let content;
|
||||
|
||||
test('agent file exists', () => {
|
||||
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-intel-updater.md must exist');
|
||||
content = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
});
|
||||
|
||||
test(
|
||||
'bare unconditional detection invocation is absent — ' +
|
||||
'the "ls -d .kilo ... || echo unknown" must not appear outside a framework-repo gate',
|
||||
() => {
|
||||
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
|
||||
// The defect signature: the bash block runs unconditionally.
|
||||
// We look for the exact shell one-liner that emits the verdict.
|
||||
const bareDetectionPattern =
|
||||
/ls -d \.kilo\b.*\|\|.*echo "?unknown"?/;
|
||||
|
||||
const hasBareDetection = bareDetectionPattern.test(content);
|
||||
|
||||
if (!hasBareDetection) {
|
||||
// Block is fully removed — option B — pass.
|
||||
return;
|
||||
}
|
||||
|
||||
// Block is still present. Verify it is surrounded by a framework-repo gate.
|
||||
// A valid gate checks package.json name or an equivalent positive signal
|
||||
// that the current project IS the GSD framework's own repo.
|
||||
const hasFrameworkGate =
|
||||
content.includes('@opengsd/gsd-core') ||
|
||||
content.includes('is-this-the-framework') ||
|
||||
content.includes('framework repo') ||
|
||||
content.includes('Only run') ||
|
||||
/if.*package\.json.*gsd-core/i.test(content) ||
|
||||
/Only.*layout detection.*GSD framework/i.test(content) ||
|
||||
/Only.*layout detection.*framework/i.test(content);
|
||||
|
||||
assert.ok(
|
||||
hasFrameworkGate,
|
||||
'agents/gsd-intel-updater.md contains a bare unconditional layout-detection ' +
|
||||
'bash block (`ls -d .kilo ... || echo unknown`) with no surrounding ' +
|
||||
'framework-repo gate (#3290). ' +
|
||||
'Either remove the block entirely, or wrap it in a check like:\n' +
|
||||
' if [[ "$(jq -r \'.name // ""\' package.json 2>/dev/null)" == "@opengsd/gsd-core" ]]; then\n' +
|
||||
' # ... detection block ...\n' +
|
||||
' fi'
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Group B — no orphan downstream consumers ────────────────────────────────
|
||||
|
||||
describe('bug #3290 — Group B: layout-detection verdict has no downstream consumers', () => {
|
||||
const SOURCE_DIRS = [
|
||||
path.join(ROOT, 'agents'),
|
||||
path.join(ROOT, 'commands', 'gsd'),
|
||||
path.join(ROOT, 'gsd-core', 'workflows'),
|
||||
];
|
||||
|
||||
/**
|
||||
* Lines that reference the three possible verdict values emitted by the
|
||||
* detection block: "claude", "kilo", "unknown" — ONLY as the verdict output
|
||||
* of the gsd-intel-updater layout detection (not general runtime references).
|
||||
*
|
||||
* We look for the specific phrase "Layout detection returned" which is the
|
||||
* sentinel the noisy output line uses.
|
||||
*/
|
||||
test('no file contains "Layout detection returned" (the noisy verdict phrase)', () => {
|
||||
const matches = [];
|
||||
|
||||
for (const dir of SOURCE_DIRS) {
|
||||
const files = walkMd(dir);
|
||||
for (const file of files) {
|
||||
const rel = path.relative(ROOT, file);
|
||||
const src = fs.readFileSync(file, 'utf-8');
|
||||
if (src.includes('Layout detection returned')) {
|
||||
// Collect matching lines for the error message
|
||||
const lines = src.split(/\r?\n/)
|
||||
.map((l, i) => ({ line: l, n: i + 1 }))
|
||||
.filter(({ line }) => line.includes('Layout detection returned'));
|
||||
matches.push({ rel, lines });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
matches.length,
|
||||
0,
|
||||
'Expected zero files to contain "Layout detection returned" (the noisy verdict ' +
|
||||
'phrase from the gsd-intel-updater layout-detection block). Found:\n' +
|
||||
matches.map(({ rel, lines }) =>
|
||||
` ${rel}:\n${lines.map(({ n, line }) => ` L${n}: ${line.trim()}`).join('\n')}`
|
||||
).join('\n')
|
||||
);
|
||||
});
|
||||
|
||||
test('no agent or workflow instructs reading the layout-detection verdict output', () => {
|
||||
// The verdict was: echo "kilo" | echo "claude" | echo "unknown"
|
||||
// If any file references "Layout detection returned unknown" as an instruction
|
||||
// to consume, that would be a consumer. We verify none exist outside of
|
||||
// the producing file (gsd-intel-updater.md).
|
||||
const verdictConsumerPattern = /Layout detection returned.*(unknown|claude|kilo)/i;
|
||||
const consumers = [];
|
||||
|
||||
for (const dir of SOURCE_DIRS) {
|
||||
const files = walkMd(dir);
|
||||
for (const file of files) {
|
||||
// Exclude the producer itself — it defines the message, not consumes it
|
||||
if (path.basename(file) === 'gsd-intel-updater.md') continue;
|
||||
const src = fs.readFileSync(file, 'utf-8');
|
||||
if (verdictConsumerPattern.test(src)) {
|
||||
consumers.push(path.relative(ROOT, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(
|
||||
consumers,
|
||||
[],
|
||||
'Expected no downstream consumer of the layout-detection verdict. Found:\n' +
|
||||
consumers.map((f) => ` ${f}`).join('\n') +
|
||||
'\nIf a consumer exists, use option A (gate) not option B (remove).'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -489,3 +489,196 @@ describe('bug #3805: fast.md log_to_state must be schema-aware', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/bug-2421-planner-grep-gate-hygiene.test.cjs — consolidation epic #1969 (B7 #1976)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:bug-2421-planner-grep-gate-hygiene (consolidation epic #1969 B7 #1976)", () => {
|
||||
// allow-test-rule: source-text-is-the-product (see #2421)
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Bug #2421: gsd-planner emits grep-count acceptance gates that count comment text
|
||||
*
|
||||
* The planner must instruct agents to use comment-aware grep patterns in
|
||||
* <automated> verify blocks. Without this, descriptive comments in file
|
||||
* headers count against the gate and force authors to reword them — the
|
||||
* "self-invalidating grep gate" anti-pattern.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PLANNER_PATH = path.join(__dirname, '..', 'agents', 'gsd-planner.md');
|
||||
|
||||
describe('gsd-planner grep gate hygiene (#2421)', () => {
|
||||
test('gsd-planner.md exists in agents source dir', () => {
|
||||
assert.ok(fs.existsSync(PLANNER_PATH), 'agents/gsd-planner.md must exist');
|
||||
});
|
||||
|
||||
test('gsd-planner.md contains Grep gate hygiene rule', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('Grep gate hygiene') || content.includes('grep gate hygiene'),
|
||||
'gsd-planner.md must contain a "Grep gate hygiene" rule to prevent self-invalidating grep gates'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-planner.md explains self-invalidating grep gate anti-pattern', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('self-invalidating'),
|
||||
'gsd-planner.md must describe the "self-invalidating" grep gate anti-pattern'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-planner.md provides comment-stripping grep example', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
// Must show a pattern that excludes comment lines (grep -v or grep -vE)
|
||||
assert.ok(
|
||||
content.includes('grep -v') || content.includes('grep -vE') || content.includes('-v '),
|
||||
'gsd-planner.md must provide a comment-stripping grep example (grep -v or grep -vE)'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-planner.md warns against bare zero-count grep gates on whole files', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('== 0') || content.includes('zero-count') || content.includes('zero count'),
|
||||
'gsd-planner.md must warn against bare zero-count grep gates without comment exclusion'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-planner.md grep gate hygiene rule appears after Nyquist Rule', () => {
|
||||
const content = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
const nyquistIdx = content.indexOf('Nyquist Rule');
|
||||
const grepGateIdx = content.indexOf('grep gate hygiene') !== -1
|
||||
? content.indexOf('grep gate hygiene')
|
||||
: content.indexOf('Grep gate hygiene');
|
||||
|
||||
assert.ok(nyquistIdx !== -1, 'Nyquist Rule must be present in gsd-planner.md');
|
||||
assert.ok(grepGateIdx !== -1, 'Grep gate hygiene must be present in gsd-planner.md');
|
||||
assert.ok(
|
||||
grepGateIdx > nyquistIdx,
|
||||
`Grep gate hygiene rule (at ${grepGateIdx}) must appear after Nyquist Rule (at ${nyquistIdx})`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/bug-3087-planner-directive-language.test.cjs — consolidation epic #1969 (B7 #1976)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:bug-3087-planner-directive-language (consolidation epic #1969 B7 #1976)", () => {
|
||||
'use strict';
|
||||
|
||||
// Regression guard for bug #3087.
|
||||
//
|
||||
// Between v1.38.3 and v1.38.4, agents/gsd-planner.md had 10 instances of
|
||||
// CRITICAL/MANDATORY/ALWAYS/MUST directive emphasis systematically removed.
|
||||
// The change was undocumented and conflicts with the stated intent of PR #2489
|
||||
// (the sycophancy-hardening pass that shipped in the same release). This test
|
||||
// enforces the restored directive language so the demotion cannot recur silently.
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
let src;
|
||||
try {
|
||||
src = fs.readFileSync(path.join(ROOT, 'agents', 'gsd-planner.md'), 'utf8');
|
||||
} catch (err) {
|
||||
throw new Error(`agents/gsd-planner.md not found — was the file renamed? (${err.message})`);
|
||||
}
|
||||
|
||||
const directives = [
|
||||
{ desc: 'User Decision Fidelity heading is CRITICAL', pattern: /## CRITICAL: User Decision Fidelity/ },
|
||||
{ desc: 'Never Simplify heading is CRITICAL', pattern: /## CRITICAL: Never Simplify User Decisions/ },
|
||||
{ desc: 'Multi-Source Audit heading is MANDATORY', pattern: /## Multi-Source Coverage Audit \(MANDATORY in every plan set\)/ },
|
||||
{ desc: 'Source audit uses "Audit ALL" imperative', pattern: /Audit ALL four source types before finalizing/ },
|
||||
{ desc: 'Discovery is MANDATORY', pattern: /Discovery is MANDATORY unless/ },
|
||||
{ desc: 'Split signals use ALWAYS', pattern: /\*\*ALWAYS split if:\*\*/ },
|
||||
{ desc: 'requirements field doc uses MUST', pattern: /\*\*MUST\*\* list requirement IDs from ROADMAP/ },
|
||||
{ desc: 'Step 0 has CRITICAL requirement ID directive', pattern: /\*\*CRITICAL:\*\* Every requirement ID MUST appear/ },
|
||||
{ desc: 'Write tool directive uses ALWAYS', pattern: /\*\*ALWAYS use the Write tool to create files\*\*/ },
|
||||
{ desc: 'File naming convention heading is CRITICAL', pattern: /\*\*CRITICAL — File naming convention \(enforced\):\*\*/ },
|
||||
];
|
||||
|
||||
for (const { desc, pattern } of directives) {
|
||||
test(`gsd-planner.md: ${desc}`, () => {
|
||||
assert.ok(
|
||||
pattern.test(src),
|
||||
`Directive enforcement missing from gsd-planner.md: "${desc}" — pattern ${pattern} not found. ` +
|
||||
`This language was demoted in v1.38.4 (PR #2489) without documentation, conflicting with ` +
|
||||
`the sycophancy-hardening intent of that release. See bug #3087.`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/bug-3430-planner-phase-contract.test.cjs — consolidation epic #1969 (B7 #1976)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:bug-3430-planner-phase-contract (consolidation epic #1969 B7 #1976)", () => {
|
||||
// allow-test-rule: source-text-is-the-product (see #3430)
|
||||
// Planner markdown is the deployed planning contract; these checks lock the
|
||||
// exact canonical forms that downstream phase-plan-index accepts.
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const PLANNER_PATH = path.join(__dirname, '..', 'agents', 'gsd-planner.md');
|
||||
|
||||
function readPlanner() {
|
||||
return fs.readFileSync(PLANNER_PATH, 'utf8');
|
||||
}
|
||||
|
||||
test('#3430: planner SUMMARY instruction uses canonical padded phase/plan form', () => {
|
||||
const content = readPlanner();
|
||||
assert.match(
|
||||
content,
|
||||
/Create `\.planning\/phases\/XX-name\/\{padded_phase\}-\{plan\}-SUMMARY\.md` when done/,
|
||||
'planner must instruct executors to write SUMMARY files in canonical padded-phase form'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
content,
|
||||
/After completion, create `\.planning\/phases\/XX-name\/\{phase\}-\{plan\}-SUMMARY\.md`/,
|
||||
'planner must not instruct the broken {phase}-{plan}-SUMMARY.md form'
|
||||
);
|
||||
});
|
||||
|
||||
test('#3430: planner depends_on docs show canonical in-phase plan ids', () => {
|
||||
const content = readPlanner();
|
||||
assert.match(
|
||||
content,
|
||||
/depends_on:[^\n]*Use `01-01`\/`01-01-auth-hardening`/,
|
||||
'planner must document canonical depends_on examples that phase-plan-index resolves'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
content,
|
||||
/depends_on:[^\n]*01-trust\/01/,
|
||||
'planner must not document phase-slug/plan-number depends_on examples as canonical'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -311,3 +311,124 @@ describe('bug #222 recurrence: orchestrator self-heals when synthesizer returns
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/bug-2419-project-researcher-agent.test.cjs — consolidation epic #1969 (B7 #1976)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:bug-2419-project-researcher-agent (consolidation epic #1969 B7 #1976)", () => {
|
||||
// allow-test-rule: source-text-is-the-product (see #2419)
|
||||
// Reads .md/.json/.yml product files whose deployed text IS what the
|
||||
// runtime loads — testing text content tests the deployed contract.
|
||||
|
||||
/**
|
||||
* Bug #2419: gsd-project-researcher agent type not found
|
||||
*
|
||||
* When gsd-new-project spawns gsd-project-researcher subagents, it fails with
|
||||
* "agent type not found" if the user has a local-only install (agents in
|
||||
* .claude/agents/ of a different project, not the global ~/.claude/agents/).
|
||||
*
|
||||
* Fix: new-project.md and new-milestone.md must parse agents_installed from
|
||||
* the init JSON and warn the user (rather than silently failing) when agents
|
||||
* are missing.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const NEW_PROJECT_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'new-project.md');
|
||||
const NEW_MILESTONE_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'new-milestone.md');
|
||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
|
||||
describe('gsd-project-researcher agent registration (#2419)', () => {
|
||||
test('gsd-project-researcher.md exists in agents source dir', () => {
|
||||
const agentFile = path.join(AGENTS_DIR, 'gsd-project-researcher.md');
|
||||
assert.ok(
|
||||
fs.existsSync(agentFile),
|
||||
'agents/gsd-project-researcher.md must exist in the source agents directory'
|
||||
);
|
||||
});
|
||||
|
||||
test('gsd-project-researcher.md has correct name in frontmatter', () => {
|
||||
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-project-researcher.md'), 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('name: gsd-project-researcher'),
|
||||
'agents/gsd-project-researcher.md must have name: gsd-project-researcher in frontmatter'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project.md parses agents_installed from init JSON', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents_installed'),
|
||||
'new-project.md must parse agents_installed from the init JSON to detect missing agents'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project.md warns user when agents_installed is false', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents_installed') && content.includes('agent type not found') ||
|
||||
content.includes('agents_installed') && content.includes('missing') ||
|
||||
content.includes('agents_installed') && content.includes('not installed'),
|
||||
'new-project.md must warn the user when agents are not installed (agents_installed is false)'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project.md reports required-agent and skill-payload diagnostics separately', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
assert.ok(content.includes('required_agents_installed'),
|
||||
'new-project.md must parse required_agents_installed from init JSON');
|
||||
assert.ok(content.includes('missing_required_agents'),
|
||||
'new-project.md must report missing required new-project agents separately');
|
||||
assert.ok(content.includes('agent_skill_payloads_available'),
|
||||
'new-project.md must distinguish skill payload availability from agent definitions');
|
||||
assert.ok(content.includes('agents_dir'),
|
||||
'new-project.md must show which agents directory was checked');
|
||||
});
|
||||
|
||||
test('new-milestone.md parses agents_installed from init JSON', () => {
|
||||
const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents_installed'),
|
||||
'new-milestone.md must parse agents_installed from the init JSON to detect missing agents'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-milestone.md warns user when agents_installed is false', () => {
|
||||
const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('agents_installed') && (
|
||||
content.includes('agent type not found') ||
|
||||
content.includes('missing') ||
|
||||
content.includes('not installed')
|
||||
),
|
||||
'new-milestone.md must warn the user when agents are not installed (agents_installed is false)'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project.md lists gsd-project-researcher in available_agent_types', () => {
|
||||
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
|
||||
const agentTypesMatch = content.match(/<available_agent_types>([\s\S]*?)<\/available_agent_types>/);
|
||||
assert.ok(agentTypesMatch, 'new-project.md must have <available_agent_types> section');
|
||||
assert.ok(
|
||||
agentTypesMatch[1].includes('gsd-project-researcher'),
|
||||
'new-project.md <available_agent_types> must list gsd-project-researcher'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-milestone.md lists gsd-project-researcher in available_agent_types', () => {
|
||||
const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8');
|
||||
const agentTypesMatch = content.match(/<available_agent_types>([\s\S]*?)<\/available_agent_types>/);
|
||||
assert.ok(agentTypesMatch, 'new-milestone.md must have <available_agent_types> section');
|
||||
assert.ok(
|
||||
agentTypesMatch[1].includes('gsd-project-researcher'),
|
||||
'new-milestone.md <available_agent_types> must list gsd-project-researcher'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,3 +127,71 @@ test('shipped workflow flags behavior-unverified truths even on infrastructure p
|
||||
test('standalone template per-truth guideline respects gaps_found precedence', () => {
|
||||
assert.match(standalone, /becomes `human_needed`[\s\S]{0,80}?gaps_found/i);
|
||||
});
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/bug-3321-verifier-runs-probes.test.cjs — consolidation epic #1969 (B7 #1976)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:bug-3321-verifier-runs-probes (consolidation epic #1969 B7 #1976)", () => {
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const VERIFIER_AGENT = path.join(REPO_ROOT, 'agents', 'gsd-verifier.md');
|
||||
|
||||
function verifierProbeContract(content) {
|
||||
const sectionStart = content.indexOf('## Step 7c: Probe Execution');
|
||||
const sectionEnd = content.indexOf('## Step 8:', sectionStart);
|
||||
assert.notEqual(sectionStart, -1, 'verifier must define Step 7c');
|
||||
assert.notEqual(sectionEnd, -1, 'verifier must close Step 7c before Step 8');
|
||||
|
||||
const section = content.slice(sectionStart, sectionEnd);
|
||||
const codeBlocks = [...section.matchAll(/```bash\r?\n([\s\S]*?)\r?\n```/g)].map((match) => match[1].split(/\r?\n/).join('\n'));
|
||||
const executionSteps = [...section.matchAll(/^\d+\.\s+(.+)$/gm)].map((match) => match[1]);
|
||||
return {
|
||||
title: 'Step 7c: Probe Execution',
|
||||
conventionalDiscoveryCommand: codeBlocks[0]?.split('\n').find((line) => line.startsWith('find scripts')) || null,
|
||||
declaredDiscoveryCommand: codeBlocks[0]?.split('\n').find((line) => line.startsWith('grep -R')) || null,
|
||||
executionCommand: codeBlocks[1] || '',
|
||||
executionSteps,
|
||||
statusRows: [...section.matchAll(/^\|\s*`([^`]+)`\s*\|\s*`([^`]+)`\s*\|[^|]+\|\s*([^|]+)\|$/gm)]
|
||||
.map((match) => ({ probe: match[1], command: match[2], statuses: match[3].trim() })),
|
||||
summaryClaimsRejected: section.includes('SUMMARY.md probe pass claims are not evidence'),
|
||||
};
|
||||
}
|
||||
|
||||
describe('bug #3321: gsd-verifier runs probes instead of trusting SUMMARY claims', () => {
|
||||
test('verifier prompt requires direct probe discovery and execution', () => {
|
||||
const content = fs.readFileSync(VERIFIER_AGENT, 'utf8');
|
||||
const contract = verifierProbeContract(content);
|
||||
|
||||
assert.equal(contract.title, 'Step 7c: Probe Execution');
|
||||
assert.equal(contract.conventionalDiscoveryCommand, "find scripts -path '*/tests/probe-*.sh' -type f 2>/dev/null | sort");
|
||||
assert.equal(
|
||||
contract.declaredDiscoveryCommand,
|
||||
"grep -R -n -E 'probe-[^[:space:]]+\\.sh|scripts/.*/tests/probe-.*\\.sh' \"$PHASE_DIR\"/*-PLAN.md \"$PHASE_DIR\"/*-SUMMARY.md 2>/dev/null",
|
||||
);
|
||||
assert.deepEqual(contract.executionSteps, [
|
||||
'Build the `PROBES` list from explicit PLAN declarations first; include conventional `scripts/*/tests/probe-*.sh` when the phase is a migration/tooling phase or the success criteria mention probes.',
|
||||
'For every documented probe path, if the file is missing or unreadable, mark `MISSING_PROBE` and set `status: gaps_found`. Do not require the executable bit because probes run through `bash "$probe"`.',
|
||||
'Run each probe from the built `PROBES` list (declared + conventional) from the repository root:',
|
||||
'Exit code 0 is PASS. Any non-zero exit is FAILED and must include stdout/stderr evidence in VERIFICATION.md.',
|
||||
'Do not substitute executor narration, SUMMARY.md PASS-marker counts, or a different dry-run driver command for the probe result.',
|
||||
]);
|
||||
assert.equal(contract.executionCommand, 'for probe in "${PROBES[@]}"; do\n timeout 30s bash "$probe"\ndone');
|
||||
assert.deepEqual(contract.statusRows, [{
|
||||
probe: 'scripts/.../probe-name.sh',
|
||||
command: 'bash "$probe"',
|
||||
statuses: 'PASS / FAILED / MISSING_PROBE',
|
||||
}]);
|
||||
assert.equal(contract.summaryClaimsRejected, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user