// allow-test-rule: source-text-is-the-product // Agent .md files are the installed AI agents — their frontmatter and body text IS what // Claude Code loads at runtime. Checking text content IS checking the deployed contract. /** * MSD Agent Frontmatter Tests * * Validates that all agent .md files have correct frontmatter fields: * - Anti-heredoc instruction present in file-writing agents * - skills: field absent from all agents (breaks Gemini CLI) * - Commented hooks: pattern in file-writing agents * - Spawn type consistency across workflows */ const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); const { listAgentFiles } = require('./helpers/agent-roster.cjs'); const AGENTS_DIR = path.join(__dirname, '..', 'agents'); const WORKFLOWS_DIR = path.join(__dirname, '..', 'msd-core', 'workflows'); const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'msd'); // Sorted basenames (without `.md`); reads below re-add `.md` via `name + '.md'`. const ALL_AGENTS = listAgentFiles(AGENTS_DIR); const FILE_WRITING_AGENTS = ALL_AGENTS.filter(name => { const content = fs.readFileSync(path.join(AGENTS_DIR, name + '.md'), 'utf-8'); const toolsMatch = content.match(/^tools:\s*(.+)$/m); return toolsMatch && toolsMatch[1].includes('Write'); }); const READ_ONLY_AGENTS = ALL_AGENTS.filter(name => !FILE_WRITING_AGENTS.includes(name)); // ─── Anti-Heredoc Instruction ──────────────────────────────────────────────── describe('HDOC: anti-heredoc instruction', () => { for (const agent of FILE_WRITING_AGENTS) { test(`${agent} has anti-heredoc instruction`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); assert.ok( content.includes("never use `Bash(cat << 'EOF')` or heredoc"), `${agent} missing anti-heredoc instruction` ); }); } test('no active heredoc patterns in any agent file', () => { for (const agent of ALL_AGENTS) { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); // Match actual heredoc commands (not references in anti-heredoc instruction) const lines = content.split(/\r?\n/); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip lines that are part of the anti-heredoc instruction or markdown code fences if (line.includes('never use') || line.includes('NEVER') || line.trim().startsWith('```')) continue; // Check for actual heredoc usage instructions if (/^cat\s+<<\s*'?EOF'?\s*>/.test(line.trim())) { assert.fail(`${agent}:${i + 1} has active heredoc pattern: ${line.trim()}`); } } } }); }); // ─── Skills Frontmatter ────────────────────────────────────────────────────── describe('SKILL: skills frontmatter absent', () => { for (const agent of ALL_AGENTS) { test(`${agent} does not have skills: in frontmatter`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; assert.ok( !frontmatter.includes('skills:'), `${agent} has skills: in frontmatter — skills: breaks Gemini CLI and must be removed` ); }); } }); // ─── Hooks Frontmatter ─────────────────────────────────────────────────────── describe('HOOK: hooks frontmatter pattern', () => { for (const agent of FILE_WRITING_AGENTS) { test(`${agent} has commented hooks pattern`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; assert.ok( frontmatter.includes('# hooks:'), `${agent} missing commented hooks: pattern in frontmatter` ); }); } for (const agent of READ_ONLY_AGENTS) { test(`${agent} (read-only) does not need hooks`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; // Read-only agents may or may not have hooks — just verify they parse assert.ok(frontmatter.includes('name:'), `${agent} has valid frontmatter`); }); } }); // ─── Spawn Type Consistency ────────────────────────────────────────────────── describe('SPAWN: spawn type consistency', () => { // #1689: `subagent_type="{TOKEN}"` is a workflow-bound placeholder for // parameterized executor dispatch (resolved at runtime via `msd-tools // resolve-agent`, defaulting to msd-executor), not a concrete agent name. // The static spawn-type checks below skip these — the effective value is // validated at dispatch time, and execute-phase.md still documents the // built-in roster (incl. msd-executor) in . const PARAMETERIZED_SPAWN_TYPE = /^\{[^}]+\}$/; test('no "First, read agent .md" workaround pattern remains', () => { const dirs = [WORKFLOWS_DIR, COMMANDS_DIR]; for (const dir of dirs) { if (!fs.existsSync(dir)) continue; const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); for (const file of files) { const content = fs.readFileSync(path.join(dir, file), 'utf-8'); const hasWorkaround = content.includes('First, read ~/.claude/agents/msd-'); assert.ok( !hasWorkaround, `${file} still has "First, read agent .md" workaround — use named subagent_type instead` ); } } }); test('named agent spawns use correct agent names', () => { const validAgentTypes = new Set([ ...ALL_AGENTS, 'general-purpose', // Allowed for orchestrator spawns ]); const dirs = [WORKFLOWS_DIR, COMMANDS_DIR]; for (const dir of dirs) { if (!fs.existsSync(dir)) continue; const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); for (const file of files) { const content = fs.readFileSync(path.join(dir, file), 'utf-8'); // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow/command markdown, bounded prose, not adversarial input const matches = content.matchAll(/subagent_type="([^"]+)"/g); for (const match of matches) { const agentType = match[1]; if (PARAMETERIZED_SPAWN_TYPE.test(agentType)) continue; assert.ok( validAgentTypes.has(agentType), `${file} references unknown agent type: ${agentType}` ); } } } }); test('diagnose-issues uses msd-debugger (not general-purpose)', () => { const content = fs.readFileSync( path.join(WORKFLOWS_DIR, 'diagnose-issues.md'), 'utf-8' ); assert.ok( content.includes('subagent_type="msd-debugger"'), 'diagnose-issues should spawn msd-debugger, not general-purpose' ); }); test('workflows spawning named agents have listing (#1357)', () => { // After /clear, Claude Code re-reads workflow instructions but loses agent // context. Without an section, the orchestrator may // fall back to general-purpose, silently breaking agent capabilities. // PR #1139 added this to plan-phase and execute-phase but missed all other // workflows that spawn named MSD agents. const dirs = [WORKFLOWS_DIR, COMMANDS_DIR]; for (const dir of dirs) { if (!fs.existsSync(dir)) continue; const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); for (const file of files) { const content = fs.readFileSync(path.join(dir, file), 'utf-8'); // Find all named subagent_type references (excluding general-purpose // and #1689 runtime placeholders) // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow/command markdown, bounded prose, not adversarial input const matches = [...content.matchAll(/subagent_type="([^"]+)"/g)]; const namedAgents = matches .map(m => m[1]) .filter(t => t !== 'general-purpose') .filter(t => !PARAMETERIZED_SPAWN_TYPE.test(t)); if (namedAgents.length === 0) continue; // Workflow spawns named agents — must have assert.ok( content.includes(''), `${file} spawns named agents (${[...new Set(namedAgents)].join(', ')}) ` + `but has no section — after /clear, the ` + `orchestrator may fall back to general-purpose (#1357)` ); // Every spawned agent type must appear in the listing for (const agent of new Set(namedAgents)) { const agentTypesMatch = content.match( // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content /([\s\S]*?)<\/available_agent_types>/ ); assert.ok( agentTypesMatch, `${file} has malformed section` ); assert.ok( agentTypesMatch[1].includes(agent), `${file} spawns ${agent} but does not list it in ` ); } } } }); test('execute-phase has a sequential inline fallback in runtime_compatibility', () => { const content = fs.readFileSync( path.join(WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8' ); assert.ok( content.includes('sequential inline execution'), 'execute-phase must document sequential inline execution as a fallback' ); assert.ok( content.includes('spot-check'), 'execute-phase must have spot-check fallback for completion detection' ); }); }); // ─── Required Frontmatter Fields ───────────────────────────────────────────── describe('AGENT: required frontmatter fields', () => { for (const agent of ALL_AGENTS) { test(`${agent} has name, description, tools, color`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; assert.ok(frontmatter.includes('name:'), `${agent} missing name:`); assert.ok(frontmatter.includes('description:'), `${agent} missing description:`); assert.ok(frontmatter.includes('tools:'), `${agent} missing tools:`); assert.ok(frontmatter.includes('color:'), `${agent} missing color:`); }); } }); // ─── Model resolution uniformity (#3895) ───────────────────────────────────── describe('MODEL: no agent hardcodes a model frontmatter pin', () => { // Exactly one shipped agent (msd-mempalace-curator) carried `model: sonnet` // while the other 33 resolved through the model-profile system. The pin // intercepted #2517's deliberate inherit case — the ship:post dispatch OMITS // model= on inherit so the agent inherits the orchestrator's model, but the // frontmatter pin silently forced sonnet there. The catalog entry // (model-catalog.json: golden/balanced sonnet) preserves default-profile // behavior; operators regain model_overrides + inherit authority. test('no shipped agent frontmatter contains a model: pin', () => { const offenders = []; for (const agent of ALL_AGENTS) { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; if (/^model:/m.test(frontmatter)) offenders.push(agent); } assert.deepEqual( offenders, [], `agents must resolve models via the model-profile system, not a frontmatter pin (#3895): ${offenders.join(', ')}` ); }); test('the curator keeps its catalog entry (deleting the pin must not orphan the agent)', () => { const catalog = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'msd-core', 'bin', 'shared', 'model-catalog.json'), 'utf-8')); const entry = catalog.agents && catalog.agents['msd-mempalace-curator']; assert.ok(entry, 'catalog entry for msd-mempalace-curator must exist'); assert.equal(entry.golden, 'sonnet', 'golden profile preserves the pinned behavior'); assert.equal(entry.balanced, 'sonnet', 'balanced profile preserves the pinned behavior'); }); }); // ─── Color Value Validation ────────────────────────────────────────────────── const VALID_AGENT_COLORS = new Set(['red', 'blue', 'green', 'yellow', 'purple', 'orange', 'pink', 'cyan']); describe('COLOR: color frontmatter must be a documented named color', () => { for (const agent of ALL_AGENTS) { test(`${agent} color: is a documented named color`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md frontmatter block, fixed-size author-controlled content const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); const frontmatter = fmMatch ? fmMatch[1] : ''; const colorMatch = frontmatter.match(/^color:\s*(.+)$/m); assert.ok(colorMatch, `${agent} missing color: field in frontmatter`); const rawValue = colorMatch[1].trim(); // Strip surrounding quotes (single or double) before validating const colorValue = rawValue.replace(/^["']|["']$/g, ''); assert.ok( VALID_AGENT_COLORS.has(colorValue), `${agent} has invalid color: "${colorValue}" — must be one of: ${[...VALID_AGENT_COLORS].join(', ')}` ); }); } }); // ─── CLAUDE.md Compliance ─────────────────────────────────────────────────── describe('CLAUDEMD: CLAUDE.md compliance enforcement', () => { test('msd-plan-checker has Dimension 10: CLAUDE.md Compliance', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'msd-plan-checker.md'), 'utf-8'); assert.ok( content.includes('Dimension 10: CLAUDE.md Compliance'), 'msd-plan-checker must have Dimension 10 for CLAUDE.md compliance checking' ); assert.ok( content.includes('claude_md_compliance'), 'msd-plan-checker must use claude_md_compliance as dimension identifier' ); }); test('msd-phase-researcher has CLAUDE.md enforcement directive', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'msd-phase-researcher.md'), 'utf-8'); assert.ok( content.includes('CLAUDE.md enforcement'), 'msd-phase-researcher must enforce CLAUDE.md directives during research' ); assert.ok( content.includes('Project Constraints (from CLAUDE.md)'), 'msd-phase-researcher must output a Project Constraints section from CLAUDE.md' ); }); test('msd-executor has CLAUDE.md enforcement directive', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'msd-executor.md'), 'utf-8'); assert.ok( content.includes('CLAUDE.md enforcement'), 'msd-executor must enforce CLAUDE.md directives during execution' ); assert.ok( content.includes('CLAUDE.md rule — it takes precedence over plan instructions'), 'msd-executor must specify CLAUDE.md precedence over plan instructions' ); }); test('all three agents read CLAUDE.md in project_context', () => { const agents = ['msd-plan-checker', 'msd-phase-researcher', 'msd-executor']; for (const agent of agents) { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); assert.ok( content.includes('Read `./CLAUDE.md`'), `${agent} must read ./CLAUDE.md in project_context section` ); } }); }); // ─── Verification Data-Flow and Environment Audit (#1245) ──────────────────── describe('VERIFY: data-flow trace, environment audit, and behavioral spot-checks', () => { test('msd-verifier has Step 4b: Data-Flow Trace', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'msd-verifier.md'), 'utf-8'); assert.ok( content.includes('Step 4b: Data-Flow Trace'), 'msd-verifier must have Step 4b for data-flow tracing' ); assert.ok( content.includes('HOLLOW'), 'msd-verifier must define HOLLOW status for wired-but-disconnected artifacts' ); assert.ok( content.includes('DISCONNECTED'), 'msd-verifier must define DISCONNECTED status for missing data sources' ); }); test('msd-verifier has Step 7b: Behavioral Spot-Checks', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'msd-verifier.md'), 'utf-8'); assert.ok( content.includes('Step 7b: Behavioral Spot-Checks'), 'msd-verifier must have Step 7b for behavioral spot-checks' ); assert.ok( content.includes('SKIP'), 'msd-verifier spot-checks must support SKIP status for untestable items' ); }); test('msd-verifier VERIFICATION.md template includes data-flow and spot-check sections', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'msd-verifier.md'), 'utf-8'); assert.ok( content.includes('Data-Flow Trace (Level 4)'), 'VERIFICATION.md template must include Data-Flow Trace section' ); assert.ok( content.includes('Behavioral Spot-Checks'), 'VERIFICATION.md template must include Behavioral Spot-Checks section' ); }); test('msd-verifier success criteria include data-flow and spot-checks', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'msd-verifier.md'), 'utf-8'); assert.ok( content.includes('Data-flow trace (Level 4)'), 'success criteria must include data-flow trace step' ); assert.ok( content.includes('Behavioral spot-checks run'), 'success criteria must include behavioral spot-checks step' ); }); test('msd-phase-researcher has Step 2.6: Environment Availability Audit', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'msd-phase-researcher.md'), 'utf-8'); assert.ok( content.includes('Step 2.6: Environment Availability Audit'), 'msd-phase-researcher must have Step 2.6 for environment availability auditing' ); assert.ok( content.includes('Environment Availability'), 'msd-phase-researcher must include Environment Availability section in RESEARCH.md template' ); }); test('msd-phase-researcher success criteria include environment audit', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'msd-phase-researcher.md'), 'utf-8'); assert.ok( content.includes('Environment availability audited'), 'success criteria must include environment availability audit step' ); }); }); // ─── Discussion Log ────────────────────────────────────────────────────────── describe('DISCUSS: discussion log generation', () => { test('discuss-phase workflow references DISCUSSION-LOG.md generation', () => { // After the discuss-phase progressive-disclosure split (#717), the DISCUSSION-LOG.md template // body lives in workflows/discuss-phase/templates/discussion-log.md and is // read at the git_commit step. Both files together must satisfy the // documentation contract. const parent = fs.readFileSync( path.join(WORKFLOWS_DIR, 'discuss-phase.md'), 'utf-8' ); const tplPath = path.join(WORKFLOWS_DIR, 'discuss-phase', 'templates', 'discussion-log.md'); const tpl = fs.existsSync(tplPath) ? fs.readFileSync(tplPath, 'utf-8') : ''; const content = parent + '\n' + tpl; assert.ok( content.includes('DISCUSSION-LOG.md'), 'discuss-phase must reference DISCUSSION-LOG.md generation' ); assert.ok( content.includes('Audit trail only'), 'discuss-phase (or its discussion-log template after the discuss-phase/modes split) must mark discussion log as audit-only' ); }); test('discussion-log template exists', () => { const templatePath = path.join(__dirname, '..', 'msd-core', 'templates', 'discussion-log.md'); assert.ok( fs.existsSync(templatePath), 'discussion-log.md template must exist' ); const content = fs.readFileSync(templatePath, 'utf-8'); assert.ok( content.includes('Do not use as input to planning'), 'template must contain audit-only notice' ); }); }); // ─── Section-writer agents must carry both Write and Edit (#581) ──────────── describe('EDITWRITE: section-writer agents must have both Write and Edit in tools', () => { // These agents perform in-place section edits on shared/existing files (e.g. // AI-SPEC.md). Without Edit in tools:, the "Edit-only" discipline in their // spawn-prompt is unenforceable — they fall back to whole-file Write and // clobber sibling sections. Same bug class as #571/#575 (fixed msd-doc-writer). // Issue #581. const SECTION_WRITER_AGENTS = [ 'msd-eval-planner', 'msd-ai-researcher', 'msd-domain-researcher', 'msd-phase-researcher', 'msd-ui-researcher', 'msd-debug-session-manager', 'msd-planner', // #973: planner lacked Edit; whole-file Write truncated ROADMAP.md ]; for (const agent of SECTION_WRITER_AGENTS) { test(`${agent} has both Write and Edit in tools: (#581)`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const toolsMatch = content.match(/^tools:\s*(.+)$/m); assert.ok(toolsMatch, `${agent} missing tools: line in frontmatter`); const tools = toolsMatch[1].split(',').map(t => t.trim()); assert.ok( tools.includes('Write'), `${agent} missing Write in tools: — required for file creation` ); assert.ok( tools.includes('Edit'), `${agent} missing Edit in tools: — required to enforce Edit-only discipline on shared files (#581)` ); }); } }); // ─── Cross-runtime agent compatibility (#1522) ────────────────────────────── describe('COMPAT: agents must not use runtime-specific frontmatter keys', () => { // permissionMode is Claude Code-specific and breaks Gemini CLI agent loading. // It also has no effect on subagent Write permissions in Claude Code (blocked // at runtime level regardless). See #1522, #1387. const AGENTS_WITH_WRITE = ['msd-executor', 'msd-debugger']; for (const agent of AGENTS_WITH_WRITE) { test(`${agent} does not have permissionMode (breaks Gemini CLI)`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); const frontmatter = content.split('---')[1] || ''; assert.ok( !frontmatter.includes('permissionMode'), `${agent} must not have permissionMode — it breaks Gemini CLI agent loading (#1522) ` + `and has no effect in Claude Code (#1387)` ); }); } }); // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2346-agent-read-loop-guards.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2346-agent-read-loop-guards (consolidation epic #1969 B7 #1976)", () => { /** * Regression tests for bug #2346 * * Multiple MSD agents (msd-ui-checker, msd-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. * msd-pattern-mapper was fixed in #2312; this covers the remaining agents. * * Fix: add 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 (see #2346) // The 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('msd-ui-checker', () => { const agentPath = path.join(AGENTS_DIR, 'msd-ui-checker.md'); const content = fs.readFileSync(agentPath, 'utf-8'); test('agent file exists', () => { assert.ok(fs.existsSync(agentPath), 'agents/msd-ui-checker.md must exist'); }); test('has block', () => { assert.ok( content.includes(''), 'msd-ui-checker.md must have a block to prevent unbounded read loops (#2346)' ); }); test('critical_rules contains no-re-read constraint', () => { const rulesStart = content.indexOf(''); const rulesEnd = content.indexOf('', rulesStart); assert.ok(rulesStart !== -1 && rulesEnd !== -1, ' 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(''); const successIdx = content.indexOf(''); assert.ok(rulesIdx !== -1 && successIdx !== -1, 'both sections must exist'); assert.ok( rulesIdx < successIdx, ' must appear before ' ); }); }); describe('msd-planner', () => { const agentPath = path.join(AGENTS_DIR, 'msd-planner.md'); const content = fs.readFileSync(agentPath, 'utf-8'); test('agent file exists', () => { assert.ok(fs.existsSync(agentPath), 'agents/msd-planner.md must exist'); }); test('has block', () => { assert.ok( content.includes(''), 'msd-planner.md must have a block to prevent unbounded read loops (#2346)' ); }); test('critical_rules contains no-re-read constraint', () => { const rulesStart = content.indexOf(''); const rulesEnd = content.indexOf('', rulesStart); assert.ok(rulesStart !== -1 && rulesEnd !== -1, ' 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(''); const successIdx = content.lastIndexOf(''); assert.ok(rulesIdx !== -1 && successIdx !== -1, 'both sections must exist'); assert.ok( rulesIdx < successIdx, ' must appear before ' ); }); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-3605-stale-research-insert-phase-agent-refs.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-3605-stale-research-insert-phase-agent-refs (consolidation epic #1969 B7 #1976)", () => { // allow-test-rule: source-text-is-the-product (see #3605) // 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 /msd-research-phase (replaced by * /msd-plan-phase --research-phase ) and v1.40.0 consolidated /msd-insert-phase * into /msd-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 /msd- → /msd: 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 = [ '/msd-research-phase', '/msd-insert-phase', '/msd-add-phase', '/msd-remove-phase', '/msd-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'), ); }); } }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/enh-2427-sycophancy-hardening.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:enh-2427-sycophancy-hardening (consolidation epic #1969 B7 #1976)", () => { '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 MSD X" opening in ) * 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 = [ 'msd-plan-checker.md', 'msd-code-reviewer.md', 'msd-security-auditor.md', 'msd-verifier.md', 'msd-eval-auditor.md', 'msd-nyquist-auditor.md', 'msd-ui-auditor.md', 'msd-integration-checker.md', 'msd-doc-verifier.md', ]; function readAgent(agentsDir, filename) { return fs.readFileSync(path.join(agentsDir, filename), 'utf-8'); } function extractRole(content) { const match = content.match(/([\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 — does not open with "You are a MSD"', () => { content = content || readAgent(AGENTS_DIR, filename); role = role || extractRole(content); const firstSentence = role.trim().slice(0, 80); assert.ok( !firstSentence.startsWith('You are a MSD'), `${filename}: must not open with "You are a MSD" — use third-person submission framing. Got: "${firstSentence}"` ); }); test('(2) FORCE adversarial stance — block present', () => { content = content || readAgent(AGENTS_DIR, filename); assert.ok( content.includes(''), `${filename}: must contain block` ); assert.ok( content.includes('FORCE stance'), `${filename}: 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 ` ); }); test('(4) BLOCKER/WARNING classification requirement present', () => { content = content || readAgent(AGENTS_DIR, filename); assert.ok( content.includes('**BLOCKER**'), `${filename}: must define BLOCKER classification in ` ); assert.ok( content.includes('**WARNING**'), `${filename}: must define WARNING classification in ` ); }); }); } }); // sdk/prompts/agents/ was removed in 377a6d2 — SDK now loads installed agents directly. }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-571-doc-writer-fix-mode-edit-only.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-571-doc-writer-fix-mode-edit-only (consolidation epic #1969 B7 #1976)", () => { /** * Regression tests for bug #571 * * msd-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 (see #571) // 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, '..', 'msd-core', 'workflows'); const AGENT_PATH = path.join(AGENTS_DIR, 'msd-doc-writer.md'); const WORKFLOW_PATH = path.join(WORKFLOWS_DIR, 'docs-update.md'); // ─── Agent fix: Edit in tools frontmatter ──────────────────────────────────── describe('bug #571: msd-doc-writer agent', () => { const content = fs.readFileSync(AGENT_PATH, 'utf-8'); test('agent file exists', () => { assert.ok(fs.existsSync(AGENT_PATH), 'agents/msd-doc-writer.md must exist'); }); test('tools frontmatter includes Edit', () => { const toolsMatch = content.match(/^tools:\s*(.+)$/m); assert.ok(toolsMatch, 'msd-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(''); const fixEnd = content.indexOf('', fixStart); assert.ok(fixStart !== -1 && fixEnd !== -1, ' 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(''); const rulesEnd = content.indexOf('', rulesStart); assert.ok(rulesStart !== -1 && rulesEnd !== -1, ' 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(''); const successIdx = content.indexOf(''); assert.ok(rulesIdx !== -1 && successIdx !== -1, 'both and must exist'); assert.ok( rulesIdx < successIdx, ' must appear before (#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), 'msd-core/workflows/docs-update.md must exist'); }); describe('fix_loop step', () => { const loopStart = content.indexOf(''); const loopEnd = content.indexOf('', 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)' ); }); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-214-writer-agents-write-truncation-contract.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-214-writer-agents-write-truncation-contract (consolidation epic #1969 B7 #1976)", () => { // allow-test-rule: source-text-is-the-product (see #214) '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. msd-phase-researcher // is locked by its own bug-214 test; this locks the other large-file writers. const WRITER_AGENTS = [ 'msd-research-synthesizer', 'msd-planner', 'msd-executor', 'msd-domain-researcher', 'msd-project-researcher', 'msd-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, //, `${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.` ); }); }); } }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-214-phase-researcher-write-truncation-contract.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-214-phase-researcher-write-truncation-contract (consolidation epic #1969 B7 #1976)", () => { // allow-test-rule: source-text-is-the-product (see #214) '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', 'msd-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, //, '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).' ); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2990-code-fixer-worktree-branch.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2990-code-fixer-worktree-branch (consolidation epic #1969 B7 #1976)", () => { 'use strict'; // allow-test-rule: source-text-is-the-product (see #2990) // agents/msd-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. // Consolidation #1969: scope MSD_TEST_MODE to this folded block so it does not // leak into sibling folded suites in the shared file (was process-isolated when // standalone). This unit suite only parses agent markdown, but keep the flag set // for its own duration to preserve the origin behaviour, and restore it after. const { before: __gtmBefore, after: __gtmAfter } = require('node:test'); const __savedMsdTestMode = process.env.MSD_TEST_MODE; __gtmBefore(() => { process.env.MSD_TEST_MODE = '1'; }); __gtmAfter(() => { if (__savedMsdTestMode === undefined) delete process.env.MSD_TEST_MODE; else process.env.MSD_TEST_MODE = __savedMsdTestMode; }); /** * Bug #2990: msd-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 `msd-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', 'msd-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: msd-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 msd-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)}`); }); }); /** * Bug #2647: msd-code-fixer hand-rolled its worktree at a hardcoded `/tmp/sv-...` * mktemp path, which on Windows/Git Bash landed OUTSIDE the project tree — * outside the agent session's permission allowlist, so every Read inside the * worktree prompted — and mktemp's MAX_PATH-avoidance substitute produced an * un-removable `C:/mvwtNN` path. The fix places the worktree repo-relative * under `.claude/worktrees/` (the same dir the harness-managed executor * worktrees use: gitignored via `.claude/`, inside the permission scope), * with a PID+epoch suffix for concurrency uniqueness. * * These assertions parse the `wt=...` assignment(s) out of the agent markdown * (source-text-is-the-product, same allow-test-rule as the #2990 block above) * and assert the path is repo-relative under `.claude/worktrees/`, never an * absolute `/tmp` path, and concurrency-unique. */ describe('Bug #2647: msd-code-fixer worktree path is repo-relative, not a hardcoded /tmp path', () => { const md = fs.readFileSync(AGENT_PATH, 'utf-8'); // Extract `wt=...` shell assignments from fenced bash blocks (skip inline // backtick spans and bash comments, mirroring parseWorktreeAddInvocations). function parseWtAssignments(markdown) { const assigns = []; const lines = markdown.split('\n'); for (const line of lines) { // Match `wt=` only as a whole token (preceded by start-of-line or // whitespace) so `prior_wt=` and `${reviewfix_wt}`-style names are not // captured. `\b` would also match the `wt` in `prior_wt`, so anchor on // `(?:^|\s)wt=`. const m = /(?:^|\s)wt=/.exec(line); if (!m) continue; const idx = m.index + m[0].length - 'wt='.length; // Skip inline-code spans (odd backtick count before the match). const before = line.slice(0, idx); if ((before.match(/`/g) || []).length % 2 === 1) continue; // Skip bash comments. if (line.trimStart().startsWith('#')) continue; // Skip `wt="."` (the use_worktrees=false opt-out — not a worktree path). const value = line.slice(idx + 'wt='.length).trim(); if (value === '"."' || value === "'.'") continue; assigns.push(value); } return assigns; } test('the worktree path is NOT an absolute /tmp path', () => { const wtAssigns = parseWtAssignments(md); assert.ok(wtAssigns.length > 0, 'expected at least one wt= assignment in msd-code-fixer.md'); const tmpViolations = wtAssigns.filter(v => /\/tmp\/sv-|mktemp -d "\/tmp\//.test(v)); assert.deepEqual( tmpViolations, [], `worktree paths still hardcoded to /tmp (#2647): ${JSON.stringify(tmpViolations, null, 2)}`, ); }); test('the worktree path is repo-relative under .claude/worktrees/', () => { const wtAssigns = parseWtAssignments(md); assert.ok(wtAssigns.length > 0, 'expected at least one wt= assignment in msd-code-fixer.md'); const nonRelative = wtAssigns.filter(v => !v.includes('.claude/worktrees/rf-')); assert.deepEqual( nonRelative, [], `worktree paths not under .claude/worktrees/ (#2647): ${JSON.stringify(nonRelative, null, 2)}`, ); }); test('the worktree path is concurrency-unique (PID + epoch suffix)', () => { const wtAssigns = parseWtAssignments(md); assert.ok(wtAssigns.length > 0, 'expected at least one wt= assignment in msd-code-fixer.md'); // The path must carry BOTH a PID (`$$`) AND a time component (`$(date +%s)`) // so concurrent runs for the same phase do not collide — the property // mktemp's XXXXXX provided before #2647. Requiring both (not either) is // faithful to the fix: PID alone could recycle after wrap; epoch alone could // collide for two same-second runs. const nonUnique = wtAssigns.filter(v => !v.includes('$$') || !v.includes('$(date +%s)')); assert.deepEqual( nonUnique, [], `worktree paths lack the PID+epoch concurrency-uniqueness suffix (#2647): ${JSON.stringify(nonUnique, 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/msd-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. // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input 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.. 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)}`); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2686-review-fix-worktree.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2686-review-fix-worktree (consolidation epic #1969 B7 #1976)", () => { /** * Regression test for bug #2686 * * The msd-code-fixer agent (spawned by /msd-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 MSD agent at * /private/tmp/sv-. */ 'use strict'; // allow-test-rule: source-text-is-the-product (see #2686) // The msd-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', 'msd-code-fixer.md'); assert.ok(fs.existsSync(agentPath), 'agents/msd-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, 'msd-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 ` (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 `msd-sdk query commit` const commitMatch = /(?:git commit|msd-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'), 'msd-code-fixer.md must include worktree cleanup (git worktree remove) to avoid leaking tmp directories (#2686)' ); }); test('agent instructions use a repo-relative worktree path under .claude/worktrees/ (not a hardcoded /tmp path)', () => { // #2647: the original #2686 fix placed the worktree at a hardcoded `/tmp/sv-` // mktemp path to match sibling MSD agents. On Windows/Git Bash that landed // OUTSIDE the project tree — outside the agent session's permission allowlist, // so every Read inside the worktree prompted — and mktemp's MAX_PATH-avoidance // substitute produced an un-removable `C:/mvwtNN` path. The worktree must now // be repo-relative under `.claude/worktrees/` (the same dir the harness-managed // executor worktrees use: gitignored via `.claude/`, inside the permission // scope), and the agent must NOT define a `/tmp/sv-` worktree path. const hasTmpWorktreePath = /\/tmp\/sv-/.test(agentContent) || /\bwt\s*=\s*["']?\/tmp\/sv-/.test(agentContent); assert.ok( !hasTmpWorktreePath, 'msd-code-fixer.md must NOT define a worktree variable at a /tmp/sv-... path (#2647 — use a repo-relative .claude/worktrees/ path instead)' ); assert.ok( /\.claude\/worktrees\//.test(agentContent), 'msd-code-fixer.md must define the worktree under .claude/worktrees/ (repo-relative, gitignored, inside the permission scope) (#2647)' ); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/enh-2500-codebase-mapper-arch-rich-format.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:enh-2500-codebase-mapper-arch-rich-format (consolidation epic #1969 B7 #1976)", () => { /** * Enhancement #2500: msd-codebase-mapper (arch focus) rich architecture output * * The codebase/ARCHITECTURE.md produced by msd-codebase-mapper was a sparse * structural inventory — file listings and module relationships. After a major * refactor, research/ARCHITECTURE.md (created at /msd-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 * - 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 (see #2500) // The msd-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', 'msd-codebase-mapper.md'); describe('enh-2500: msd-codebase-mapper arch focus — rich architecture output', () => { let agentContent; let archTemplate; before(() => { assert.ok(fs.existsSync(AGENT_PATH), 'agents/msd-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(' 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)' ); }); }); }); }