Files
msd-core/tests/secure-phase-single-writer.test.cjs
Tom Boucher b5ce72f729 fix(#2119): single SECURITY.md writer — auditor is return-only (#2154)
* fix(2119): single SECURITY.md writer — auditor is return-only

The gsd-security-auditor held Write/Edit and was instructed to write
SECURITY.md (no <N>- prefix, no template frontmatter), while the
orchestrator's Step 6 also wrote the correct padded <N>-SECURITY.md
from templates/SECURITY.md. Two writers, two naming conventions, two
shapes — the auditor's unprefixed file was invisible to the workflow's
*-SECURITY.md glob detector and unparseable for the threats_open gate.

Fix (option 1 from the issue): make the auditor return-only.
- Remove Write/Edit from auditor's tools
- Rewrite all 'Write SECURITY.md' instructions to 'Return structured
  verdict' with threats_open count
- Add explicit constraint in workflow Step 5 spawn prompt
- Update existing test (was asserting Write in tools — now asserts absence)
- Add new regression test for single-writer contract
- Update docs/AGENTS.md stale Tools/Produces rows
- Regenerate golden fixtures + agent size baseline

* docs(changeset): backfill PR number (#2154)

* chore(#2119): regenerate pi/qwen golden fixtures after next merge

The single-writer change edits gsd-core/workflows/secure-phase.md and
agents/gsd-security-auditor.md; pi.json (added on next) and qwen.json (merge
straggler) were the only runtime fixtures still holding pre-change hashes for
those files. All other runtimes already reflect the change. Regenerated via
the sanctioned gen-golden-install-parity script.

* merge origin/next — regenerate goldens + baseline for merged state

* fix slash-command syntax: /gsd-secure-phase → /gsd:secure-phase (#2154 CI fix)
2026-07-13 00:47:15 -04:00

74 lines
2.4 KiB
JavaScript

/**
* Regression test for #2119: /gsd-secure-phase dual SECURITY.md writers.
*
* The gsd-security-auditor agent must NOT have Write/Edit tools — the
* orchestrator (secure-phase.md Step 6) is the sole SECURITY.md writer.
* The auditor returns a structured verdict; it never writes files.
*/
const { describe, test } = 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-security-auditor.md');
function parseYamlTools(content) {
const lines = content.split(/\r?\n/);
let inFrontmatter = false;
let inTools = false;
const tools = [];
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === '---') {
inFrontmatter = !inFrontmatter;
if (!inFrontmatter) break;
continue;
}
if (!inFrontmatter) continue;
if (trimmed.startsWith('tools:')) {
inTools = true;
continue;
}
if (inTools) {
if (trimmed.startsWith('- ')) {
tools.push(trimmed.slice(2).trim());
} else if (trimmed && !trimmed.startsWith('#')) {
inTools = false;
}
}
}
return tools;
}
describe('#2119 — security auditor is return-only (no file writes)', () => {
const content = fs.readFileSync(AGENT_PATH, 'utf-8');
test('auditor tools do not include Write or Edit', () => {
const tools = parseYamlTools(content);
assert.ok(tools.length > 0, 'tools list should be non-empty');
assert.ok(
!tools.includes('Write'),
`Write must not be in auditor tools (got: ${tools.join(', ')}) — orchestrator is the sole SECURITY.md writer (#2119)`,
);
assert.ok(
!tools.includes('Edit'),
`Edit must not be in auditor tools (got: ${tools.join(', ')}) — orchestrator is the sole SECURITY.md writer (#2119)`,
);
});
test('auditor description does not claim to produce SECURITY.md', () => {
const lines = content.split(/\r?\n/);
const descLine = lines.find((l) => l.startsWith('description:'));
assert.ok(descLine, 'description field must exist');
assert.ok(
!descLine.includes('Produces SECURITY.md'),
'description must not claim to produce SECURITY.md — auditor returns a verdict, orchestrator writes (#2119)',
);
assert.ok(
descLine.includes('Returns structured') || descLine.includes('returns'),
'description should state the auditor returns a structured verdict',
);
});
});