Files
msd-core/tests/agent-descriptor-parity.install.test.cjs
Jakub Zych 792139b5ed
Some checks failed
Tests / PR mergeability (push) Successful in 19s
Tests / Base branch health (push) Successful in 10s
Tests / Detect test scope (push) Successful in 17s
Tests / lint-tests (push) Failing after 1m43s
Tests / plugin-validate (push) Successful in 1m7s
Tests / test (ubuntu-latest, 24, shard 1/3) (push) Failing after 18s
Tests / test (ubuntu-latest, 24, shard 2/3) (push) Failing after 19s
Tests / test (ubuntu-latest, 24, shard 3/3) (push) Failing after 19s
Tests / test (ubuntu-latest, 24) (push) Failing after 17s
Tests / test (inert CI) (push) Has been skipped
Tests / QA loop walk (smell ratchet) (push) Failing after 18s
Tests / Coverage gate (merged shards) (push) Has been skipped
Tests / Publish emitted-baseline artifact (push) Has been skipped
Dismiss Unauthorized PR Approvals / dismiss-unauthorized-approval (push) Successful in 8s
Tests / Required tests (push) Has been cancelled
Tests / conformance test (macos-latest, 24) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 1/3) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 2/3) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 3/3) (push) Has been cancelled
chore: sweep Kimi mentions from comments and notes
2026-10-06 20:19:17 +02:00

604 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// allow-test-rule: source-text-is-the-product #2875 — the J-row assertions
// pattern-match rendered agent-.md frontmatter (`effort:`, `model:`,
// disallowedTools, branding text). That frontmatter IS the deployed artifact
// each runtime loads at dispatch time (no typed IR exists between
// applyAgentFrontmatterExtensions/injectEffortFrontmatter and the file a
// runtime reads) — matching CONTRIBUTING.md's `source-text-is-the-product`
// exemption, not a workaroundable "hide the grep in a parser" case.
/**
* agent-descriptor-parity.install.test.cjs — #2875 Part 2 (the agents-bypass
* closure), 50-test-matrix.md sections H, I, J (rows H1-H8, I1-I3, J1-J10).
*
* HONEST BASELINE (rewritten — a prior revision of this file was found to
* test the wrong thing on every axis; see the fixed defects below):
*
* bin/install.js's inline agent-staging loop is GONE (deleted in the same
* commit that made every runtime descriptor-driven for agents). There is no
* second, independently-maintained agent-staging pipeline left in this
* codebase to diff against — an "old pipeline vs new pipeline" comparison is
* therefore IMPOSSIBLE post-deletion, and a prior revision of this file's
* header claiming to compare against "bin/install.js's inline agent-staging
* loop" was false the moment that loop was deleted.
*
* What THIS revision actually proves instead, and how:
*
* 1. The REAL production entry point (`installAgentsKindStandalone`,
* `install-engine.cjs`) is driven directly, against the REAL, BUILT
* `capability-registry.cjs` — no synthetic registry override. Every H
* row therefore byte-compares actual output written by a real
* `capabilities/<runtime>/capability.json` edit, not a hand-rolled
* stand-in for one. A wrong-but-syntactically-valid `converter` name
* landing in a real descriptor changes the ACTUAL side's output and is
* caught (see H8's red-proof, which demonstrates this directly).
*
* 2. `computeExpectedOutput` (the "oracle") independently assembles the
* EXPECTED bytes by calling the individual conversion PRIMITIVES
* directly: `composeWorkflow`, `applyAgentPathRewrites`,
* `processAttribution`, the runtime's converter function (dispatched
* off `EXPECTED_CONVERTER_NAME_BY_RUNTIME` — a hand-verified,
* independent map, NOT read from the descriptor under test),
* `applyAgentFrontmatterExtensions`, `normalizeAgentBodyForRuntime`.
* These primitives are — by construction, not accident —
* single-sourced: there is no live duplicate of `composeWorkflow` or
* `applyAgentPathRewrites` to diff against either, because #2875 Part 2
* collapsed the duplication into these shared functions. Reusing them
* here does not defeat the test: the property under test in every H row
* is "does capability.json's declared `converter` name resolve to the
* CORRECT converter and get invoked in the correct position of the
* pipeline" — which the independent `EXPECTED_CONVERTER_NAME_BY_RUNTIME`
* map exists specifically to keep decoupled from the descriptor.
*
* 3. Both scopes are exercised for every runtime that declares a
* per-scope `agents` kind (claude, codex, opencode × global+local). The prior revision was global-only, which
* is exactly the class of gap that let an agents-drop
* regression reach `next` undetected: cline-local (fixed alongside
* this rewrite — capabilities/cline/capability.json's `local`
* artifactLayout now declares an `agents` kind).
*
* H8 is mandatory, not optional: a parity harness never demonstrated failing
* is decoration. It feeds the oracle a DELIBERATELY WRONG (but real,
* allowlisted) converter name and asserts the comparison goes red against
* the REAL (correctly-configured) production output — proving that if
* capability.json's declared converter ever regressed, this exact harness
* would catch it.
*
* WHAT THIS FILE DOES NOT PROVE: H8's red-proof demonstrates exactly one
* failure class — the oracle and the real descriptor path resolving to a
* DIFFERENT converter for the same runtime. It says nothing about a bug
* INSIDE a shared primitive (`composeWorkflow`, `applyAgentPathRewrites`,
* `processAttribution`, `applyAgentFrontmatterExtensions`,
* `normalizeAgentBodyForRuntime`, or a named converter itself): both the
* oracle (point 2 above) and the real descriptor path call the identical
* function, so a regression inside one of those functions changes BOTH
* sides identically and every H row stays green. That is a deliberate,
* unavoidable consequence of point 2's single-sourcing (there is no second,
* independently-implemented copy of those primitives left to diff against
* post-#2875-Part-2) — this file is a converter-WIRING parity gate, not a
* substitute for direct unit coverage of the primitives themselves (which
* live in their own owning test files, e.g. `runtime-artifact-conversion`'s
* suite).
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup, createTempDir, sandboxHome } = require('./helpers.cjs');
const REPO_ROOT = path.join(__dirname, '..');
const LIB_DIR = path.join(REPO_ROOT, 'msd-core', 'bin', 'lib');
const runtimeArtifactConversion = require(path.join(LIB_DIR, 'runtime-artifact-conversion.cjs'));
const installModelOverrideResolver = require(path.join(LIB_DIR, 'install-model-override-resolver.cjs'));
const installEngine = require(path.join(LIB_DIR, 'install-engine.cjs'));
const capabilityRegistry = require(path.join(LIB_DIR, 'capability-registry.cjs'));
const { composeWorkflow } = require(path.join(LIB_DIR, 'workflow-fragments.cjs'));
const installBin = require(path.join(REPO_ROOT, 'bin', 'install.js'));
// ---------------------------------------------------------------------------
// Fixture builders
// ---------------------------------------------------------------------------
/** Deterministic sample agent sources — NOT the real agents/ tree (the real
* tree's exact roster can change independently of this suite). Covers:
* ~/.claude/ + $HOME/.claude/ (anchored + bare) path forms, a Co-Authored-By
* trailer, and one row-J4 "disallowedTools hit" agent name plus one "miss". */
const SAMPLE_AGENTS = {
'msd-planner.md': [
'---',
'name: msd-planner',
'description: Plans phases for MSD workflows.',
'tools: Read, Write, Edit, Bash',
'---',
'',
'Reads @~/.claude/msd-core/commands/msd/plan-phase.md and $HOME/.claude/CLAUDE.md.',
'Bare forms too: ~/.claude and $HOME/.claude.',
'References Claude Code and CLAUDE.md and .claude/settings.json.',
'',
'Co-Authored-By: Claude <noreply@anthropic.com>',
'',
].join('\n'),
'msd-plan-checker.md': [
'---',
'name: msd-plan-checker',
'description: Checks plans for MSD workflows.',
'tools: Read, Grep, Glob',
'---',
'',
'A read-only checker agent (row J4 "hit" — declared in READONLY_AGENT_DISALLOWED_TOOLS).',
'',
].join('\n'),
};
function buildSourceTree(agentFiles) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-agent-parity-src-'));
const commandsMsd = path.join(root, 'commands', 'msd');
fs.mkdirSync(commandsMsd, { recursive: true });
// Runtime Surface source providers are atomic across every source class a
// layout needs. Keep this marker fixture complete for commands + agents so
// the installer resolver does not correctly fall back to the package tree.
fs.writeFileSync(path.join(commandsMsd, 'fixture-command.md'), '# Fixture command\n');
const agentsDir = path.join(root, 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
for (const [name, content] of Object.entries(agentFiles)) {
fs.writeFileSync(path.join(agentsDir, name), content);
}
return { root, commandsMsd, agentsDir };
}
/** A fresh "install destination" dir with a `.msd-source` marker pointing at
* `commandsMsd` — the same marker findInstallSourceRoot/findAgentsSourceRoot
* read (runtime-artifact-layout.cts). */
function buildTargetDir(commandsMsd) {
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-agent-parity-dest-'));
fs.writeFileSync(path.join(targetDir, '.msd-source'), commandsMsd);
return targetDir;
}
// ---------------------------------------------------------------------------
// Oracle — independently-assembled EXPECTED output (see header doc)
// ---------------------------------------------------------------------------
/** Hand-verified, independent of any capability.json — this is the thing
* every H row's ACTUAL side (a real capability.json) is checked against.
* `null` means converter:null (identity — claude). */
const EXPECTED_CONVERTER_NAME_BY_RUNTIME = {
claude: null,
codex: 'convertClaudeAgentToCodexAgent',
opencode: 'convertClaudeToOpencodeFrontmatter',
};
/** Converter functions callable by name — bin/install.js still owns
* codex/opencode's (never migrated to runtime-artifact-conversion.cjs). */
const NAMED_CONVERTERS = {
convertClaudeAgentToCodexAgent: installBin.convertClaudeAgentToCodexAgent,
convertClaudeToOpencodeFrontmatter: installBin.convertClaudeToOpencodeFrontmatter,
};
/** opencode takes an options bag (`{isAgent, modelOverride}`), resolved
* ONCE per call via the single shared precedence resolver (J5-J8) — every
* other converter here takes only `content`. */
const MODEL_OVERRIDE_CONVERTER_NAMES = new Set(['convertClaudeToOpencodeFrontmatter']);
/**
* Assemble the EXPECTED per-file output for `runtime` from `agentsDir`,
* calling the shared conversion primitives directly (see header doc for why
* this is not circular). `converterNameOverride`, when passed, replaces
* `EXPECTED_CONVERTER_NAME_BY_RUNTIME[runtime]` — used ONLY by H8's
* red-proof to inject a deliberately wrong converter.
*/
function computeExpectedOutput(runtime, agentsDir, ctx, converterNameOverride) {
const converterName = converterNameOverride !== undefined ? converterNameOverride : EXPECTED_CONVERTER_NAME_BY_RUNTIME[runtime];
const { pathPrefix, attribution, targetDir } = ctx;
const out = new Map();
const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
const agentSourcePath = path.join(agentsDir, entry.name);
let content = fs.readFileSync(agentSourcePath, 'utf8');
// Step 0 (#2995): strip msd:section markers — same order the real
// pipeline uses (stageAgentsForRuntimeWithConverter, install-profiles.cts).
content = composeWorkflow(content, { sourcePath: agentSourcePath });
const agentName = runtimeArtifactConversion.deriveAgentName(entry.name);
// Step 1: path rewrites
content = runtimeArtifactConversion.applyAgentPathRewrites(content, runtime, pathPrefix);
// Step 2: attribution
content = runtimeArtifactConversion.processAttribution(content, attribution);
// Step 3: converter — dispatched off the INDEPENDENT expected-name map,
// never off the descriptor under test.
if (converterName) {
const fn = NAMED_CONVERTERS[converterName];
assert.ok(typeof fn === 'function', `oracle: no converter function registered for "${converterName}"`);
if (MODEL_OVERRIDE_CONVERTER_NAMES.has(converterName)) {
const modelOverride = installModelOverrideResolver.resolveAgentModelOverride(
agentName,
installModelOverrideResolver.readMsdEffectiveModelOverrides(targetDir),
installModelOverrideResolver.readMsdRuntimeProfileResolver(targetDir),
);
content = fn(content, { isAgent: true, modelOverride });
} else {
content = fn(content);
}
}
// converter:null (claude) — content unchanged by this step.
// Step 4: frontmatter extensions (effort/disallowedTools)
content = runtimeArtifactConversion.applyAgentFrontmatterExtensions(content, { runtime, agentName, targetDir });
// Step 5: normalize colon->hyphen refs
content = runtimeArtifactConversion.normalizeAgentBodyForRuntime(
content,
runtime,
runtimeArtifactConversion.readMsdCommandNames(),
);
out.set(entry.name, content);
}
return out;
}
// ---------------------------------------------------------------------------
// Real descriptor path — the REAL production entry point, REAL registry
// ---------------------------------------------------------------------------
/**
* Drive the ACTUAL production entry point (`installAgentsKindStandalone`)
* against the REAL, built `capability-registry.cjs` — no override. This is
* exactly what `bin/install.js`'s `install()` calls for every runtime whose
* layout is not otherwise reached by the generic `installRuntimeArtifacts`
* loop (and, for the runtimes reached by that loop, produces identical
* output to it — both route through the SAME `convertedAgentsKind` /
* `_resolveNamedConverter` dispatch in runtime-artifact-layout.cts).
*/
function runRealDescriptorPath(runtime, scope, targetDir, ctx) {
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
const result = installEngine.installAgentsKindStandalone(
runtime,
targetDir,
scope,
resolvedProfile,
ctx.pathPrefix,
() => ctx.attribution,
);
const out = new Map();
if (!result) return out;
for (const entry of fs.readdirSync(result.destDir, { withFileTypes: true })) {
if (entry.isFile()) out.set(entry.name, fs.readFileSync(path.join(result.destDir, entry.name), 'utf8'));
}
return out;
}
// ---------------------------------------------------------------------------
// Comparison helper
// ---------------------------------------------------------------------------
/** Every (runtime, scope) pair the REAL capabilities/<runtime>/capability.json
* today declares an `agents` kind for (measured 2026-08-17). K1 below is the
* machine-checked guarantee that this list cannot silently go stale — it
* sweeps the real registry and fails if a declarant is missing here. */
const RUNTIME_SCOPE_PAIRS = [
['claude', 'global'], ['claude', 'local'],
['codex', 'global'], ['codex', 'local'],
['opencode', 'global'], ['opencode', 'local'],
];
/** Model override literal shared by J8's two `_stageWithModelOverride` calls. */
const J8_OVERRIDE_MODEL = 'shared/explicit-model';
/**
* Standalone helper (module scope, no test-context access — the
* CONTRIBUTING.md "Never use try/finally inside test bodies" exemption) for
* J8: stage a single-agent source tree with a real `.planning/config.json`
* model_overrides block for `runtime` through the real descriptor path.
*/
function _stageWithModelOverride(runtime, overrideModel) {
const agentFiles = { 'msd-planner.md': SAMPLE_AGENTS['msd-planner.md'] };
const { commandsMsd, root } = buildSourceTree(agentFiles);
const targetDir = buildTargetDir(commandsMsd);
fs.mkdirSync(path.join(targetDir, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(targetDir, '.planning', 'config.json'),
JSON.stringify({ model_overrides: { 'msd-planner': overrideModel } }),
);
try {
const ctx = { pathPrefix: `${targetDir}/`, attribution: undefined, targetDir };
return runRealDescriptorPath(runtime, 'global', targetDir, ctx);
} finally {
cleanup(root);
cleanup(targetDir);
}
}
function comparePipelines(runtime, scope, converterNameOverride) {
const { commandsMsd, agentsDir, root } = buildSourceTree(SAMPLE_AGENTS);
const targetDir = buildTargetDir(commandsMsd);
try {
const ctx = { pathPrefix: `${targetDir}/`, attribution: undefined, targetDir };
const expected = computeExpectedOutput(runtime, agentsDir, ctx, converterNameOverride);
const actual = runRealDescriptorPath(runtime, scope, targetDir, ctx);
return { expected, actual };
} finally {
cleanup(root);
cleanup(targetDir);
}
}
/** Asserts H1-H6 + H7 in one shot: same filenames (Set equality, order-free)
* AND byte-identical content per filename. */
function assertMapsIdentical(expected, actual) {
assert.deepEqual(
[...expected.keys()].sort(),
[...actual.keys()].sort(),
'filenames diverged between the oracle and the real descriptor path (row H7)',
);
for (const [name, expectedContent] of expected) {
assert.equal(
actual.get(name),
expectedContent,
`content diverged for ${name} between the oracle and the real descriptor path`,
);
}
}
// ---------------------------------------------------------------------------
// H rows — the parity gate, one row per (runtime, scope)
// ---------------------------------------------------------------------------
for (const [runtime, scope] of RUNTIME_SCOPE_PAIRS) {
test(`agent-descriptor-parity: H row — ${runtime} (${scope}) real descriptor output matches the independent oracle`, () => {
const { expected, actual } = comparePipelines(runtime, scope);
assert.ok(expected.size > 0, 'fixture produced no oracle output — test is vacuous');
assertMapsIdentical(expected, actual);
});
}
// ---------------------------------------------------------------------------
// H7 — the harness compares filenames, not only content (meta)
// ---------------------------------------------------------------------------
test('agent-descriptor-parity: H7 — a filename-only divergence fails the harness', (t) => {
const { commandsMsd, agentsDir, root } = buildSourceTree(SAMPLE_AGENTS);
const targetDir = buildTargetDir(commandsMsd);
t.after(() => {
cleanup(root);
cleanup(targetDir);
});
const ctx = { pathPrefix: `${targetDir}/`, attribution: undefined, targetDir };
const expected = computeExpectedOutput('claude', agentsDir, ctx);
const actual = runRealDescriptorPath('claude', 'global', targetDir, ctx);
// Deliberately rename one actual-side entry — same bytes, different name.
const [firstName, firstContent] = [...actual.entries()][0];
actual.delete(firstName);
actual.set(`RENAMED-${firstName}`, firstContent);
assert.throws(
() => assertMapsIdentical(expected, actual),
/filenames diverged/,
'a renamed output file must fail the harness',
);
});
// ---------------------------------------------------------------------------
// H8 — the harness can actually FAIL (mandatory, not optional)
// ---------------------------------------------------------------------------
test('agent-descriptor-parity: H8 — a deliberately-wrong (but real, allowlisted) expected converter turns the harness RED', (t) => {
// OpenCode's REAL capability.json declares convertClaudeToOpencodeFrontmatter.
// Feed the ORACLE a different, real, allowlisted converter name
// (convertClaudeAgentToCodexAgent) instead — simulating exactly the failure
// mode row H exists to catch: capability.json's declared converter silently
// diverging from the correct one. The REAL descriptor path is untouched and
// still uses opencode's real (correct) converter, so this proves: IF
// capability.json ever regressed to the wrong name, THIS harness's H row
// for opencode would go red exactly like this.
const { commandsMsd, agentsDir, root } = buildSourceTree(SAMPLE_AGENTS);
const targetDir = buildTargetDir(commandsMsd);
t.after(() => {
cleanup(root);
cleanup(targetDir);
});
const ctx = { pathPrefix: `${targetDir}/`, attribution: undefined, targetDir };
const wrongExpected = computeExpectedOutput('opencode', agentsDir, ctx, 'convertClaudeAgentToCodexAgent');
const realActual = runRealDescriptorPath('opencode', 'global', targetDir, ctx);
let threw = false;
let observedDiff = null;
try {
assertMapsIdentical(wrongExpected, realActual);
} catch (err) {
threw = true;
observedDiff = err.message;
}
assert.equal(threw, true, 'H8 FAILED: the harness did not go red for a deliberately-wrong expected converter');
assert.match(observedDiff, /content diverged for msd-(planner|plan-checker)\.md/, 'expected the content-diverged assertion to name the mismatched file');
// Verbatim red-proof output for the record (see CHANGES report):
console.log(`H8 red-proof observed: ${observedDiff}`);
});
// ---------------------------------------------------------------------------
// I1-I3 — per-agent resolution context
// ---------------------------------------------------------------------------
test('agent-descriptor-parity: I3 — deriveAgentName matches the pipeline exactly, including a no-.md-suffix boundary', () => {
assert.equal(runtimeArtifactConversion.deriveAgentName('msd-planner.md'), 'msd-planner');
// Boundary: a filename with no trailing .md is returned unchanged (the
// regex has nothing to match) — matches `entry.name.replace(/\.md$/, '')`.
assert.equal(runtimeArtifactConversion.deriveAgentName('msd-planner'), 'msd-planner');
assert.equal(runtimeArtifactConversion.deriveAgentName('msd-planner.MD'), 'msd-planner.MD');
});
test('agent-descriptor-parity: I2 — real descriptor path with no agentCtx is unaffected (converter-only)', (t) => {
const { commandsMsd, agentsDir, root } = buildSourceTree(SAMPLE_AGENTS);
const targetDir = buildTargetDir(commandsMsd);
t.after(() => {
cleanup(root);
cleanup(targetDir);
});
const runtimeArtifactLayout = require(path.join(LIB_DIR, 'runtime-artifact-layout.cjs'));
const realLayout = runtimeArtifactLayout.resolveRuntimeArtifactLayout('claude', targetDir, 'global');
const agentsKindEntry = realLayout.kinds.find((k) => k.kind === 'agents');
const stagedDir = agentsKindEntry.stage({ name: 'full', skills: '*', agents: new Set() }); // no agentCtx
t.after(() => cleanup(stagedDir));
const planner = fs.readFileSync(path.join(stagedDir, 'msd-planner.md'), 'utf8');
const original = fs.readFileSync(path.join(agentsDir, 'msd-planner.md'), 'utf8');
assert.equal(planner, original, 'no agentCtx must leave content byte-identical to source (converter:null == identity)');
});
// ---------------------------------------------------------------------------
// J1-J4 — frontmatter extensions
// ---------------------------------------------------------------------------
test('agent-descriptor-parity: J1 — claude effort is injected via applyAgentFrontmatterExtensions', () => {
const content = '---\nname: msd-planner\ndescription: x\n---\n\nBody.\n';
const viaShared = runtimeArtifactConversion.applyAgentFrontmatterExtensions(content, { runtime: 'claude', agentName: 'msd-planner', targetDir: null });
assert.match(viaShared, /^effort: /m, 'expected an effort: key to be injected for claude');
});
test('agent-descriptor-parity: J2 — effort resolving to inherit writes NO effort: key at all, exercised via the REAL guard in applyAgentFrontmatterExtensions (#3533 trap row)', (t) => {
// #2875 Part 2 defect fix: a prior revision of this row asserted the DUMB
// half (injectEffortFrontmatter DOES emit the literal 'inherit' if called
// with it) and never called applyAgentFrontmatterExtensions at all — so
// deleting the `universalEffort !== 'inherit'` guard at
// runtime-artifact-conversion.cts:3504 left this row green. This revision
// writes a REAL .planning/config.json under targetDir (readMsdEffectiveEffortConfig
// walks up from targetDir looking for it — install-effort-resolver.cts)
// and calls the REAL applyAgentFrontmatterExtensions end to end, so removing
// that guard makes THIS assertion fail (verified: red with the guard
// removed, green with it restored — see CHANGES report).
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-agent-parity-j2-'));
t.after(() => cleanup(targetDir));
fs.mkdirSync(path.join(targetDir, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(targetDir, '.planning', 'config.json'),
JSON.stringify({ effort: { agent_overrides: { 'msd-inherit-agent': 'inherit' } } }),
);
const content = '---\nname: msd-inherit-agent\ndescription: x\n---\n\nBody.\n';
const out = runtimeArtifactConversion.applyAgentFrontmatterExtensions(content, { runtime: 'claude', agentName: 'msd-inherit-agent', targetDir });
assert.doesNotMatch(out, /^effort:/m, 'an agent resolving to "inherit" must get no effort: key at all — not even effort: inherit');
// Sanity: injectEffortFrontmatter itself is dumb and WOULD write the
// literal if called with it — the guard is applyAgentFrontmatterExtensions
// never calling it for 'inherit', which is exactly what the assertion above proves.
assert.match(runtimeArtifactConversion.injectEffortFrontmatter(content, 'inherit'), /^effort: inherit$/m);
});
test('agent-descriptor-parity: J3 — a runtime NOT declaring agentFrontmatterExtensions gets nothing injected', () => {
const content = '---\nname: msd-plan-checker\ndescription: x\n---\n\nBody.\n';
const out = runtimeArtifactConversion.applyAgentFrontmatterExtensions(content, { runtime: 'opencode', agentName: 'msd-plan-checker', targetDir: null });
assert.equal(out, content, 'opencode declares no agentFrontmatterExtensions — output must be byte-identical to input');
});
test('agent-descriptor-parity: J4 — disallowedTools injected only on a READONLY_AGENT_DISALLOWED_TOOLS hit', () => {
const content = '---\nname: msd-plan-checker\ndescription: x\n---\n\nBody.\n';
const hit = runtimeArtifactConversion.applyAgentFrontmatterExtensions(content, { runtime: 'claude', agentName: 'msd-plan-checker', targetDir: null });
assert.match(hit, /^disallowedTools: /m, 'msd-plan-checker is a declared read-only agent — expected a disallowedTools hit');
const missContent = '---\nname: msd-not-a-readonly-agent\ndescription: x\n---\n\nBody.\n';
const miss = runtimeArtifactConversion.applyAgentFrontmatterExtensions(missContent, { runtime: 'claude', agentName: 'msd-not-a-readonly-agent', targetDir: null });
assert.doesNotMatch(miss, /^disallowedTools:/m, 'an agent absent from READONLY_AGENT_DISALLOWED_TOOLS must get no disallowedTools key');
});
// ---------------------------------------------------------------------------
// J5-J8 — model-override resolution (opencode), single-sourced
// ---------------------------------------------------------------------------
test('agent-descriptor-parity: J5 — explicit model_overrides[agent] wins (highest precedence)', () => {
const modelOverrides = { 'msd-planner': 'anthropic/explicit-model' };
const runtimeResolver = { resolve: () => ({ model: 'anthropic/tier-model' }) }; // would win if precedence were wrong
const result = installModelOverrideResolver.resolveAgentModelOverride('msd-planner', modelOverrides, runtimeResolver);
assert.equal(result, 'anthropic/explicit-model');
});
test('agent-descriptor-parity: J6 — falls back to the runtime tier resolver when no explicit override exists', () => {
const runtimeResolver = { resolve: (agentName) => (agentName === 'msd-planner' ? { model: 'anthropic/tier-model' } : null) };
const result = installModelOverrideResolver.resolveAgentModelOverride('msd-planner', null, runtimeResolver);
assert.equal(result, 'anthropic/tier-model');
});
test('agent-descriptor-parity: J7 — neither configured resolves to null (omit), never "" or the string "null"', () => {
const result = installModelOverrideResolver.resolveAgentModelOverride('msd-planner', null, null);
assert.equal(result, null);
assert.notEqual(result, '');
const contentWithoutModelOverride = installBin.convertClaudeToOpencodeFrontmatter(
'---\nname: msd-planner\ndescription: x\ntools: Read\n---\n\nBody.\n',
{ isAgent: true, modelOverride: result },
);
assert.doesNotMatch(contentWithoutModelOverride, /^model:/m, 'an omitted override must not appear as a model: key at all');
});
test('agent-descriptor-parity: J8 — opencode resolves model overrides through the REAL descriptor path (installAgentsKindStandalone)', () => {
// Drives the REAL production entry point with a REAL .planning/config.json
// model_overrides block and asserts the staged output carries the resolved
// model — which only happens if the runtime is genuinely wired to the
// shared installModelOverrideResolver.resolveAgentModelOverride.
const opencodeOut = _stageWithModelOverride('opencode', J8_OVERRIDE_MODEL);
// J8_OVERRIDE_MODEL is a fixed constant: assert the exact expected
// frontmatter line, matched line-wise, rather than building a RegExp from
// an interpolated value (CodeQL js/incomplete-sanitization).
const expectedModelLine = `model: ${J8_OVERRIDE_MODEL}`;
assert.ok(
opencodeOut.get('msd-planner.md').split('\n').some((line) => line.trim() === expectedModelLine),
'opencode must apply the shared model override via the real descriptor path',
);
});
// ---------------------------------------------------------------------------
// K1 — migration completeness: every registry runtime with an `agents` kind
// is reachable from the REAL production entry point, not the (now-deleted)
// inline loop.
// ---------------------------------------------------------------------------
/**
* bin/install.js's inline agent-staging loop and its `_DESCRIPTOR_AGENTS_RUNTIMES`
* gate were DELETED in #2875 Part 2 Task C — there is no longer a symbol to
* assert absent (a source-text check would violate `local/no-source-grep` and
* would prove nothing about runtime behavior anyway, per CLAUDE.md's
* "Behavioral tests are required"). Row K1 is instead proven the only way
* that is actually meaningful once the code is gone: for EVERY `role:
* "runtime"` capability in the REAL capability-registry that declares an
* `agents` kind (either scope), the REAL production entry point
* (`installRuntimeArtifacts` — which internally routes combinedFamilyInstall
* runtimes like kilo/opencode through `installOpencodeFamilyAgents`, #2875
* Part 2 Task A) actually materializes agents/ on disk. If any runtime were
* still silently depending on the deleted inline loop, this call would write
* nothing to agents/ for it (the deleted code was the ONLY thing that used to
* write it for the seven runtimes migrated in this change) and the assertion
* below would fail.
*/
test('agent-descriptor-parity: K1 — every registry runtime declaring an agents kind is reachable from installRuntimeArtifacts (the inline loop is gone)', (t) => {
const runtimesWithAgentsKind = Object.entries(capabilityRegistry.runtimes || {})
.filter(([, cap]) => {
const layout = cap.runtime && cap.runtime.artifactLayout;
if (!layout) return false;
const entries = [...(layout.global || []), ...(layout.local || [])];
return entries.some((e) => e.kind === 'agents');
})
.map(([id]) => id);
assert.ok(runtimesWithAgentsKind.length >= 3, 'sanity: expected at least the kept runtimes to declare an agents kind');
assert.ok(runtimesWithAgentsKind.includes('opencode'), 'opencode must be covered here');
// #3712: this loop reaches `codex`, whose global skills kind declares a `home`
// override (`.agents`) resolved from os.homedir() rather than from targetDir.
// Sandboxing targetDir alone does NOT contain it — before this line the call
// below pruned every msd-* skill from the developer's REAL ~/.agents/skills
// (71 -> 0) while the suite still exited 0. Sandbox HOME for the whole loop so
// codex's skills land in <homeDir>/.agents/skills inside a temp dir we clean.
const homeDir = createTempDir('msd-adp-home-');
t.after(() => cleanup(homeDir));
sandboxHome(t, homeDir);
for (const runtime of runtimesWithAgentsKind) {
const { commandsMsd, root } = buildSourceTree(SAMPLE_AGENTS);
const targetDir = buildTargetDir(commandsMsd);
t.after(() => {
cleanup(root);
cleanup(targetDir);
});
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
const result = installEngine.installRuntimeArtifacts(runtime, targetDir, 'global', resolvedProfile, () => undefined, undefined);
const agentsKindEntry = result.kinds.find((k) => k.kind === 'agents');
assert.ok(agentsKindEntry, `${runtime}: installRuntimeArtifacts reported no agents kind in the executed plan — it did not go through the descriptor path`);
const writtenFiles = fs.readdirSync(agentsKindEntry.destDir).filter((f) => f.endsWith('.md'));
assert.ok(writtenFiles.length > 0, `${runtime}: agents kind reported but nothing was actually written to ${agentsKindEntry.destDir}`);
}
});