Files
msd-core/tests/model-routing-spawn-threading.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

151 lines
6.5 KiB
JavaScript

// allow-test-rule: source-text-is-the-product #2072
// Workflow .md files ARE the deployed orchestration contract the runtime executes;
// asserting that a spawn threads a resolved model= is asserting the deployed contract.
'use strict';
/**
* #2072 — model_overrides / models.<phaseType> were silently inert for
* msd-assumptions-analyzer and msd-code-reviewer on Claude: the resolver honored
* them, but the workflows spawned the agents with NO model= param, so the resolved
* value never reached the Agent tool and the agents inherited the session model.
*
* Fix contract: every spawn of these two catalog agents must thread a resolved
* model=, and the workflow must obtain that model (inline `resolve-model` for the
* single-agent workflows, or the `reviewer_model` field of the init.quick bundle).
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const WF = path.join(__dirname, '..', 'msd-core', 'workflows');
const read = (rel) => fs.readFileSync(path.join(WF, rel), 'utf-8');
// Every .md under msd-core/workflows (incl. nested steps/ and modes/).
function allWorkflowMd() {
const out = [];
(function walk(dir, rel) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
if (e.isDirectory()) walk(path.join(dir, e.name), path.join(rel, e.name));
else if (e.name.endsWith('.md')) out.push(path.join(rel, e.name));
}
})(WF, '');
return out;
}
// Return the full text of every `Agent( … )` call in `content`, tracking string
// state (triple- and single-double-quoted) and paren depth so a prompt body's own
// parens/quotes don't end the call early. Order-independent: a call's params are
// captured whether subagent_type= appears before or after the prompt.
function agentCalls(content) {
const calls = [];
const re = /Agent\(/g;
let m;
while ((m = re.exec(content)) !== null) {
let i = m.index + m[0].length;
let depth = 1;
let tq = false; // inside """ … """
let sq = false; // inside " … "
while (i < content.length && depth > 0) {
if (tq) {
if (content.startsWith('"""', i)) { tq = false; i += 3; continue; }
i++; continue;
}
if (sq) {
if (content[i] === '\\') { i += 2; continue; }
if (content[i] === '"') { sq = false; }
i++; continue;
}
if (content.startsWith('"""', i)) { tq = true; i += 3; continue; }
if (content[i] === '"') { sq = true; i++; continue; }
if (content[i] === '(') { depth++; }
else if (content[i] === ')') { depth--; }
i++;
}
calls.push(content.slice(m.index, i));
re.lastIndex = i; // don't re-scan inside this call
}
return calls;
}
describe('#2072: routed-agent spawns thread the resolved model', () => {
test('discuss-phase-assumptions.md resolves + threads msd-assumptions-analyzer model', () => {
const c = read('discuss-phase-assumptions.md');
assert.match(c, /resolve-model msd-assumptions-analyzer/, 'must resolve the routed model');
assert.match(
c,
/subagent_type="msd-assumptions-analyzer",\s*model="\{ANALYZER_MODEL\}"/,
'spawn must thread the resolved model=',
);
});
test('code-review.md resolves + threads msd-code-reviewer model', () => {
const c = read('code-review.md');
assert.match(c, /resolve-model msd-code-reviewer/);
assert.match(c, /subagent_type="msd-code-reviewer",\s*model="\{REVIEWER_MODEL\}"/);
});
test('code-review-fix.md re-review resolves + threads msd-code-reviewer model', () => {
const c = read('code-review-fix.md');
assert.match(c, /resolve-model msd-code-reviewer/);
assert.match(c, /subagent_type="msd-code-reviewer",\s*model="\{REVIEWER_MODEL\}"/);
});
test('quick.md review step threads msd-code-reviewer own model (not executor_model)', () => {
const c = read('quick.md');
// reviewer_model comes from the init.quick bundle; the spawn must use it.
assert.match(c, /subagent_type="msd-code-reviewer",\s*\n\s*model="\{reviewer_model\}"/);
assert.doesNotMatch(
c,
/subagent_type="msd-code-reviewer",\s*\n\s*model="\{executor_model\}"/,
'reviewer must not reuse the executor model (own model_overrides would be ignored)',
);
});
test('code-review-fix.md threads msd-code-fixer model at both fixer spawns', () => {
const c = read('code-review-fix.md');
assert.match(c, /resolve-model msd-code-fixer/, 'must resolve the fixer model');
const fixerSpawns = c.match(/subagent_type="msd-code-fixer", model="\{FIXER_MODEL\}"/g) || [];
assert.strictEqual(fixerSpawns.length, 2, 'both msd-code-fixer spawns must thread FIXER_MODEL');
});
// Parity guard: EVERY spawn of the fixed routed agents across ALL workflows must
// carry a model= in its Agent(...) call, so a new silently-inert spawn cannot
// regress. Uses a quote/paren-aware scan of the WHOLE Agent(...) call, so it holds
// for single-line and multi-line calls, multiple spawns per file, and either param
// ordering (subagent_type= before OR after the prompt body).
test('no spawn of the fixed routed agents is missing a model= (parity guard)', () => {
const ROUTED = /subagent_type="(msd-code-reviewer|msd-assumptions-analyzer|msd-code-fixer)"/;
const offenders = [];
for (const rel of allWorkflowMd()) {
for (const call of agentCalls(read(rel))) {
const routed = call.match(ROUTED);
if (routed && !/\bmodel\s*=/.test(call)) {
offenders.push(`${rel}: ${routed[1]} spawn missing model=`);
}
}
}
assert.deepEqual(offenders, [], `routed-agent spawn(s) missing model=:\n${offenders.join('\n')}`);
});
// The scanner itself must catch a prompt-first, un-threaded spawn (the exact blind
// spot a naive "params before prompt=" heuristic misses) — otherwise the guard
// above could pass vacuously.
test('parity guard detects a prompt-first spawn that omits model=', () => {
const synthetic = [
'Agent(',
' prompt="""do the thing (with parens) and a " quote""",',
' subagent_type="msd-code-reviewer"',
')',
].join('\n');
const [call] = agentCalls(synthetic);
assert.ok(/subagent_type="msd-code-reviewer"/.test(call), 'scanner must capture the prompt-first subagent_type');
assert.ok(!/\bmodel\s*=/.test(call), 'and correctly see that model= is absent');
// A well-formed prompt-first spawn WITH model= must be accepted.
const ok = agentCalls(synthetic.replace(')', ' model="{reviewer_model}"\n)'))[0];
assert.ok(/\bmodel\s*=/.test(ok));
});
});