Files
msd-core/tests/runtime-converters.test.cjs
Jakub Zych 6cfa0c55d2 refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity
Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes,
cline, codebuddy and pi end to end: capability descriptors, installer branches
and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters,
hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi
migrations, Kimi payload normalization in the hook guards, dead hostBehaviors
vocabulary, launcher home probes, fixtures, runtime-specific tests and the
prose that presented them as supported.

Installer output for the six kept runtimes is byte-identical to before the
prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and
read-injection-scanner are left in place pending a decision.
2026-10-06 20:02:40 +02:00

2465 lines
119 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.
/**
* Runtime Converter Tests — OpenCode
*
* Tests for small runtime-specific conversion functions from install.js.
* Larger runtime test suites (Codex, Antigravity) have their own files.
*
* OpenCode: flat-runtime frontmatter converter (agent + command modes)
* model: inherit is NOT added (runtime uses its configured default model)
* but mode: subagent IS added (required by the runtime's agents).
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
process.env.MSD_TEST_MODE = '1';
const {
convertClaudeToOpencodeFrontmatter,
convertClaudeAgentToAntigravityAgent,
neutralizeAgentReferences,
} = require('../bin/install.js');
const {
convertClaudeCommandToOpencodeSkill,
} = require('../msd-core/bin/lib/install-engine.cjs');
// Sample Claude agent frontmatter (matches actual MSD agent format)
const SAMPLE_AGENT = `---
name: msd-executor
description: Executes MSD plans with atomic commits
tools: Read, Write, Edit, Bash, Grep, Glob
color: yellow
skills:
- msd-executor-workflow
# hooks:
# PostToolUse:
# - matcher: "Write|Edit"
# hooks:
# - type: command
# command: "npx eslint --fix $FILE 2>/dev/null || true"
---
<role>
You are a MSD plan executor.
</role>`;
// Sample Claude command frontmatter (for comparison — commands work differently)
const SAMPLE_COMMAND = `---
name: msd-execute-phase
description: Execute all plans in a phase
allowed-tools:
- Read
- Write
- Bash
---
Execute the phase plan.`;
const flatRuntimeSuites = [
{
label: 'OpenCode',
convert: convertClaudeToOpencodeFrontmatter,
configDir: '.config/opencode',
},
];
describe('#4482: OpenCode conversion strips runtime notes declared for another runtime', () => {
const convert = (body) => liveConversion.convertClaudeToOpencodeFrontmatter(
['---', 'name: msd-test', 'description: test', '---', body].join('\n'),
);
test('removes a note declared for another runtime without leaving an empty wrapper', () => {
const out = convert([
'<runtime_note runtime="claude">',
'**Claude Code:** Use `AskUserQuestion` for interactive prompts.',
'</runtime_note>',
'',
'<objective>Keep me.</objective>',
].join('\n'));
assert.ok(!out.includes('interactive prompts'));
assert.ok(!out.includes('<runtime_note'));
assert.ok(out.includes('<objective>Keep me.</objective>'));
});
test('leaves unrelated runtime notes intact', () => {
const note = '<runtime_note>\n**OpenCode:** Keep this runtime-specific guidance.\n</runtime_note>';
assert.ok(convert(note).includes(note));
});
test('the shared audience filter drops a declared-audience note for every other runtime and preserves it for its audience', () => {
const note = '<runtime_note runtime="claude">\n**Claude Code:** Only Claude sees this.\n</runtime_note>';
const runtimes = ['antigravity', 'codex', 'cursor', 'opencode', 'zcode'];
for (const runtime of runtimes) {
const out = liveConversion.filterRuntimeNotesForTarget(note, runtime);
assert.ok(!out.includes('Only Claude sees this'), `${runtime} must not receive the Claude-declared note`);
}
assert.ok(liveConversion.filterRuntimeNotesForTarget(note, 'claude').includes('Only Claude sees this'));
});
test('a real OpenCode install filters mvp-phase workflow assets too', (t) => {
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
const { cleanup } = require('./helpers.cjs');
const { configDir, root } = runMinimalInstall({ runtime: 'opencode', scope: 'global' });
t.after(() => cleanup(root));
const installed = fs.readFileSync(path.join(configDir, 'msd-core', 'workflows', 'mvp-phase.md'), 'utf8');
assert.ok(!installed.includes('vscode_askquestions'));
assert.ok(installed.includes('TEXT_MODE fallback'));
assert.ok(installed.includes('<runtime_note>'));
});
});
for (const { label, convert, configDir } of flatRuntimeSuites) {
describe(`${label} agent conversion (isAgent: true)`, () => {
test('keeps name: field for agents', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(frontmatter.includes('name: msd-executor'), 'name: should be preserved for agents');
});
test('does not add model: inherit', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added');
});
test('adds mode: subagent', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(frontmatter.includes('mode: subagent'), 'mode: subagent should be added');
});
test('strips tools: field', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('tools:'), 'tools: should be stripped for agents');
assert.ok(!frontmatter.includes('read: true'), 'tools object should not be generated');
assert.ok(!frontmatter.includes('permission:'), 'OpenCode agents should not emit permission block');
});
test('strips skills: array', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('skills:'), 'skills: should be stripped');
assert.ok(!frontmatter.includes('msd-executor-workflow'), 'skill entries should be stripped');
});
test('strips color: field', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('color:'), 'color: should be stripped for agents');
});
test('strips commented hooks block', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('# hooks:'), 'commented hooks should be stripped');
assert.ok(!frontmatter.includes('PostToolUse'), 'hook content should be stripped');
});
test('keeps description: field', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(frontmatter.includes('description: Executes MSD plans'), 'description should be kept');
});
test('preserves body content', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
assert.ok(result.includes('<role>'), 'body should be preserved');
assert.ok(result.includes('You are a MSD plan executor.'), 'body content should be intact');
});
test('applies body text replacements', () => {
const agentWithClaudePaths = `---
name: test-agent
description: Test
tools: Read
---
Read ~/.claude/agent-memory/ for context.
Use $HOME/.claude/skills/ for reference.
Check .claude/skills/ and .claude/agents/ locally.
Use ./.claude/hooks/msd-statusline.js during local testing.
Fallback skills live in .agents/skills/.`;
const result = convert(agentWithClaudePaths, { isAgent: true });
assert.ok(result.includes(`~/${configDir}/agent-memory/`), '~/.claude should be replaced');
assert.ok(result.includes(`$HOME/${configDir}/skills/`), '$HOME/.claude should be replaced');
});
});
describe(`${label} command conversion (isAgent: false, default)`, () => {
test('strips name: field for commands', () => {
const result = convert(SAMPLE_COMMAND);
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('name:'), 'name: should be stripped for commands');
});
test('does not add model: or mode: for commands', () => {
const result = convert(SAMPLE_COMMAND);
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('model:'), 'model: should not be added for commands');
assert.ok(!frontmatter.includes('mode:'), 'mode: should not be added for commands');
});
test('keeps description: for commands', () => {
const result = convert(SAMPLE_COMMAND);
const frontmatter = result.split('---')[1];
assert.ok(frontmatter.includes('description:'), 'description should be kept');
});
});
// ─── #2256: model_overrides support for OpenCode agents ────────────────────
if (label === 'OpenCode') {
describe('OpenCode agent model override (modelOverride option) (#2256)', () => {
test('adds model: field when modelOverride is provided', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true, modelOverride: 'gpt-5.3-codex' });
const frontmatter = result.split('---')[1];
assert.ok(frontmatter.includes('model: gpt-5.3-codex'), 'model: field must be added with override value');
});
test('does not add model: field when modelOverride is null', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true, modelOverride: null });
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('model:'), 'model: field must be absent when no override');
});
test('does not add model: field when modelOverride is omitted', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('model:'), 'model: field must be absent when option omitted');
});
test('model: field appears after mode: subagent', () => {
const result = convert(SAMPLE_AGENT, { isAgent: true, modelOverride: 'o4-mini' });
const frontmatter = result.split('---')[1];
const modeIdx = frontmatter.indexOf('mode: subagent');
const modelIdx = frontmatter.indexOf('model: o4-mini');
assert.ok(modeIdx !== -1, 'mode: subagent must be present');
assert.ok(modelIdx !== -1, 'model: field must be present');
assert.ok(modelIdx > modeIdx, 'model: must appear after mode: subagent');
});
test('model override does not affect command conversion', () => {
// modelOverride has no effect when isAgent is false (commands)
const result = convert(SAMPLE_COMMAND, { modelOverride: 'gpt-5.4' });
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('model:'), 'model: must not appear in command output');
});
});
}
}
describe('convertClaudeToOpencodeFrontmatter output parity: bin/install.js vs runtime-artifact-conversion.cjs (#4482)', () => {
const { convertClaudeToOpencodeFrontmatter: convertViaConversionModule } =
require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
const input = `${SAMPLE_COMMAND}\n\n<runtime_note runtime="claude">\n**Claude Code:** Claude-only guidance.\n</runtime_note>`;
test('published and module converters filter runtime notes identically', () => {
assert.equal(
convertClaudeToOpencodeFrontmatter(input),
convertViaConversionModule(input),
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Antigravity agent conversion — shared Gemini-backend tool mapping (#1394 / #1928)
// ─────────────────────────────────────────────────────────────────────────────
//
// The gemini-RUNTIME's own top-level converter (convertClaudeToGeminiAgent) and
// its dedicated test coverage were removed with the gemini runtime (#1928,
// Google sunset Gemini CLI 2026-06-18). convertAntigravityToolName and
// claudeToAntigravityTools STAY — they are shared infra reused by Antigravity
// (which runs on the same backend tool-name vocabulary), so the
// Antigravity-facing regression coverage below is retained unchanged.
describe('#1394 regression: excludes Skill/SlashCommand from Antigravity frontmatter', () => {
// Skill/SlashCommand are Claude-only tools with no Gemini-backend built-in
// equivalent. Without explicit exclusion they hit the lowercase fallback and
// emit an invalid 'skill'/'slashcommand' tool name, which fails frontmatter
// validation (tools.N: Invalid tool name) and aborts the entire agent load.
// Antigravity reuses convertAntigravityToolName (it runs on the Gemini backend),
// so the exclusion intentionally applies there too. Antigravity surfaces MSD
// skills through the skill surface (SKILL.md), not the agent tools: allowlist,
// so dropping the invalid 'skill' tool name does not remove skill access —
// this locks that cross-runtime behavior (criterion 4).
test('Antigravity conversion also excludes Skill/SlashCommand (shared Gemini backend)', () => {
const input = `---
name: msd-planner
description: Creates executable phase plans.
tools: Read, Write, Bash, Skill, WebFetch, SlashCommand
---
<role>Plan the phase.</role>`;
const result = convertClaudeAgentToAntigravityAgent(input);
const toolsItems = result.split('\n').filter(l => l.startsWith('- ')).map(l => l.slice(2));
assert.ok(toolsItems.includes('view_file'), 'maps Read -> view_file (#4705 native name)');
assert.ok(toolsItems.includes('web_fetch'), 'maps WebFetch -> web_fetch');
assert.ok(!toolsItems.some(t => /\bskill\b/.test(t)), 'no invalid skill tool in Antigravity frontmatter');
assert.ok(!toolsItems.some(t => /\bslashcommand\b/.test(t)), 'no invalid slashcommand tool in Antigravity frontmatter');
});
});
// ─── neutralizeAgentReferences (#766) ─────────────────────────────────────────
describe('neutralizeAgentReferences', () => {
test('replaces standalone Claude with "the agent"', () => {
const input = 'Claude handles these decisions. Claude should read the file.';
const result = neutralizeAgentReferences(input, 'AGENTS.md');
assert.ok(!result.includes('Claude handles'), 'standalone Claude replaced');
assert.ok(result.includes('the agent handles'), 'replaced with "the agent"');
});
test('preserves Claude Code (product name)', () => {
const input = 'This is a Claude Code bug. Use Claude Code settings.';
const result = neutralizeAgentReferences(input, 'AGENTS.md');
assert.ok(result.includes('Claude Code bug'), 'Claude Code preserved');
assert.ok(result.includes('Claude Code settings'), 'Claude Code preserved');
});
test('preserves Claude model names', () => {
const input = 'Use Claude Opus for planning. Claude Sonnet for execution. Claude Haiku for research.';
const result = neutralizeAgentReferences(input, 'AGENTS.md');
assert.ok(result.includes('Claude Opus'), 'Opus preserved');
assert.ok(result.includes('Claude Sonnet'), 'Sonnet preserved');
assert.ok(result.includes('Claude Haiku'), 'Haiku preserved');
});
test('replaces CLAUDE.md with runtime instruction file', () => {
const input = 'Read CLAUDE.md for project instructions. Check ./CLAUDE.md if exists.';
const result = neutralizeAgentReferences(input, 'AGENTS.md');
assert.ok(result.includes('AGENTS.md'), 'CLAUDE.md -> AGENTS.md');
assert.ok(!result.includes('CLAUDE.md'), 'no CLAUDE.md remains');
});
test('uses different instruction file per runtime', () => {
const input = 'Read CLAUDE.md for instructions.';
assert.ok(neutralizeAgentReferences(input, 'GEMINI.md').includes('GEMINI.md'));
assert.ok(neutralizeAgentReferences(input, 'copilot-instructions.md').includes('copilot-instructions.md'));
assert.ok(neutralizeAgentReferences(input, 'AGENTS.md').includes('AGENTS.md'));
});
test('removes AGENTS.md load-blocking instruction', () => {
const input = 'Do NOT load full `AGENTS.md` files — they contain agent definitions.';
const result = neutralizeAgentReferences(input, 'AGENTS.md');
assert.ok(!result.includes('Do NOT load full'), 'blocking instruction removed');
});
test('preserves claude- prefixes (CSS classes, package names)', () => {
const input = 'The claude-ctx session and claude-code package.';
const result = neutralizeAgentReferences(input, 'AGENTS.md');
assert.ok(result.includes('claude-ctx'), 'claude- prefix preserved');
assert.ok(result.includes('claude-code'), 'claude-code preserved');
});
});
// ─── OpenCode skill converter (SKILL.md) — #784 ──────────────────────────────
const SKILL_SAMPLE_COMMAND = `---
description: Show available MSD commands and usage guide
argument-hint: "[topic]"
allowed-tools:
- Read
- Bash
---
Run \`/msd:help\` to see the guide. AskUserQuestion when unsure.
`;
const SKILL_BETA_COMMAND = `---
description: "[BETA] Offload plan phase to the cloud and import back."
---
Body for /msd:ultraplan-phase.
`;
describe('convertClaudeCommandToOpencodeSkill (#784)', () => {
const cases = [
{ label: 'opencode', convert: convertClaudeCommandToOpencodeSkill },
];
for (const { label, convert } of cases) {
describe(`${label} skill conversion`, () => {
test('emits SKILL.md frontmatter with name matching the skill dir', () => {
const out = convert(SKILL_SAMPLE_COMMAND, 'msd-help');
assert.ok(out.startsWith('---\n'), 'opens with frontmatter');
assert.match(out, /^name: msd-help$/m, 'name equals the skill name');
});
test('preserves the description from the source command', () => {
const out = convert(SKILL_SAMPLE_COMMAND, 'msd-help');
assert.match(out, /^description: "Show available MSD commands and usage guide"$/m);
});
test('drops the command tools/permission block (skills inherit perms)', () => {
const out = convert(SKILL_SAMPLE_COMMAND, 'msd-help');
const fmEnd = out.indexOf('\n---', 4);
const fm = out.slice(0, fmEnd);
assert.ok(!/tools:/.test(fm), 'no tools block in skill frontmatter');
assert.ok(!/permission:/.test(fm), 'no permission block in skill frontmatter');
});
test('rewrites /msd: colon refs to hyphen form in the body', () => {
const out = convert(SKILL_SAMPLE_COMMAND, 'msd-help');
assert.ok(!/\/msd:/.test(out), 'no /msd: colon refs remain');
assert.match(out, /\/msd-help/, 'colon ref rewritten to hyphen form');
});
test('quotes descriptions with leading YAML flow indicators ([BETA])', () => {
const out = convert(SKILL_BETA_COMMAND, 'msd-ultraplan-phase');
assert.match(out, /^description: "\[BETA\] /m, 'leading [BETA] safely quoted');
});
test('falls back to a synthetic description when none present', () => {
const out = convert('Body only, no frontmatter.', 'msd-mystery');
assert.match(out, /^name: msd-mystery$/m);
assert.match(out, /^description: "Run MSD workflow msd-mystery\."$/m);
});
});
}
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/feat-1173-agent-converters-descriptor.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:feat-1173-agent-converters-descriptor (consolidation epic #1969 B3 #1972)", () => {
'use strict';
/**
* feat-1173: Descriptor-driven agent converter wiring.
*
* Verifies that the descriptor-driven install path (dispatchKindEntry) applies
* per-runtime agent conversion when the 'agents' kind entry has a non-null
* converter — instead of silently raw-copying.
*
* Behavioral assertions: invoke the staging/dispatch seam, inspect the staged
* output files. NOT source-grep.
*
* TDD flow (REGRESSION-MUST-FAIL-FIRST rule):
* Before the fix, dispatchKindEntry ignores the converter for agents kind and
* raw-copies. The tests below prove conversion is applied by asserting that
* the staged .md contains runtime-specific frontmatter transformations absent
* in the raw source.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const ROOT = path.join(__dirname, '..');
const {
resolveRuntimeArtifactLayoutFromRegistry,
} = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'));
const {
cleanupStagedSkills,
} = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'install-profiles.cjs'));
const { cleanup } = require('./helpers.cjs');
// ─── fixtures ────────────────────────────────────────────────────────────────
/**
* Minimal Claude agent source with comma-separated tools (Claude format).
* Codex conversion adds <codex_agent_role> block.
* Cursor conversion strips color field.
*/
const CLAUDE_AGENT_SOURCE = `---
name: msd-planner
description: A MSD planning agent.
tools: Bash, Read, Write
color: blue
---
# MSD Planner
This agent plans MSD phases using ~/.claude/skills.
`;
/**
* Create a temp agents directory with a .msd-source marker pointing back to it
* (so findAgentsSourceRoot finds the fixture dir, not the real agents/ dir).
* The .msd-source convention expects the marker to point at a commands/msd dir;
* agents/ is resolved as a sibling of commands/. So we set up:
* <tmproot>/
* commands/msd/ (empty, satisfies the sibling check)
* agents/
* msd-planner.md
* .msd-source <- points to <tmproot>/commands/msd
*/
function makeFixtureRoot(agentFiles) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1173-root-'));
const commandsDir = path.join(root, 'commands', 'msd');
const agentsDir = path.join(root, 'agents');
fs.mkdirSync(commandsDir, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
// .msd-source marker must point to commands/msd so that agentsSourceRoot resolves to agents/
fs.writeFileSync(path.join(root, '.msd-source'), commandsDir + '\n', 'utf8');
for (const { name, content } of agentFiles) {
fs.writeFileSync(path.join(agentsDir, name), content, 'utf8');
}
return root;
}
function makeSyntheticRegistry(converterName) {
return {
runtimes: {
testruntime: {
runtime: {
artifactLayout: {
global: [
{
kind: 'agents',
destSubpath: 'agents',
prefix: 'msd-',
nesting: 'flat',
recursive: false,
converter: converterName,
},
],
local: [],
},
},
},
},
};
}
// ─── stageAgentsForRuntimeWithConverter unit tests ────────────────────────────
describe('feat-1173: stageAgentsForRuntimeWithConverter', () => {
test('is exported from install-profiles', () => {
const installProfiles = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'install-profiles.cjs'));
assert.strictEqual(
typeof installProfiles.stageAgentsForRuntimeWithConverter,
'function',
'stageAgentsForRuntimeWithConverter must be exported',
);
});
test('applies converter to each staged agent file', (t) => {
const { stageAgentsForRuntimeWithConverter } = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'install-profiles.cjs'));
const agentsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1173-agents-'));
fs.writeFileSync(path.join(agentsDir, 'msd-planner.md'), CLAUDE_AGENT_SOURCE, 'utf8');
fs.writeFileSync(path.join(agentsDir, 'msd-executor.md'), CLAUDE_AGENT_SOURCE.replace('msd-planner', 'msd-executor'), 'utf8');
t.after(() => {
cleanup(agentsDir);
cleanupStagedSkills();
});
const calls = [];
const converter = (content) => {
calls.push(content);
return content.replace('~/.claude/', '~/.copilot/');
};
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
const stagedDir = stageAgentsForRuntimeWithConverter(agentsDir, resolvedProfile, converter);
assert.strictEqual(calls.length, 2, 'converter called for each agent file');
const stagedFiles = fs.readdirSync(stagedDir).sort();
assert.deepStrictEqual(stagedFiles, ['msd-executor.md', 'msd-planner.md']);
// Converter replaced ~/.claude/ with ~/.copilot/ in all staged files
for (const file of stagedFiles) {
const content = fs.readFileSync(path.join(stagedDir, file), 'utf8');
assert.ok(!content.includes('~/.claude/'), `${file}: converter must have replaced ~/.claude/`);
assert.ok(content.includes('~/.copilot/'), `${file}: converter must have injected ~/.copilot/`);
}
});
test('non-existent srcAgentsDir returns srcAgentsDir unchanged', () => {
const { stageAgentsForRuntimeWithConverter } = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'install-profiles.cjs'));
const ghost = path.join(os.tmpdir(), 'msd-1173-no-exist-' + Date.now());
const converter = (c) => c;
const result = stageAgentsForRuntimeWithConverter(ghost, { name: 'full', skills: '*', agents: new Set() }, converter);
assert.strictEqual(result, ghost, 'must return srcAgentsDir unchanged for non-existent dir');
});
test('only copies .md files (ignores non-.md)', (t) => {
const { stageAgentsForRuntimeWithConverter } = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'install-profiles.cjs'));
const agentsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1173-agents-'));
fs.writeFileSync(path.join(agentsDir, 'msd-planner.md'), CLAUDE_AGENT_SOURCE, 'utf8');
fs.writeFileSync(path.join(agentsDir, 'README.txt'), 'not an agent', 'utf8');
t.after(() => {
cleanup(agentsDir);
cleanupStagedSkills();
});
const converter = (c) => c;
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
const stagedDir = stageAgentsForRuntimeWithConverter(agentsDir, resolvedProfile, converter);
const stagedFiles = fs.readdirSync(stagedDir);
assert.deepStrictEqual(stagedFiles, ['msd-planner.md'], 'only .md files should be staged');
});
});
// ─── dispatchKindEntry wiring tests ──────────────────────────────────────────
describe('feat-1173: dispatchKindEntry agents converter wiring', () => {
test('agents kind with convertClaudeAgentToCodexAgent converter applies codex conversion', (t) => {
const fixtureRoot = makeFixtureRoot([{ name: 'msd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
t.after(() => {
cleanup(fixtureRoot);
cleanupStagedSkills();
});
const registry = makeSyntheticRegistry('convertClaudeAgentToCodexAgent');
const layout = resolveRuntimeArtifactLayoutFromRegistry(
registry, 'testruntime', fixtureRoot, 'global',
);
const agentKind = layout.kinds[0];
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
const stagedDir = agentKind.stage(resolvedProfile);
const stagedContent = fs.readFileSync(path.join(stagedDir, 'msd-planner.md'), 'utf8');
// Codex conversion adds <codex_agent_role> block
assert.notStrictEqual(stagedContent, CLAUDE_AGENT_SOURCE, 'converter must have transformed the content');
assert.ok(
stagedContent.includes('<codex_agent_role>'),
`Codex conversion must add <codex_agent_role>. Got:\n${stagedContent.slice(0, 300)}`,
);
});
test('agents kind with convertClaudeAgentToCursorAgent converter applies cursor conversion', (t) => {
const fixtureRoot = makeFixtureRoot([{ name: 'msd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
t.after(() => {
cleanup(fixtureRoot);
cleanupStagedSkills();
});
const registry = makeSyntheticRegistry('convertClaudeAgentToCursorAgent');
const layout = resolveRuntimeArtifactLayoutFromRegistry(
registry, 'testruntime', fixtureRoot, 'global',
);
const agentKind = layout.kinds[0];
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
const stagedDir = agentKind.stage(resolvedProfile);
const stagedContent = fs.readFileSync(path.join(stagedDir, 'msd-planner.md'), 'utf8');
// Cursor conversion strips color field and rewrites ~/.claude/ paths
assert.notStrictEqual(stagedContent, CLAUDE_AGENT_SOURCE, 'converter must have transformed the content');
assert.ok(
!stagedContent.includes('color:'),
`Cursor agent conversion should strip the color field. Got:\n${stagedContent.slice(0, 300)}`,
);
});
test('agents kind with converter=null still raw-copies (backward compat for claude)', (t) => {
const fixtureRoot = makeFixtureRoot([{ name: 'msd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
t.after(() => {
cleanup(fixtureRoot);
cleanupStagedSkills();
});
const registry = makeSyntheticRegistry(null);
const layout = resolveRuntimeArtifactLayoutFromRegistry(
registry, 'testruntime', fixtureRoot, 'global',
);
const agentKind = layout.kinds[0];
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
const stagedDir = agentKind.stage(resolvedProfile);
// converter=null: stageAgentsForProfile with skills='*' returns srcAgentsDir unchanged
// (no staging dir is created; the source dir IS the staged dir, a passthrough)
const stagedContent = fs.readFileSync(path.join(stagedDir, 'msd-planner.md'), 'utf8');
assert.strictEqual(stagedContent, CLAUDE_AGENT_SOURCE, 'converter=null must raw-copy the agent content');
});
test('scope threads isGlobal to a scope-aware converter (global vs local differ)', (t) => {
// The plumbing kept by #1173 (option a): convertedAgentsKind / dispatchKindEntry
// pass the install scope to the converter as isGlobal. A scope-aware converter
// (antigravity) must therefore produce different output for global vs local. This
// proves the thread is live via a synthetic descriptor — no real runtime
// declares a converted agents kind yet (declarations deferred to the ADR-1235
// §0 parity follow-up).
const fixtureRoot = makeFixtureRoot([{ name: 'msd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
t.after(() => {
cleanup(fixtureRoot);
cleanupStagedSkills();
});
const agentsEntry = {
kind: 'agents',
destSubpath: 'agents',
prefix: 'msd-',
nesting: 'flat',
recursive: false,
converter: 'convertClaudeAgentToAntigravityAgent',
};
const registry = {
runtimes: { testruntime: { runtime: { artifactLayout: { global: [agentsEntry], local: [agentsEntry] } } } },
};
const profile = { name: 'full', skills: '*', agents: new Set() };
const stageFor = (scope) => {
const layout = resolveRuntimeArtifactLayoutFromRegistry(registry, 'testruntime', fixtureRoot, scope);
const agentKind = layout.kinds.find((k) => k.kind === 'agents');
assert.ok(agentKind, `${scope} layout must include an agents kind`);
return fs.readFileSync(path.join(agentKind.stage(profile), 'msd-planner.md'), 'utf8');
};
const globalOut = stageFor('global');
const localOut = stageFor('local');
assert.notStrictEqual(
globalOut,
localOut,
'scope-aware converter output must differ by scope — proves isGlobal is threaded from the descriptor scope',
);
});
});
// ─── real registry: claude agents kind has converter=null ────────────────────
describe('feat-1173: real registry claude agents kind has converter=null (backward compat)', () => {
test('claude local artifacts layout has agents entry with converter=null', () => {
const registry = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'capability-registry.cjs'));
const claudeDesc = registry.runtimes?.claude?.runtime?.artifactLayout?.local ?? [];
const agentsEntry = claudeDesc.find((e) => e.kind === 'agents');
assert.ok(agentsEntry, 'claude local artifactLayout must have an agents entry');
assert.strictEqual(agentsEntry.converter, null, 'claude agents entry must have converter=null');
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/fix-1515-codex-runtime-default.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:fix-1515-codex-runtime-default (consolidation epic #1969 B3 #1972)", () => {
'use strict';
/**
* Regression tests for bug #1515: Codex install with runtime-neutral
* .planning/config.json resolves runtime as 'claude' and enables worktree
* isolation (unsafe for Codex).
*
* Root causes:
* A) config-get reads in workflows lacked --raw → output JSON-quoted →
* every comparison like [ "$RUNTIME" = "codex" ] failed silently.
* B) The conversion engine emitted --default claude for every runtime →
* neutral Codex config fell back to claude default.
*
* All tests assert on the SUT's RETURN VALUE (engine output), not raw file reads,
* except the integration test (test 4) which is explicitly the source↔engine
* parity guard and carries the allow-test-rule exemption.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('fast-check');
const conversion = require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
// ---------------------------------------------------------------------------
// Unit tests: engine stamps codex-specific defaults into emitted workflows
// ---------------------------------------------------------------------------
test('codex emit stamps its own runtime default into the runtime-resolution line', () => {
const line =
'RUNTIME=$(msd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n';
const out = conversion._applyRuntimeRewrites(line, 'codex', '$HOME/.codex/', true, undefined);
assert.ok(
out.includes('config-get runtime --default codex --raw'),
`Expected 'config-get runtime --default codex --raw' in output; got:\n${out}`,
);
assert.ok(
out.includes('|| echo "codex")'),
`Expected '|| echo "codex")' in output; got:\n${out}`,
);
assert.ok(
!out.includes('--default claude'),
`Expected '--default claude' to be fully rewritten; got:\n${out}`,
);
assert.ok(
!out.includes('echo "claude"'),
`Expected 'echo "claude"' to be fully rewritten; got:\n${out}`,
);
});
test('codex emit leaves workflow.use_worktrees at the true default — isolation is negotiated, not stamped (#1515 premise superseded by #2584/#2652)', () => {
// #1515 stamped `--default false` here because worktree isolation *was*
// Claude Code's isolation="worktree" spawn parameter, so a Codex install that
// resolved use_worktrees=true would have run unisolated while believing it was
// isolated. #2584 removed that premise: Codex declares
// `dispatch.isolation: orchestrator-worktree`, meaning MSD creates the
// worktree itself and spawns `codex exec --cd <worktree>` — supported, not
// unsafe. Keeping the stamp would resolve USE_WORKTREES=false before
// dispatch-isolation is consulted, re-deciding isolation by runtime name,
// which is the exact defect #2652 removes. The safety property #1515 protects
// is now held by the isolation gate's fail-closed resolution, not by a
// name-scoped install-time default.
const line =
'USE_WORKTREES=$(msd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")\n';
const out = conversion._applyRuntimeRewrites(line, 'codex', '$HOME/.codex/', true, undefined);
assert.strictEqual(
conversion._negotiatedDispatchIsolation('codex'),
'orchestrator-worktree',
'codex must declare orchestrator-worktree for this expectation to hold',
);
assert.strictEqual(
out,
line,
`Expected the use_worktrees read to survive codex emit untouched; got:\n${out}`,
);
});
test('claude runtime does NOT rewrite the runtime default — stamping is non-claude-scoped (#1521 inversion)', () => {
// #1521 generalizes stamping to ALL non-Claude runtimes. The negative case
// (no stamping) is now the 'claude' runtime, not other non-Claude runtimes.
const line =
'RUNTIME=$(msd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n';
const out = conversion._applyRuntimeRewrites(line, 'claude', '$HOME/.claude/', true, undefined);
assert.ok(
out.includes('--default claude --raw'),
`Expected claude output to preserve '--default claude --raw'; got:\n${out}`,
);
assert.ok(
!out.includes('--default codex'),
`Expected claude output NOT to contain '--default codex'; got:\n${out}`,
);
});
// ---------------------------------------------------------------------------
// Integration / parity guard: real source ↔ engine output for codex (all surfaces)
// ---------------------------------------------------------------------------
test('regression: every edited workflow gets codex-stamped (source↔engine parity, all surfaces) (#1515)', () => {
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// `out` is _applyRuntimeRewrites's engine-transformed shell text, not shipped
// source — substring-matching it is the "Rendered file" row CONTRIBUTING
// requires a typed-IR builder for. No such IR exists yet for the shell
// rewrite output; production change out of scope here. Tracked under #3090.
const WORKFLOWS = ['execute-phase.md', 'autonomous.md', 'manager.md', 'diagnose-issues.md', 'quick.md'];
const CLAUDE_RUNTIME = 'config-get runtime --default claude --raw 2>/dev/null || echo "claude"';
const CODEX_RUNTIME = 'config-get runtime --default codex --raw 2>/dev/null || echo "codex"';
const TRUE_WT = 'config-get workflow.use_worktrees --raw 2>/dev/null || echo "true"';
const FALSE_WT = 'config-get workflow.use_worktrees --default false --raw 2>/dev/null || echo "false"';
for (const wf of WORKFLOWS) {
const src = fs.readFileSync(path.join(__dirname, '..', 'msd-core', 'workflows', wf), 'utf8');
const out = conversion._applyRuntimeRewrites(src, 'codex', '$HOME/.codex/', true, undefined);
// No un-stamped claude runtime line may survive codex emit on ANY surface.
assert.ok(!out.includes(CLAUDE_RUNTIME), `${wf}: residual un-stamped runtime read — engine regex no longer matches source line (parity drift)`);
// If the source HAS such a read, the codex form must be present.
if (src.includes(CLAUDE_RUNTIME)) assert.ok(out.includes(CODEX_RUNTIME), `${wf}: runtime read not stamped to codex`);
// #2652: codex declares orchestrator-worktree, so the use_worktrees read is
// left for `msd_run query dispatch-isolation` to decide at run time — the
// install-time `--default false` stamp would pre-empt that negotiation.
if (src.includes(TRUE_WT)) {
assert.ok(out.includes(TRUE_WT), `${wf}: use_worktrees read was stamped away for a runtime that negotiates worktree isolation (#2652)`);
assert.ok(!out.includes(FALSE_WT), `${wf}: codex gained the --default false use_worktrees stamp (#2652)`);
}
}
});
// ---------------------------------------------------------------------------
// Property tests (RULESET.TESTS.property-based-testing)
// ---------------------------------------------------------------------------
test('property: runtime stamping applies for ALL non-claude runtimes; only claude leaves --default claude unchanged (#1521)', () => {
// #1521: generalised from codex-only to all non-claude runtimes.
// Use the canonical list from the conversion module to avoid hand-rolled array drift.
const { NON_CLAUDE_RUNTIMES } = conversion;
const RUNTIMES = ['claude', ...NON_CLAUDE_RUNTIMES];
const line = 'RUNTIME=$(msd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n';
fc.assert(fc.property(fc.constantFrom(...RUNTIMES), (rt) => {
const out = conversion._applyRuntimeRewrites(line, rt, `$HOME/.${rt}/`, true, undefined);
return rt === 'claude'
? out.includes('--default claude --raw') && !out.includes('--default codex')
: out.includes(`--default ${rt} --raw`) && !out.includes('--default claude');
}));
});
test('property: codex stamping is idempotent on resolution lines (#1515)', () => {
fc.assert(fc.property(fc.constantFrom('runtime', 'use_worktrees'), (which) => {
const line = which === 'runtime'
? 'RUNTIME=$(msd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n'
: 'USE_WORKTREES=$(msd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")\n';
const once = conversion._applyRuntimeRewrites(line, 'codex', '$HOME/.codex/', true, undefined);
const twice = conversion._applyRuntimeRewrites(once, 'codex', '$HOME/.codex/', true, undefined);
return once === twice;
}));
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/fix-1521-non-claude-runtime-default-resolution.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:fix-1521-non-claude-runtime-default-resolution (consolidation epic #1969 B3 #1972)", () => {
'use strict';
/**
* Regression tests for #1521: every non-Claude runtime stamps its own runtime
* identity into emitted workflows, and stamps workflow.use_worktrees=false
* where its negotiated dispatch.isolation is `none`.
*
* #1519 (Codex-only fix) was generalized here to ALL non-Claude runtimes on the
* premise that MSD's worktree isolation was Claude Code's isolation="worktree"
* spawn parameter, which no other runtime honored. #2584 replaced that premise
* with the negotiated `dispatch.isolation` capability, and #2652 scoped the
* use_worktrees stamp to match: a host declaring harness-worktree or
* orchestrator-worktree keeps the `true` default, because stamping it false
* pre-empts the negotiation and re-decides isolation by runtime name.
*
* All tests assert on the SUT's RETURN VALUE (engine output), not raw file reads,
* except the parity integration test which carries the allow-test-rule exemption.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('fast-check');
const conversion = require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
// #1521: use the canonical list from the conversion module rather than a hand-rolled
// local array that can drift from the real runtime set.
const { NON_CLAUDE_RUNTIMES: NON_CLAUDE } = conversion;
const WORKFLOWS = [
'execute-phase.md', 'autonomous.md', 'manager.md', 'diagnose-issues.md', 'quick.md',
];
const CLAUDE_RUNTIME_LINE = 'config-get runtime --default claude --raw 2>/dev/null || echo "claude"';
const TRUE_WT_LINE = 'config-get workflow.use_worktrees --raw 2>/dev/null || echo "true"';
const FALSE_WT_LINE = 'config-get workflow.use_worktrees --default false --raw 2>/dev/null || echo "false"';
// ---------------------------------------------------------------------------
// Parity across ALL non-Claude runtimes × all 5 workflows
// ---------------------------------------------------------------------------
test('parity: every non-Claude runtime stamps its own runtime default, and use_worktrees=false only where isolation negotiates to none (#1521, #2652)', () => {
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// `out` is _applyRuntimeRewrites's engine-transformed shell text, not shipped
// source — substring-matching it is the "Rendered file" row CONTRIBUTING
// requires a typed-IR builder for. No such IR exists yet for the shell
// rewrite output; production change out of scope here. Tracked under #3090.
for (const rt of NON_CLAUDE) {
const isolation = conversion._negotiatedDispatchIsolation(rt);
for (const wf of WORKFLOWS) {
const src = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', wf),
'utf8',
);
const out = conversion._applyRuntimeRewrites(src, rt, `$HOME/.${rt}/`, true, undefined);
// No un-stamped claude runtime line may survive
assert.ok(
!out.includes(CLAUDE_RUNTIME_LINE),
`${rt}/${wf}: residual un-stamped claude runtime read — _stampNonClaudeRuntimeDefaults not applied`,
);
// If the source had a runtime read, the output must have --default <rt>
if (src.includes(CLAUDE_RUNTIME_LINE)) {
assert.ok(
out.includes(`config-get runtime --default ${rt} --raw 2>/dev/null || echo "${rt}"`),
`${rt}/${wf}: runtime line not stamped to --default ${rt}`,
);
}
if (!src.includes(TRUE_WT_LINE)) continue;
// #2652: the use_worktrees=false stamp is scoped to the runtimes whose
// negotiated dispatch.isolation really is `none`. A host that declares
// harness-worktree (cursor) or orchestrator-worktree (codex, opencode)
// must keep the unstamped `true` default, or the stamp
// resolves USE_WORKTREES=false before dispatch-isolation is consulted and
// the runtime is judged by its name after all.
if (isolation === 'none') {
assert.ok(
!out.includes(TRUE_WT_LINE),
`${rt}/${wf}: residual un-stamped use_worktrees=true read — _stampNonClaudeRuntimeDefaults not applied`,
);
assert.ok(
out.includes(FALSE_WT_LINE),
`${rt}/${wf}: use_worktrees line not defaulted to false`,
);
} else {
assert.ok(
out.includes(TRUE_WT_LINE),
`${rt}/${wf}: declares dispatch.isolation=${isolation} but the use_worktrees read was stamped away — the install-time default pre-empts the negotiated capability (#2652)`,
);
assert.ok(
!out.includes(FALSE_WT_LINE),
`${rt}/${wf}: declares dispatch.isolation=${isolation} but gained the --default false stamp`,
);
}
}
}
});
// ---------------------------------------------------------------------------
// #2652: the stamp's scope is the negotiated capability, not the runtime name
// ---------------------------------------------------------------------------
test('regression #2652: _stampNonClaudeRuntimeDefaults leaves use_worktrees alone for every runtime that negotiates a worktree isolation', () => {
const line =
'USE_WORKTREES=$(msd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")\n';
// The set is derived from the registry, not hand-listed, so a newly declared
// worktree host is covered the moment it lands — the same reason
// NON_CLAUDE_RUNTIMES is derived rather than literal (#1521).
const declaresWorktree = NON_CLAUDE.filter(
(rt) => conversion._negotiatedDispatchIsolation(rt) !== 'none',
);
const declaresNone = NON_CLAUDE.filter(
(rt) => conversion._negotiatedDispatchIsolation(rt) === 'none',
);
// Both arms must be non-empty or the test proves nothing about either.
assert.ok(
declaresWorktree.length > 0,
'no non-Claude runtime negotiates a worktree isolation — the regression this pins is unreachable',
);
assert.ok(
declaresNone.length > 0,
'every non-Claude runtime negotiates a worktree isolation — the false stamp is dead code',
);
for (const rt of declaresWorktree) {
assert.strictEqual(
conversion._stampNonClaudeRuntimeDefaults(line, rt),
line,
`${rt}: negotiates ${conversion._negotiatedDispatchIsolation(rt)} but its use_worktrees default was stamped false at install time, so dispatch-isolation-gate.md resolves ISOLATION=none regardless of what it declared (#2652)`,
);
}
for (const rt of declaresNone) {
assert.ok(
conversion._stampNonClaudeRuntimeDefaults(line, rt).includes(FALSE_WT_LINE),
`${rt}: negotiates none, so the false default must still be stamped (#1521)`,
);
}
});
test('regression #2652: _negotiatedDispatchIsolation fails closed on an undeclared or unknown runtime', () => {
// Mirrors routeDispatchIsolation's fail-closed contract (ADR-1239): anything
// outside the closed vocabulary resolves to `none`, so the #1521 stamp — and
// the workflows' isolation gate — degrade to sequential rather than to an
// unisolated dispatch that believes it is isolated.
for (const unknown of ['not-a-runtime', '', 'CLAUDE', '__proto__']) {
assert.strictEqual(
conversion._negotiatedDispatchIsolation(unknown),
'none',
`${JSON.stringify(unknown)}: expected fail-closed 'none'`,
);
}
// A registry-known host whose declared value is the `undocumented` sentinel
// (or absent) is out of vocabulary and must degrade the same way.
const registry = require('../msd-core/bin/lib/capability-registry.cjs');
const undocumented = NON_CLAUDE.filter((rt) => {
const declared = registry?.runtimes?.[rt]?.runtime?.hostIntegration?.dispatch?.isolation;
return declared === 'undocumented' || declared == null;
});
assert.ok(undocumented.length > 0, 'no runtime declares the undocumented sentinel — nothing to pin');
for (const rt of undocumented) {
assert.strictEqual(conversion._negotiatedDispatchIsolation(rt), 'none', `${rt}: undocumented must degrade to none`);
}
});
// ---------------------------------------------------------------------------
// #2652 review round-5/6 Major 1 — PARITY with the runtime resolver.
//
// `_negotiatedDispatchIsolation` (install time, this module) and
// `routeDispatchIsolation` (dispatch time, msd-core/bin/msd-tools.cjs) are two
// implementations of ONE rule: what isolation may this host negotiate. They
// read the same inputs — the capability registry and `resolveOrchestratorExec`
// — but duplicate the DECISION on top of them, on two surfaces (the install
// engine and the CLI query hub) with no call edge between them. So neither
// symbol appears in the other's impact graph and no static analysis sees the
// duplication. Divergence would be silent, and its consequence is the #2652
// defect returning by the back door: the installer stamping
// `use_worktrees=false` for a host the resolver would have granted a worktree
// (or the reverse — an unstamped host whose dispatch then refuses).
//
// The assertion is therefore behavioral on BOTH sides: the resolver leg drives
// the REAL CLI and reads its actual stdout.
// ---------------------------------------------------------------------------
test('regression #2652: _negotiatedDispatchIsolation agrees with the routeDispatchIsolation CLI for every registered runtime', () => {
const { runMsdTools, createTempProject, cleanup: cleanupDir } = require('./helpers.cjs');
const registry = require('../msd-core/bin/lib/capability-registry.cjs');
const RUNTIME_IDS = Object.keys(registry.runtimes).sort();
// Install time cannot know the worktree a future dispatch will target, so
// `_negotiatedDispatchIsolation` probes the orchestrator descriptor with a
// placeholder. The CLI is asked BOTH ways, because the two calls are both real:
//
// --cwd-target <probe> the executor-spawn call, which resolves the descriptor.
// Same question install time asks; must match exactly.
// (no --cwd-target) the FIRST call every dispatch site makes — the
// `Resolve ISOLATION` block in dispatch-isolation-gate.md.
// It skips descriptor resolution, so it can only differ
// from install time for an orchestrator host whose
// descriptor does not resolve. No such host exists today
// and that is worth pinning: if one lands, the install
// stamps `use_worktrees=false` while the workflow gate
// still reports `orchestrator-worktree` — the #2652 split
// brain, in the one shape the target-bound leg cannot see.
const PROBE_TARGET = '/msd-orchestrator-worktree-probe';
const dir = createTempProject('msd-2652-parity-');
try {
const disagreements = [];
const seen = new Set();
for (const rt of RUNTIME_IDS) {
const res = runMsdTools(
['query', 'dispatch-isolation', '--json', '--cwd-target', PROBE_TARGET],
dir,
{ MSD_RUNTIME: rt, HOME: dir },
);
assert.equal(res.success, true, `${rt}: dispatch-isolation query failed: ${res.error}`);
const parsed = JSON.parse(res.output);
// Guard the guard: if resolveRuntime normalized `rt` to some other id, the
// comparison below would be pinning the wrong host and silently pass.
assert.equal(
parsed.runtime,
rt,
`${rt}: the CLI resolved runtime "${parsed.runtime}" instead — parity would be measured against the wrong host`,
);
const gateRes = runMsdTools(
['query', 'dispatch-isolation', '--raw'],
dir,
{ MSD_RUNTIME: rt, HOME: dir },
);
assert.equal(gateRes.success, true, `${rt}: no-target dispatch-isolation query failed: ${gateRes.error}`);
const gateIsolation = gateRes.output.trim();
const installTime = conversion._negotiatedDispatchIsolation(rt);
seen.add(installTime);
if (installTime !== parsed.isolation) {
disagreements.push(
`${rt}: install-time _negotiatedDispatchIsolation → "${installTime}", ` +
`dispatch-time routeDispatchIsolation (--cwd-target) → "${parsed.isolation}"`,
);
}
if (installTime !== gateIsolation) {
disagreements.push(
`${rt}: install-time _negotiatedDispatchIsolation → "${installTime}", ` +
`workflow-gate routeDispatchIsolation (no --cwd-target) → "${gateIsolation}"`,
);
}
}
assert.deepEqual(
disagreements,
[],
'the install-time and dispatch-time isolation resolvers disagree. One of them was ' +
'changed without the other (DEFECT.GENERATIVE-FIX-DIVERGENCE): the installer and the ' +
'dispatch gate would then make opposite isolation decisions for the same host.\n' +
disagreements.join('\n'),
);
// A parity check over a set that only ever answers `none` proves nothing —
// both legs could be stubbed to a constant and still agree.
for (const mode of ['harness-worktree', 'orchestrator-worktree', 'none']) {
assert.ok(
seen.has(mode),
`no registered runtime resolves to "${mode}" — the parity above never exercised that branch`,
);
}
} finally {
cleanupDir(dir);
}
});
// ---------------------------------------------------------------------------
// Claude unchanged — no stamping for the native runtime
// ---------------------------------------------------------------------------
test('claude runtime leaves runtime default and use_worktrees=true unchanged (#1521)', () => {
const src = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', 'execute-phase.md'),
'utf8',
);
const out = conversion._applyRuntimeRewrites(src, 'claude', '$HOME/.claude/', true, undefined);
// Claude emit must preserve the original --default claude line
if (src.includes(CLAUDE_RUNTIME_LINE)) {
assert.ok(
out.includes(CLAUDE_RUNTIME_LINE),
`claude/execute-phase.md: expected original claude runtime line to survive; got mutated`,
);
}
// Claude emit must NOT gain --default false for use_worktrees
assert.ok(
!out.includes(FALSE_WT_LINE),
`claude/execute-phase.md: use_worktrees line must NOT be stamped false for claude runtime`,
);
});
// ---------------------------------------------------------------------------
// fc property — identity: each runtime stamps itself, claude stays unchanged
// ---------------------------------------------------------------------------
test('property: _stampNonClaudeRuntimeDefaults stamps each non-claude runtime and leaves claude unchanged (#1521)', () => {
const line =
'RUNTIME=$(msd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n';
fc.assert(
fc.property(fc.constantFrom(...NON_CLAUDE, 'claude'), (rt) => {
const out = conversion._applyRuntimeRewrites(line, rt, `$HOME/.${rt}/`, true, undefined);
if (rt === 'claude') {
return out.includes('--default claude') && !/--default (?!claude)/.test(out);
}
return out.includes(`--default ${rt}`) && !out.includes('--default claude');
}),
);
});
// ---------------------------------------------------------------------------
// fc property — idempotence: stamping twice equals once
// ---------------------------------------------------------------------------
test('property: _stampNonClaudeRuntimeDefaults is idempotent (#1521)', () => {
fc.assert(
fc.property(
fc.constantFrom(...NON_CLAUDE),
fc.constantFrom('runtime', 'use_worktrees'),
(rt, which) => {
const line =
which === 'runtime'
? 'RUNTIME=$(msd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")\n'
: 'USE_WORKTREES=$(msd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")\n';
const once = conversion._applyRuntimeRewrites(line, rt, `$HOME/.${rt}/`, true, undefined);
const twice = conversion._applyRuntimeRewrites(once, rt, `$HOME/.${rt}/`, true, undefined);
return once === twice;
},
),
);
});
// ---------------------------------------------------------------------------
// Guard generalization: execute-phase.md uses != "claude" not = "codex"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Guard generalization: execute-phase.md, quick.md, and diagnose-issues.md
// all use != "claude" (not = "codex") for the worktree guard (#1521)
// ---------------------------------------------------------------------------
test('execute-phase.md, quick.md, and diagnose-issues.md guards are generalized to != "claude" (not Codex-specific) (#1521)', () => {
// allow-test-rule: source-text-is-the-product (#1521)
// Reads the raw shipped workflow .md source directly (not engine-transformed
// output) and asserts on its literal guard-clause text — the deployed prose
// IS the runtime contract here.
// #2584 Phase 3 (#2627): execute-phase.md graduated PAST the `!= "claude"`
// guard — worktree isolation there is now keyed on the negotiated
// `dispatch.isolation` capability, so no runtime name appears in its guard at
// all.
//
// #2652: quick.md and diagnose-issues.md have now graduated too. This block
// previously asserted they still carried the #1521 generalized form, with a
// comment noting "they do not negotiate isolation" — i.e. the test knowingly
// pinned the un-migrated state #2652 was filed about. They negotiate now, so
// they are held to the same capability-keyed contract as execute-phase.md.
const GUARD_WORKFLOWS = ['quick.md', 'diagnose-issues.md', 'execute-phase.md'];
for (const wf of GUARD_WORKFLOWS) {
const src = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', wf),
'utf8',
);
assert.ok(
!src.includes('[ "$RUNTIME" != "claude" ] && [ "$USE_WORKTREES" != "false" ]'),
`${wf}: worktree isolation must branch on dispatch.isolation, not on != "claude" (#2584/#2652)`,
);
assert.ok(
!src.includes('[ "$RUNTIME" = "codex" ] && [ "$USE_WORKTREES" != "false" ]'),
`${wf}: found Codex-specific guard — isolation is a negotiated capability (#2584/#2652)`,
);
}
// The two migrated sites must actually negotiate, not merely drop the guard.
for (const wf of ['quick.md', 'diagnose-issues.md']) {
const src = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', wf),
'utf8',
);
assert.ok(
src.includes('query dispatch-isolation'),
`${wf}: must resolve ISOLATION via \`msd_run query dispatch-isolation\` (#2652)`,
);
}
// execute-phase.md: the guard must be capability-keyed, with NO runtime name.
const executePhase = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', 'execute-phase.md'),
'utf8',
);
assert.ok(
!/\[\s*"\$RUNTIME"\s*(?:!=|=)\s*"[a-z-]+"\s*\]\s*&&\s*\[\s*"\$USE_WORKTREES"/.test(executePhase),
'execute-phase.md: worktree isolation must branch on dispatch.isolation, not on a runtime name (#2584)',
);
assert.ok(
executePhase.includes('Resolve ISOLATION'),
'execute-phase.md: must resolve the negotiated ISOLATION capability',
);
});
// ---------------------------------------------------------------------------
// Orchestration gating: manager.md + autonomous.md gate background dispatch on
// the typed FLATTEN query. #1708 (ADR-1239 Phase B) graduated #1521's
// codex-specific check to a documentation-sourced shouldFlattenDispatch — the
// prose now branches on `FLATTEN` (false = background), not a runtime name.
// ---------------------------------------------------------------------------
test('manager.md and autonomous.md gate run_in_background on FLATTEN=false, not a runtime name (#1521, graduated by #1708)', () => {
// allow-test-rule: source-text-is-the-product (#1521/#1708)
// Reads the raw shipped manager.md/autonomous.md workflow prose directly —
// the deployed orchestration dispatch gating text IS the runtime contract.
const manager = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', 'manager.md'),
'utf8',
);
const autonomous = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', 'autonomous.md'),
'utf8',
);
// Both files must gate run_in_background on the typed FLATTEN decision (not a runtime name)
assert.ok(
/If `FLATTEN` is `false`[\s\S]{0,500}?run_in_background=true/.test(manager),
'manager.md: expected run_in_background dispatch gated on FLATTEN=false (typed dispatch-should-flatten query)',
);
assert.ok(
/If `FLATTEN` is `false`[\s\S]{0,1200}?run_in_background=true/.test(autonomous),
'autonomous.md: expected run_in_background dispatch gated on FLATTEN=false (typed dispatch-should-flatten query)',
);
// Inline is the else branch, keyed on FLATTEN — never a runtime name
assert.ok(
/Otherwise[\s\S]{0,250}?inline/i.test(manager),
'manager.md: expected "Otherwise ... inline" branch keyed on FLATTEN',
);
assert.ok(
/Otherwise[\s\S]{0,250}?inline/i.test(autonomous),
'autonomous.md: expected "Otherwise ... inline" branch keyed on FLATTEN',
);
// And the old runtime-name gating must be gone (no `RUNTIME` is `codex` dispatch gate)
assert.ok(
!/`RUNTIME` is `codex`[\s\S]{0,500}?run_in_background=true/.test(manager),
'manager.md: must no longer gate run_in_background on the runtime name',
);
});
test('manager.md and autonomous.md no longer contain old "not claude" background-dispatch gating (#1521)', () => {
// allow-test-rule: source-text-is-the-product (#1521)
// Reads the raw shipped manager.md/autonomous.md workflow prose directly —
// the deployed orchestration dispatch gating text IS the runtime contract.
const manager = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', 'manager.md'),
'utf8',
);
const autonomous = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', 'autonomous.md'),
'utf8',
);
// The old phrasing that unconditionally sent every non-claude runtime to background must be gone
assert.ok(
!manager.includes('If `RUNTIME` is not `claude` (e.g. Codex)'),
'manager.md: old "If `RUNTIME` is not `claude` (e.g. Codex)" gating must be replaced',
);
assert.ok(
!autonomous.includes('On other runtimes:'),
'autonomous.md: old "On other runtimes:" branch label must be replaced',
);
});
});
}
// ────────────────────────────────────────────────────────────────────────
// #2486: settings.md must not recommend or persist worktree isolation on
// non-Claude runtimes, and health.md must surface an inherited explicit
// use_worktrees=true before execution fails closed on it. Both rely on the
// #1521 stamping machinery, so the canonical runtime/use_worktrees reads in
// those two files are part of the runtime contract surface.
// ────────────────────────────────────────────────────────────────────────
{
const fs = require('node:fs');
const path = require('node:path');
const conversion = require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
const { NON_CLAUDE_RUNTIMES } = conversion;
const RUNTIME_BRANCH_WORKFLOWS = ['settings.md', 'health.md'];
const CLAUDE_RUNTIME_LINE = 'config-get runtime --default claude --raw 2>/dev/null || echo "claude"';
const TRUE_WT_LINE = 'config-get workflow.use_worktrees --raw 2>/dev/null || echo "true"';
// #2486 review round 2 (#2584): the capability read that replaced the runtime-name
// gate. Round 4: `inspect-dispatch-isolation`, the side-effect-free inspection verb —
// the recording `dispatch-isolation` verb stamps the executor-dispatch sentinel as an
// unconditional #3045 side effect, which an inspection surface must never do.
// Round 9 (#2486 review, Major 3): this used to pin the single-line
// `ISOLATION=$(… || echo "none")` read. That shape was the defect — `|| echo
// "none"` collapses "this runtime declares no primitive" into "the resolver
// failed", and both surfaces then assert the former, which is false. The
// canonical read now captures the raw value and tracks whether a verdict was
// actually learned, the same shape the execution-side isolation gate uses.
// Pinned as two invariants rather than one long literal so that reformatting
// the block does not fail the test while a semantic regression still does.
const ISOLATION_LINE = '_INSPECTED_RAW=$(msd_run query inspect-dispatch-isolation --raw 2>/dev/null)';
const ISOLATION_RESOLVED_FLAG = 'INSPECTED_RESOLVED';
const ISOLATION_COLLAPSING_FALLBACK = /inspect-dispatch-isolation --raw 2>\/dev\/null \|\| echo/;
const readWorkflow = (wf) =>
fs.readFileSync(path.join(__dirname, '..', 'msd-core', 'workflows', wf), 'utf8');
// Behavioral W025 coverage (#2486 Major 2) executes the shipped block, so it
// needs a subprocess and a CRLF-safe read. `readFileNormalized` normalizes at
// the READ boundary — a `\r?\n` fence regex alone leaves embedded CR inside
// the captured body, which bash then treats as part of the token
// (DEFECT.WINDOWS-CRLF-TEST-PORTABILITY).
//
// The subprocess goes through the process seam, never a hand-rolled
// spawnSync (CONTRIBUTING "Spawning a subprocess: use the process seam").
// `runHook` already documents `interpreter: 'bash'` for running a shell
// script, so the harness is written to a file rather than passed as `-c`.
const { runHook } = require('./helpers/process-seam.cjs');
const { readFileNormalized, createTempDir, cleanup: cleanupDir } = require('./helpers.cjs');
const { scanFencedBlocks } = require('../msd-core/bin/lib/markdown-sectionizer.cjs');
// Skipped on Windows, where there is no bash. Checked by platform rather than
// by shelling out to `which`, which is itself non-portable.
const NO_BASH = process.platform === 'win32';
/** The first ```bash fenced block in `src` whose body includes `marker`. */
function bashBlockContaining(src, marker) {
const lines = src.split(/\r?\n/);
for (const block of scanFencedBlocks(lines)) {
if (block.closeLineIdx === -1) continue;
if ((block.infoString || '').trim() !== 'bash') continue;
const body = lines.slice(block.openLineIdx + 1, block.closeLineIdx).join('\n');
if (body.includes(marker)) return body;
}
return undefined;
}
describe('#2486 regression: settings/health worktrees isolation branch', () => {
// Review round 2 (#2584 Phase 3): isolation is a DECLARED CAPABILITY, not a
// runtime name. cursor declares harness-worktree and codex/opencode
// declare orchestrator-worktree, so a `RUNTIME != claude` gate
// would block a supported configuration on several runtimes and false-warn in
// health. Both surfaces branch on `dispatch-isolation` instead.
test('settings.md and health.md contain NO runtime-name gate for the worktrees branch', () => {
// allow-test-rule: source-text-is-the-product (#2486)
// Workflow .md text IS what the runtime loads — asserting on it tests the deployed contract.
for (const wf of RUNTIME_BRANCH_WORKFLOWS) {
const src = readWorkflow(wf);
assert.ok(
!src.includes(CLAUDE_RUNTIME_LINE),
`${wf}: the worktrees branch must not read the runtime name — gate on dispatch-isolation (#2584)`,
);
assert.ok(
!/RUNTIME"?\s*(!=|=)\s*"?claude/.test(src),
`${wf}: residual RUNTIME-vs-claude comparison — execute-phase forbids a runtime-name fan-out`,
);
// Prose gates count too: the shell-syntax check above missed two
// sentences that still asserted the obsolete non-Claude premise
// (the config-key list and the emitted-JSON schema comment).
assert.ok(
!/non-Claude (installs?|runtimes?)\b[^.]{0,120}\b(fail closed|default it to `?false)/i.test(src),
`${wf}: prose still states the obsolete "non-Claude cannot honor worktrees" premise — gate on dispatch.isolation`,
);
assert.ok(
!/never written as true on a non-Claude runtime/i.test(src),
`${wf}: emitted-JSON comment still claims a runtime-name persistence rule`,
);
}
});
// Round 4 note: an earlier revision carried a test here that encoded the
// #2728 merge order as a red assertion against quick.md/diagnose-issues.md.
// Deleted: a repo test cannot sequence merges — it only made this change
// unmergeable on its own schedule. The settings behavior change is instead
// scoped entirely to the `ISOLATION = none` branch (below), which needs
// nothing from any sibling PR; the `!= none` path is base behavior unchanged.
test('settings.md and health.md resolve isolation with the sentinel-free inspection read', () => {
// allow-test-rule: source-text-is-the-product (#2486)
// Workflow .md text IS what the runtime loads — asserting on it tests the deployed contract.
for (const wf of RUNTIME_BRANCH_WORKFLOWS) {
const src = readWorkflow(wf);
assert.ok(
src.includes(ISOLATION_LINE),
`${wf}: missing the canonical inspect-dispatch-isolation read (fail-closes unknown/undocumented to none)`,
);
// Major 3: fail-closed is right; reporting the failure AS a capability
// verdict is not. Both surfaces must be able to tell the two apart.
// #2486 review: the diagnostics name their state INSPECTED_ISOLATION,
// not ISOLATION. That is load-bearing, not cosmetic — #2728's
// "every dispatch-site degrade block re-records" guard scans for a
// literal `ISOLATION=none` in any workflow bash block, and a read-only
// surface has no sentinel to re-record. Naming the read differently
// keeps these files out of that scan BY CONSTRUCTION; the alternative
// was exempting the two files, which silently covered any future
// dispatch block added to them.
assert.ok(
!/^\s*ISOLATION=/m.test(src),
`${wf}: a read-only diagnostic must not assign the dispatch-site variable name ISOLATION — use INSPECTED_ISOLATION so #2728's re-record guard does not have to carve out this file`,
);
assert.ok(
src.includes(ISOLATION_RESOLVED_FLAG),
`${wf}: must track ISOLATION_RESOLVED — a resolver failure is not a declaration of 'none' (#2486 review, Major 3)`,
);
assert.ok(
!ISOLATION_COLLAPSING_FALLBACK.test(src),
`${wf}: '|| echo "none"' on the inspect read collapses "could not resolve" into "declares none" — capture the raw value and branch on ISOLATION_RESOLVED instead`,
);
// #2486 round 4 (B1): the RECORDING resolver is dispatch-only. On
// current next, `query dispatch-isolation` persists its decision to
// .msd/dispatch-isolation-sentinel.json as an unconditional #3045 side
// effect, and the isolation guard hooks hard-fail dispatches that
// disagree with the recorded sentinel. An inspection surface calling it
// would let /msd:health or /msd:settings hard-block executor dispatch
// for the sentinel's lifetime — across sessions, since the sentinel
// root resolves linked worktrees to the main checkout.
assert.ok(
!src.includes('query dispatch-isolation'),
`${wf}: calls the RECORDING dispatch-isolation verb — inspection surfaces must use inspect-dispatch-isolation, which never writes the sentinel`,
);
assert.ok(
src.includes('"$INSPECTED_ISOLATION" = "none"') || src.includes('`INSPECTED_ISOLATION` = `none`'),
`${wf}: must gate on ISOLATION = none, the only value that cannot honor worktrees`,
);
}
});
test('the isolation read is runtime-neutral — no per-runtime stamping rewrites it', () => {
// inspect-dispatch-isolation resolves the runtime internally and fail-closes,
// so unlike the #1521 runtime read it must survive every emit byte-identical.
// allow-test-rule: integration-test-input (#2486)
// The workflow source is fed to _applyRuntimeRewrites as real fixture input;
// the assertion is on the transformation's output.
for (const rt of NON_CLAUDE_RUNTIMES) {
for (const wf of RUNTIME_BRANCH_WORKFLOWS) {
const out = conversion._applyRuntimeRewrites(readWorkflow(wf), rt, `$HOME/.${rt}/`, true, undefined);
assert.ok(
out.includes(ISOLATION_LINE),
`${rt}/${wf}: the inspect-dispatch-isolation read must not be rewritten by per-runtime stamping`,
);
}
// The settings tri-state read must survive stamping too: if
// _stampNonClaudeRuntimeDefaults ever matched the bare (no-fallback)
// shape, absence would again collapse into an explicit false and the
// pre-selection rule would go dead on that runtime.
const settingsOut = conversion._applyRuntimeRewrites(readWorkflow('settings.md'), rt, `$HOME/.${rt}/`, true, undefined);
assert.ok(
settingsOut.includes('USE_WORKTREES_CURRENT=$(msd_run query config-get workflow.use_worktrees --raw 2>/dev/null)'),
`${rt}/settings.md: the bare tri-state worktrees read must survive per-runtime stamping byte-identical`,
);
}
});
test('claude emit of settings.md and health.md keeps the recommended Yes option unchanged', () => {
const settingsOut = conversion._applyRuntimeRewrites(readWorkflow('settings.md'), 'claude', '$HOME/.claude/', true, undefined);
assert.ok(
settingsOut.includes('{ label: "Yes (Recommended)", description: "Each parallel executor runs in its own worktree branch — no conflicts between agents." }'),
'claude/settings.md: the worktree-capable Worktrees question must be unchanged',
);
});
test('settings.md source carries the isolation-none branch: no enabling option, never persist true', () => {
const src = readWorkflow('settings.md');
assert.ok(
src.includes('**Conditional options — Worktrees (#2486):**'),
'settings.md: missing the conditional-options block for the Worktrees question',
);
// Round 4: the current-value read carries NO --default/fallback on purpose.
// _stampNonClaudeRuntimeDefaults rewrites the canonical fallback line to
// `--default false || echo "false"` on every non-Claude emit, which made
// key-absence indistinguishable from an explicit false — and the
// "pre-select Leave unchanged only when absent" rule dead there. The bare
// read signals absence as empty output and matches no stamp pattern.
assert.ok(
src.includes('USE_WORKTREES_CURRENT=$(msd_run query config-get workflow.use_worktrees --raw 2>/dev/null)'),
'settings.md: current worktrees value must use the bare tri-state read (empty = absent)',
);
assert.ok(
!src.includes(`USE_WORKTREES_CURRENT=$(msd_run query ${TRUE_WT_LINE})`),
'settings.md: the stampable fallback read collapses absent into false on non-Claude emits',
);
assert.ok(
src.includes('NEVER write `workflow.use_worktrees: true` from this workflow when the runtime declares no isolation primitive'),
'settings.md: missing the never-persist-true instruction for isolation-none runtimes',
);
assert.ok(
src.includes('{ label: "No (Recommended)", description: "Write use_worktrees: false.'),
'settings.md: isolation-none branch must recommend No',
);
assert.ok(
src.includes('{ label: "Leave unchanged", description: "Do not write the key.'),
'settings.md: isolation-none branch must offer leaving the key untouched for shared configs',
);
// Round 2: in the broken-inheritance case (explicit non-false value) the
// pre-selected default must be the repair, not "Leave unchanged".
// Round-5 review: this used to pin `Pre-select "Leave unchanged" only
// when the key is absent`. That assumed an absent key is already safe,
// which holds only on an emit that stamped the default to false —
// execute-phase/quick/diagnose read it as `|| echo "true"` otherwise. The
// recommended default must now be the explicit repair in every case.
assert.ok(
src.includes('Pre-select "No (Recommended)" in every case, including an absent key'),
'settings.md: the recommended default must be the explicit false — an absent key is safe only on a stamped emit',
);
assert.ok(
!/Pre-select "Leave unchanged" only when the key is absent/.test(src),
'settings.md: the old absent-key-is-safe pre-selection rule is falsified on un-stamped emits (#2486 round-5 review)',
);
assert.ok(
src.includes('when it is an explicit non-false value — the broken-inheritance case'),
'settings.md: the broken-inheritance case must pre-select the recommended repair',
);
});
test('health.md source carries the W025 isolation/worktrees compatibility check', () => {
const src = readWorkflow('health.md');
assert.ok(
src.includes('W025:'),
'health.md: missing the W025 diagnostic line',
);
assert.ok(
src.includes('Status: DEGRADED'),
'health.md: a config the execution workflows fail closed on must degrade the reported status',
);
// #2486 review Major 1: W025's correctness must NOT depend on
// `_stampNonClaudeRuntimeDefaults` having rewritten the read. A
// `|| echo "true"` fallback makes an ABSENT key look like an explicit
// `true`, so the check fires on a config that never set it — correct only
// on a stamped emit, wrong on the un-stamped source/Claude emit. Pin the
// stamp-independent shape settings.md already uses.
assert.ok(
src.includes('USE_WORKTREES=$(msd_run query config-get workflow.use_worktrees --raw 2>/dev/null)'),
'health.md: the W025 worktrees read must be bare — a --default/|| fallback collapses key-absent into explicit-true (#2486 Major 1)',
);
assert.ok(
!/workflow\.use_worktrees --raw 2>\/dev\/null \|\| echo/.test(src),
'health.md: the W025 read reintroduced a fallback, so it depends on install-time stamping again (#2486 Major 1)',
);
assert.ok(
src.includes('[ -n "$USE_WORKTREES" ]'),
'health.md: W025 must require the key to be PRESENT before warning that the config "sets" it',
);
});
// #2486 review Major 2: the coverage above is still text-matching. Execute
// the SHIPPED block so the predicate is proven by behavior — Major 1 would
// have been caught by any one of these cases.
test('W025 fires only on an explicit non-false key under isolation=none (behavioral, #2486 Major 2)', { skip: NO_BASH }, (t) => {
const scratch = createTempDir('msd-w025-');
t.after(() => cleanupDir(scratch));
const src = readFileNormalized(
path.join(__dirname, '..', 'msd-core', 'workflows', 'health.md'),
);
const block = bashBlockContaining(src, 'W025:');
assert.ok(block, 'health.md: no ```bash block containing the W025 diagnostic');
/** Run the shipped block with `msd_run` stubbed to the given answers. */
const fire = (isolation, worktreesOut) => {
const harness = [
'set -u',
'msd_run() {',
' case "$*" in',
` *inspect-dispatch-isolation*) printf '%s' ${JSON.stringify(isolation)} ;;`,
// Faithful to the real CLI: `config-get` EXITS NON-ZERO for an absent
// key, it does not print empty and succeed. A stub that succeeds here
// would let `|| echo "true"` be reintroduced and still pass — the
// fallback only triggers on failure (#2486 round-7 review, Major 4).
worktreesOut === ''
? ' *"config-get workflow.use_worktrees"*) return 1 ;;'
: ` *"config-get workflow.use_worktrees"*) printf '%s' ${JSON.stringify(worktreesOut)} ;;`,
" *) printf '' ;;",
' esac; }',
block,
].join('\n');
const scriptPath = path.join(scratch, `w025-${isolation}-${worktreesOut || 'absent'}.sh`);
fs.writeFileSync(scriptPath, harness);
const res = runHook(scriptPath, [], { interpreter: 'bash' });
assert.equal(
res.outcome, 'exited',
`W025 block did not complete cleanly: outcome=${res.outcome} ${res.stderr || ''}`,
);
assert.equal(res.exitCode, 0, `W025 block exited ${res.exitCode}: ${res.stderr}`);
return res.stdout.includes('W025:');
};
// The reviewer's exact repro: an isolation=none runtime, source (un-stamped) emit, cfg={}.
// The key is absent, so `config-get` prints nothing. This FIRED before.
assert.equal(
fire('none', ''),
false,
'W025 fired on an ABSENT use_worktrees key — the warning claims the config "sets" a non-false value, and it does not (#2486 Major 1 repro)',
);
// The defect W025 actually exists for.
assert.equal(
fire('none', 'true'),
true,
'W025 stayed quiet on an explicit true under isolation=none — that is the config execute-phase/quick fail closed on',
);
assert.equal(
fire('none', 'false'), false, 'W025 fired on an explicit false — nothing to repair',
);
// A host that CAN isolate is never the subject of this warning.
for (const iso of ['harness-worktree', 'orchestrator-worktree']) {
assert.equal(
fire(iso, 'true'),
false,
`W025 fired on ${iso}, which declares an isolation primitive — the gate is the declared capability, not the runtime name (#2584)`,
);
}
});
// #2486 round-9 review, Major 3: the source-text pins above assert only
// that ISOLATION_RESOLVED is *mentioned*, so flipping the shipped block's
// `ISOLATION_RESOLVED=true` to `false` left every one of them green. What
// has to be pinned is the BRANCH: a resolver that answered and a resolver
// that failed must produce different W025 text, because the whole point is
// to stop reporting a failed query as a capability verdict. This test
// drives the shipped block twice and fails on the mutation.
test('W025 distinguishes a declared none from an unresolvable query (behavioral, #2486 Major 3)', { skip: NO_BASH }, (t) => {
const scratch = createTempDir('msd-w025-provenance-');
t.after(() => cleanupDir(scratch));
const src = readFileNormalized(
path.join(__dirname, '..', 'msd-core', 'workflows', 'health.md'),
);
const block = bashBlockContaining(src, 'W025:');
assert.ok(block, 'health.md: no ```bash block containing the W025 diagnostic');
// `resolves` false = the inspect call exits non-zero, the real shape of a
// shim that cannot resolve. The worktrees key is an explicit true in both
// runs, so the ONLY difference is whether the capability query answered.
const runBlock = (resolves) => {
const harness = [
'set -u',
'msd_run() {',
' case "$*" in',
resolves
? " *inspect-dispatch-isolation*) printf 'none' ;;"
: ' *inspect-dispatch-isolation*) return 1 ;;',
" *\"config-get workflow.use_worktrees\"*) printf 'true' ;;",
" *) printf '' ;;",
' esac; }',
block,
].join('\n');
const scriptPath = path.join(scratch, `w025-resolves-${resolves}.sh`);
fs.writeFileSync(scriptPath, harness);
const res = runHook(scriptPath, [], { interpreter: 'bash' });
assert.equal(res.outcome, 'exited', `block did not complete: ${res.stderr || ''}`);
assert.equal(res.exitCode, 0, `block exited ${res.exitCode}: ${res.stderr}`);
return res.stdout;
};
const resolved = runBlock(true);
const unresolved = runBlock(false);
// Both must warn — an explicit true is worth reporting either way.
assert.match(resolved, /W025:/, 'a resolved none with an explicit true must still warn');
assert.match(unresolved, /W025:/, 'an unresolvable capability with an explicit true must still warn');
// ...but they must not say the same thing.
assert.notEqual(
resolved.trim(),
unresolved.trim(),
'W025 emitted identical text whether or not the capability resolved — that is the Major 3 conflation',
);
assert.match(
unresolved,
/could not resolve/i,
'the unresolved branch must report that the query failed, not assert a capability verdict',
);
assert.doesNotMatch(
unresolved,
/declares no executor-isolation primitive|has no usable executor-isolation primitive/i,
'the unresolved branch must NOT claim the runtime has no primitive — nothing established that',
);
assert.doesNotMatch(
resolved,
/could not resolve/i,
'the resolved branch must state the capability finding, not a resolution failure',
);
// Round-3 review: asserting only "differs and omits the other phrase"
// would stay green if the resolved text were replaced with anything at
// all. Pin what it must positively say — the capability finding and the
// consequence a user acts on.
assert.match(
resolved,
/no usable executor-isolation primitive/i,
'the resolved branch must state the capability finding it actually reached',
);
assert.match(
resolved,
/fail closed/i,
'the resolved branch must state the consequence — that is what makes W025 actionable',
);
assert.match(
resolved,
/use_worktrees/,
'the resolved branch must name the offending key',
);
// Both branches must still route the user to the same repair.
for (const [name, out] of [['resolved', resolved], ['unresolved', unresolved]]) {
assert.match(out, /(?:\/msd[:-]settings|\$msd-settings)\b/, `${name}: W025 must name the repair command`);
}
});
test('W025 is documented consistently across health.md and both config references', () => {
// The rename W020 -> W025 landed in health.md only; the two docs kept
// saying W020, which collides with a code src/verify.cts already emits.
// #3309: health.md's generated `<error_codes>` table now carries a real,
// UNRELATED W020 row of its own (`Worktree health scan degraded` —
// git-worktree-list-inventory failure, #3384/#3652 territory), whose
// description legitimately contains the bare word "worktree" right next
// to "W020". A bare `worktree` probe can no longer tell that apart from
// the stale isolation-check naming this guard exists for, so it narrows
// to the literal config key (`use_worktrees`) the isolation warning is
// actually about — the real W020 row's text never mentions that key.
for (const rel of ['msd-core/workflows/health.md', 'docs/CONFIGURATION.md', 'msd-core/references/planning-config.md']) {
const text = fs.readFileSync(path.join(__dirname, '..', rel), 'utf8');
assert.ok(text.includes('W025'), `${rel}: must document the worktrees warning as W025`);
assert.ok(
!/\bW020\b[^)]{0,120}use_worktrees/i.test(text),
`${rel}: stale W020 reference for the worktrees warning`,
);
}
});
// Round 4 note: an earlier revision carried a W025-vs-src/verify.cts
// namespace-collision test here that read verify.cts as raw text — the
// source-grep shape RULESET.TESTS.delete-bad-tests says to delete, not
// exempt. Deleted without a behavioral replacement: verify.cts exposes no
// enumerable W-code registry to assert against, and building one is a
// shared-module refactor outside this fix. The namespace claim lives as
// guidance in health.md's error-codes note instead of as a fake test.
test('the health.md error-codes table is not broken by the namespace note', () => {
// The note was inserted BETWEEN two rows, which terminates the GFM table
// and orphans the trailing row(s) into literal pipe-delimited text.
// #3309: the hand-written "Note: the `W0NN` warning-code namespace..."
// paragraph (and the `W025` row it sat under) is gone — `gen-health-docs.cjs`
// now GENERATES this table from `RULES`, and deliberately excludes W025
// (a workflow-layer diagnostic emitted by this file's own bash step, never
// by `cmdValidateHealth`/`RULES` — see the generator's module header and its
// `FOOTNOTE_PARAGRAPH`, which still names W025 for cross-reference). The
// table's actual last row is now I010, not I001, and the footnote's own
// opening sentence replaces the old namespace note. The hazard this test
// guards — a footnote landing mid-table — still applies to the new content.
const src = readWorkflow('health.md');
const i001 = src.indexOf('| I001 |');
const i010 = src.indexOf('| I010 |');
const note = src.indexOf('Note: this table is **generated**');
assert.ok(i001 > -1 && i010 > -1 && note > -1, 'health.md: expected I001, I010 and the generated-table note');
assert.ok(i010 > i001, 'health.md: I010 row must follow the I001 row');
assert.ok(
note > i010,
'health.md: the generated-table note must come AFTER the final table row — placing it between rows ends the table and orphans trailing rows',
);
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-2876-skill-frontmatter-quote.test.cjs — consolidation epic #1969 (B8 #1977)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-2876-skill-frontmatter-quote (consolidation epic #1969 B8 #1977)", () => {
/**
* Bug #2876: SKILL.md frontmatter parse failure when `description` begins
* with a YAML flow indicator like `[BETA]`.
*
* description: [BETA] Offload plan phase to Claude Code's ultraplan…
*
* YAML 1.2 treats a leading `[` as the start of a flow sequence, so any
* downstream parser (gh-copilot, JetBrains' kit, etc.) fails with
* "Unexpected scalar at node end". The Antigravity
* skill+agent converters in `bin/install.js` re-emit the description
* unquoted; the Claude variant `yamlQuote(...)`s it. Bring the others
* in line so any value is round-trip-safe regardless of leading char.
*
* The test is structural: it parses each emitted frontmatter into lines
* and asserts the `description` value is a quoted YAML scalar (double or
* single quoted) when the source description starts with a flow indicator.
* It does not regex the bytes for substrings.
*/
'use strict';
process.env.MSD_TEST_MODE = '1';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const REPO_ROOT = path.join(__dirname, '..');
const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8'));
const installPath = path.resolve(REPO_ROOT, pkg.bin['msd-core']);
const install = require(installPath);
// Build a minimal Claude command source whose description starts with the
// reporter's exact flow-indicator prefix. Apostrophe in the body forces
// any naive single-quoting to also escape correctly — the canonical
// safe form is `JSON.stringify(...)` (used by yamlQuote).
const REPORTER_DESCRIPTION =
"[BETA] Offload plan phase to Claude Code's ultraplan cloud — drafts remotely while terminal stays free, review in browser with inline comments, import back via /msd-import. Claude Code only.";
// Use unquoted description in the source frontmatter — that's exactly the
// shape that ships in commands/msd/*.md when authors paste a description
// without quoting it (see commands/msd/ultraplan-phase.md). The bug is
// triggered when the converter re-emits this same value to the destination
// runtime without quoting. `extractFrontmatterField` strips a single outer
// quote pair but does not unescape internal characters, so quoting the
// fixture input would actually mask the bug.
function buildClaudeCommand(description) {
return [
'---',
'name: msd:ultraplan-phase',
`description: ${description}`,
'argument-hint: "[phase-number]"',
'allowed-tools:',
' - Read',
' - Bash',
'---',
'',
'# body',
'',
].join('\n');
}
function buildClaudeAgent(description) {
return [
'---',
'name: msd-extract-learnings',
`description: ${description}`,
'tools: Read, Bash',
'---',
'',
'# body',
'',
].join('\n');
}
function extractFrontmatter(content) {
// Leading delimiter is `---\n`; closing is the next standalone `---`
// on its own line. Tests parse line-structurally so the assertion
// doesn't drift on whitespace/order changes (per project test-rigor).
assert.ok(content.startsWith('---'), `output must begin with frontmatter, got: ${content.slice(0, 40)}`);
const lines = content.split('\n');
let openIdx = -1;
let closeIdx = -1;
for (let i = 0; i < lines.length; i += 1) {
if (lines[i] === '---') {
if (openIdx === -1) openIdx = i;
else { closeIdx = i; break; }
}
}
assert.ok(openIdx !== -1 && closeIdx !== -1, `output must have a closed frontmatter block, got:\n${content}`);
return lines.slice(openIdx + 1, closeIdx);
}
function findDescriptionLine(frontmatterLines) {
for (const line of frontmatterLines) {
if (line.startsWith('description:')) return line;
}
assert.fail(`no description line found in frontmatter:\n${frontmatterLines.join('\n')}`);
return ''; // unreachable
}
function isQuotedYamlScalar(valueText) {
// YAML safe-quoted scalar: starts with `"` and ends with `"`, OR
// starts with `'` and ends with `'`. This is what `yamlQuote()`
// (JSON.stringify) and the Claude variant of these converters emit.
const trimmed = valueText.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return true;
if (trimmed.startsWith("'") && trimmed.endsWith("'")) return true;
return false;
}
function parseQuotedYamlValue(valueText) {
const trimmed = valueText.trim();
if (trimmed.startsWith('"')) return JSON.parse(trimmed);
if (trimmed.startsWith("'")) return trimmed.slice(1, -1).replace(/''/g, "'");
return trimmed;
}
function assertDescriptionRoundTrips(emitted, expected, label) {
const fmLines = extractFrontmatter(emitted);
const descLine = findDescriptionLine(fmLines);
const valueText = descLine.slice('description:'.length);
assert.ok(
isQuotedYamlScalar(valueText),
`(${label}) description must be a quoted YAML scalar (parser-safe for leading flow indicators). Got line: ${descLine}`,
);
assert.strictEqual(
parseQuotedYamlValue(valueText),
expected,
`(${label}) description must round-trip through YAML quoting unchanged.`,
);
}
const COMMAND_CONVERTERS = [
{ label: 'convertClaudeCommandToAntigravitySkill', fn: (src) => install.convertClaudeCommandToAntigravitySkill(src, 'msd-ultraplan-phase') },
];
const AGENT_CONVERTERS = [
{ label: 'convertClaudeAgentToAntigravityAgent', fn: (src) => install.convertClaudeAgentToAntigravityAgent(src) },
];
// A grab-bag of leading characters that all break unquoted YAML scalar
// parsing per YAML 1.2 §7.3.3 / §6.9. The reporter's case is `[`; the
// rest defend against neighbouring drift.
const FLOW_HOSTILE_PREFIXES = ['[', '{', '*', '&', '!', '|', '>', '%', '@', '`'];
// For the reporter case we assert only the quoting requirement, not
// byte-equality of the round-tripped value.
function assertDescriptionIsQuoted(emitted, label) {
const fmLines = extractFrontmatter(emitted);
const descLine = findDescriptionLine(fmLines);
const valueText = descLine.slice('description:'.length);
assert.ok(
isQuotedYamlScalar(valueText),
`(${label}) description must be a quoted YAML scalar (parser-safe for leading flow indicators). Got line: ${descLine}`,
);
}
describe('bug-2876: skill+agent converters emit YAML-quoted description', () => {
for (const { label, fn } of COMMAND_CONVERTERS) {
test(`${label}: reporter's "[BETA] ..." description is quoted`, () => {
const out = fn(buildClaudeCommand(REPORTER_DESCRIPTION));
assertDescriptionIsQuoted(out, label);
});
for (const prefix of FLOW_HOSTILE_PREFIXES) {
test(`${label}: leading ${JSON.stringify(prefix)} is quoted`, () => {
// Avoid leading/trailing `'` or `"` in the payload — `extractFrontmatterField`
// strips a single outer quote char of either kind regardless of whether
// the value was actually quoted, which would obscure the round-trip
// assertion. Pre-existing behavior, out of scope for #2876.
const desc = `${prefix} edge-case payload — flow indicator at start`;
const out = fn(buildClaudeCommand(desc));
assertDescriptionRoundTrips(out, desc, `${label} prefix=${prefix}`);
});
}
}
for (const { label, fn } of AGENT_CONVERTERS) {
test(`${label}: reporter-shape "[BETA] ..." description is quoted`, () => {
const out = fn(buildClaudeAgent(REPORTER_DESCRIPTION));
assertDescriptionIsQuoted(out, label);
});
}
});
});
}
// ─── #3706: OpenCode subagent effort (variant) + frontmatter escaping ────────
//
// `query resolve-execution` reported an effort (high/xhigh) that never reached
// OpenCode: the bake wrote `model:` and nothing else, so every subagent ran at
// whatever opencode.jsonc defaults the model to. The effort-side twin of #3705.
//
// bin/install.js has its own copy of these converters, but no `isAgent: true`
// call site: its agents install path runs through the compiled bin/lib module,
// not through its own copy. The live copy in src/runtime-artifact-conversion.cts
// (compiled to msd-core/bin/lib/runtime-artifact-conversion.cjs) is therefore
// the one the bake actually uses, and the one under test here.
const liveConversion = require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
const OPENCODE_CONVERTERS = [['src (live bake)', liveConversion.convertClaudeToOpencodeFrontmatter]];
describe('#3706: OpenCode agent variant (resolved effort)', () => {
const AGENT = ['---', 'name: msd-executor', 'description: x', 'model: sonnet', '---', '', 'Body.'].join('\n');
const hasKey = (out, key) => new RegExp(`^${key}:`, 'm').test(out);
for (const [label, convert] of OPENCODE_CONVERTERS) {
test(`[${label}] emits variant alongside model`, () => {
const out = convert(AGENT, {
isAgent: true, modelOverride: 'synthetic/hf:zai-org/GLM-5.2', variant: 'high',
});
assert.match(out, /^model: synthetic\/hf:zai-org\/GLM-5\.2$/m);
assert.match(out, /^variant: high$/m);
});
test(`[${label}] carries whatever level was resolved, not a fixed one`, () => {
assert.match(convert(AGENT, { isAgent: true, variant: 'xhigh' }), /^variant: xhigh$/m);
});
test(`[${label}] omits the key entirely when no variant is supplied`, () => {
// The control that keeps every existing install byte-identical.
const out = convert(AGENT, { isAgent: true, modelOverride: 'M' });
assert.ok(!hasKey(out, 'variant'), `expected no variant key, got:\n${out}`);
assert.match(out, /^model: M$/m, 'the model side is unaffected');
});
test(`[${label}] commands get neither model nor variant`, () => {
const out = convert(AGENT, { isAgent: false, modelOverride: 'M', variant: 'high' });
assert.ok(!hasKey(out, 'variant'));
assert.ok(!hasKey(out, 'model'));
});
test(`[${label}] adding a variant perturbs no other line`, () => {
const withV = convert(AGENT, { isAgent: true, modelOverride: 'M', variant: 'high' });
const without = convert(AGENT, { isAgent: true, modelOverride: 'M' });
assert.match(withV, /^mode: subagent$/m);
assert.deepEqual(
withV.split('\n').filter((l) => !/^variant:/.test(l)),
without.split('\n'),
);
});
}
});
// ─── #3706: config-supplied values cannot inject frontmatter keys ────────────
//
// `model:` and now `variant:` both interpolate values read from
// .planning/config.json / ~/.msd/defaults.json. Raw interpolation let a value
// containing a newline inject additional TOP-LEVEL keys — proven by execution
// during the #3705 security review. The sink predates #3706; this change adds a
// SECOND write to it, so it is closed here rather than doubled.
describe('#3706: frontmatter values are escaped, not interpolated raw', () => {
const AGENT = ['---', 'name: msd-executor', 'description: x', '---', '', 'Body.'].join('\n');
for (const [label, convert] of OPENCODE_CONVERTERS) {
test(`[${label}] a newline-bearing model value cannot add top-level keys`, () => {
const out = convert(AGENT, {
isAgent: true, modelOverride: 'sonnet\ntools: ["*"]\npermission: bypass',
});
assert.ok(!/^tools:/m.test(out), `injected a tools key:\n${out}`);
assert.ok(!/^permission: bypass$/m.test(out), `injected a permission key:\n${out}`);
assert.match(out, /^model: "/m, 'the value is quoted');
});
test(`[${label}] a newline-bearing variant value cannot add top-level keys`, () => {
const out = convert(AGENT, { isAgent: true, variant: 'high\nevil: yes' });
assert.ok(!/^evil:/m.test(out), `injected a key:\n${out}`);
assert.match(out, /^variant: "/m);
});
}
});
// ─── #3706: values that look plain but do not round-trip ────────────────────
//
// Matching /^[A-Za-z0-9._:\/@+-]+$/ is not the same question as "does YAML read
// this back as the exact string that went in". Each row below is wrong when
// emitted bare; the last is the control that keeps real installs unchanged.
describe('#3706: YAML-ambiguous scalars are quoted, not emitted bare', () => {
const AGENT = ['---', 'name: msd-executor', 'description: x', '---', '', 'Body.'].join('\n');
const QUOTED = [
// A PARSE ERROR bare, not merely ambiguous: '@' is a YAML reserved
// indicator and may not open a plain scalar, so the whole agent file
// becomes unreadable.
'@org/model',
// A trailing ':' reads as a nested mapping key — "bad indentation of a
// mapping entry" kills the frontmatter.
'foo:',
// YAML 1.1 resolves these to booleans/null, so a variant named 'no'
// arrives as `false` and matches no entry in the user's variants map.
'no', 'yes', 'true', 'off', 'y', 'n', 'null',
// '12:30' resolves to the integer 750; ':' is legal mid-identifier here,
// so the form is reachable rather than contrived.
'12:30', '0755', '1.5', '0x1f',
// #3706 hardening: '(.+?)' in the value regex needs one character before
// this fix, so an empty/whitespace-only value read as "key absent" rather
// than "key present, value empty" — these are the YAML-ambiguous classes
// that predicate missed once presence and value became distinct questions.
'~', '.inf', '.Inf', '.nan', '+1', '-1', '-0', '+1.5', '.5', '0x1F',
'2026-08-25', '2026-08-25T10:00:00Z', 'a: b', 'a #b',
];
// The whole point of the predicate: no churn for anybody's existing files.
const BARE = ['sonnet', 'synthetic/hf:zai-org/GLM-5.2', 'gpt-5.6-luna', 'claude-opus-5', 'x-', 'a.b', 'GLM-5.2'];
for (const [label, convert] of OPENCODE_CONVERTERS) {
const modelLine = (v) =>
(convert(AGENT, { isAgent: true, modelOverride: v })
.split('\n').find((l) => l.startsWith('model:')) ?? '');
for (const v of QUOTED) {
test(`[${label}] quotes ${JSON.stringify(v)}`, () => {
assert.equal(modelLine(v), `model: "${v}"`);
});
}
for (const v of BARE) {
test(`[${label}] leaves ${JSON.stringify(v)} bare`, () => {
assert.equal(modelLine(v), `model: ${v}`);
});
}
test(`[${label}] the same predicate governs variant, not just model`, () => {
assert.match(convert(AGENT, { isAgent: true, variant: 'no' }), /^variant: "no"$/m);
});
}
});
// ─── #3706: the threading seam — where the bug actually was ─────────────────
//
// Every test above exercises a converter directly. #3706 broke one layer up, in
// runtime-artifact-layout's convertedAgentsKind: the converter had no variant to
// write because nothing resolved or passed one. A converter-only suite goes green
// while the value still never arrives, so the resolve→render chain is pinned here.
describe('#3706: install-time effort resolves and renders for OpenCode', () => {
const effortResolver = require('../msd-core/bin/lib/install-effort-resolver.cjs');
const catalog = require('../msd-core/bin/lib/model-catalog.cjs');
// Exactly the composition the layout performs per agent.
const thread = (effortCfg, agentName) => {
const universal = effortCfg ? effortResolver.resolveInstallTimeEffort(effortCfg, agentName) : null;
return universal ? catalog.clampEffortForHost('opencode', universal) : null;
};
test('no effort config at all yields no variant', () => {
// The gate. resolveInstallTimeEffort ALWAYS returns a level ('high' from the
// catalog default) even for a null config, so gating on its return value
// would stamp `variant:` into every existing OpenCode install.
assert.equal(effortResolver.resolveInstallTimeEffort(null, 'msd-executor'), 'high');
assert.equal(thread(null, 'msd-executor'), null);
});
test('a configured agent override reaches the emitted value', () => {
assert.equal(thread({ agent_overrides: { 'msd-executor': 'xhigh' } }, 'msd-executor'), 'xhigh');
});
test('a configured default reaches an agent with no catalog tier', () => {
// effort.default is only consulted for agents the tier ladder does not
// answer for; see the tiered-agent test below for the other half.
assert.equal(thread({ default: 'low' }, 'not-a-catalog-agent'), 'low');
});
test("'inherit' is never emitted as a literal", () => {
// #3533 (10d): inherit means "omit the key and follow the host default" and
// is not a wire level on ANY runtime. It IS a member of EFFORT_SET, so the
// resolver returns it verbatim — the declared render seam is what drops it.
// Writing `variant: inherit` would name a variant that cannot resolve.
for (const cfg of [
{ agent_overrides: { 'msd-executor': 'inherit' } },
{ routing_tier_defaults: { light: 'inherit', standard: 'inherit', heavy: 'inherit' } },
]) {
assert.equal(effortResolver.resolveInstallTimeEffort(cfg, 'msd-executor'), 'inherit');
assert.equal(thread(cfg, 'msd-executor'), null, `should omit for ${JSON.stringify(cfg)}`);
}
});
test('a bare effort.default does NOT reach a tiered agent', () => {
// Not obvious, and it is why the `inherit` case above goes through
// agent_overrides instead: for an agent WITH a catalog tier the manifest
// tier ladder (#3531) answers before effort.default is ever consulted, so
// `default: 'inherit'` leaves a tiered agent at its tier value. Pinned
// because a test written against `default` alone would assert nothing.
assert.equal(effortResolver.resolveInstallTimeEffort({ default: 'inherit' }, 'msd-executor'), 'high');
assert.equal(thread({ default: 'inherit' }, 'msd-executor'), 'high');
});
test('a level OpenCode does not accept is dropped, not emitted', () => {
// 'ultra' is a real EFFORT_SET member that OpenCode's declared supported set
// does not contain. Omitting beats naming a variant that cannot resolve.
assert.equal(catalog.renderEffortArgv('opencode', 'ultra', 'argv').value, null);
});
test('the full universal ladder OpenCode does accept passes through', () => {
for (const lvl of ['minimal', 'low', 'medium', 'high', 'xhigh', 'max']) {
assert.equal(catalog.renderEffortArgv('opencode', lvl, 'argv').value, lvl);
}
});
test('an empty model override omits the key rather than writing an empty value', () => {
// Found by the property below, which originally asserted a round-trip for
// every generated string and failed on "". An empty override is falsy, so
// no `model:` line is written at all — that is the #2256/J7 contract (omit
// the key, never `model: ""`), so the property is scoped to non-empty
// values and the empty case is pinned here instead of being generated away.
const out = liveConversion.convertClaudeToOpencodeFrontmatter(
['---', 'name: x', 'description: y', '---', '', 'Body.'].join('\n'),
{ isAgent: true, modelOverride: '' },
);
assert.ok(!/^model:/m.test(out), `expected no model key, got:\n${out}`);
});
test('every emitted model line parses back to the exact value', () => {
// The real contract, stated directly: whatever a user puts in config, the
// generated frontmatter must read back as that same string. A per-character
// rule list is only a means to this end, and this is what catches the case
// nobody thought to enumerate.
const fc = require('fast-check');
const yaml = require('js-yaml');
const chars = ':@-_./"\\ \n\t#&*!|>%`{}[],?~+abcXY019';
fc.assert(
fc.property(
fc.string({ unit: fc.constantFrom(...chars.split('')), minLength: 1 }),
(value) => {
const out = liveConversion.convertClaudeToOpencodeFrontmatter(
['---', 'name: x', 'description: y', '---', '', 'Body.'].join('\n'),
{ isAgent: true, modelOverride: value },
);
const fmBody = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(out)[1];
return yaml.load(fmBody).model === value;
},
),
{ numRuns: 2000 },
);
});
});
// ─── #3706: the layout seam actually threads the variant ─────────────────────
//
// Every test above calls a converter directly. Direct-converter tests all
// still pass if a future edit drops `variant` from the `rawConverter(...)`
// call inside runtime-artifact-layout.cts's convertedAgentsKind, or flips its
// `converterName ===` gate — this drives the REAL staging path
// (resolveRuntimeArtifactLayout -> the OpenCode 'agents' kind's stage()) so
// that regression cannot hide behind converter-only coverage.
describe('#3706: the layout seam actually threads the variant', () => {
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup } = require('./helpers.cjs');
const { resolveRuntimeArtifactLayout } = require('../msd-core/bin/lib/runtime-artifact-layout.cjs');
// Fixture layout mirrors the .msd-source marker convention
// (findAgentsSourceRoot): the marker at <configDir>/.msd-source points to
// <configDir>/commands/msd, and agents/ is resolved as its sibling.
function makeConfigDirFixture(agentContent) {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3706-seam-configdir-'));
const commandsDir = path.join(configDir, 'commands', 'msd');
const agentsDir = path.join(configDir, 'agents');
fs.mkdirSync(commandsDir, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(configDir, '.msd-source'), commandsDir + '\n', 'utf8');
fs.writeFileSync(path.join(commandsDir, 'msd-help.md'), '# complete marker provider\n', 'utf8');
fs.writeFileSync(path.join(agentsDir, 'msd-executor.md'), agentContent, 'utf8');
return configDir;
}
function makeProjectRootWithEffort(effort) {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3706-seam-project-'));
fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(projectRoot, '.planning', 'config.json'),
JSON.stringify({ effort }),
'utf8',
);
return projectRoot;
}
// resolveInstallTimeEffort also merges ~/.msd/defaults.json (os.homedir()),
// so HOME/USERPROFILE are pinned to an empty temp dir for the duration of
// each stage() call to keep the assertion hermetic against the real
// developer machine's home directory.
function stageWithFakeHome(agentKind, resolvedProfile, agentCtx) {
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3706-seam-home-'));
const realHome = process.env.HOME;
const realUserProfile = process.env.USERPROFILE;
process.env.HOME = fakeHome;
process.env.USERPROFILE = fakeHome;
try {
return agentKind.stage(resolvedProfile, agentCtx);
} finally {
// On POSIX, USERPROFILE (and potentially HOME) is unset before this
// helper runs — `process.env.X = undefined` would coerce to the
// literal string "undefined" and leak that into the environment for
// the rest of the test process. Restore by deletion when the saved
// value was genuinely absent.
if (realHome === undefined) delete process.env.HOME;
else process.env.HOME = realHome;
if (realUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = realUserProfile;
cleanup(fakeHome);
}
}
const AGENT_SOURCE = ['---', 'name: msd-executor', 'description: x', '---', '', 'Body.'].join('\n');
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
test('a configured effort reaches the real OpenCode agents kind staging output', () => {
const configDir = makeConfigDirFixture(AGENT_SOURCE);
const projectRoot = makeProjectRootWithEffort({ agent_overrides: { 'msd-executor': 'xhigh' } });
try {
const layout = resolveRuntimeArtifactLayout('opencode', configDir, 'global');
const agentKind = layout.kinds.find((k) => k.kind === 'agents');
assert.ok(agentKind, 'opencode global layout must include an agents kind');
const agentCtx = { runtime: 'opencode', pathPrefix: '', attribution: null, targetDir: projectRoot };
const stagedDir = stageWithFakeHome(agentKind, resolvedProfile, agentCtx);
const stagedContent = fs.readFileSync(path.join(stagedDir, 'msd-executor.md'), 'utf8');
assert.match(stagedContent, /^variant: xhigh$/m, `expected variant: xhigh in staged output:\n${stagedContent}`);
} finally {
cleanup(configDir);
cleanup(projectRoot);
}
});
test('no effort config at all reaches the real staging output with no variant key', () => {
const configDir = makeConfigDirFixture(AGENT_SOURCE);
const noConfigRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3706-seam-noconfig-'));
try {
const layout = resolveRuntimeArtifactLayout('opencode', configDir, 'global');
const agentKind = layout.kinds.find((k) => k.kind === 'agents');
const agentCtx = { runtime: 'opencode', pathPrefix: '', attribution: null, targetDir: noConfigRoot };
const stagedDir = stageWithFakeHome(agentKind, resolvedProfile, agentCtx);
const stagedContent = fs.readFileSync(path.join(stagedDir, 'msd-executor.md'), 'utf8');
assert.doesNotMatch(stagedContent, /^variant:/m, `expected no variant key in staged output:\n${stagedContent}`);
} finally {
cleanup(configDir);
cleanup(noConfigRoot);
}
});
});
// ── #4705 — Antigravity agent tools: native names, YAML sequence ─────────────
// The converter emitted Gemini CLI tool names as a comma-separated scalar;
// Antigravity's documented subagent contract (antigravity.google/docs/subagents)
// wants a YAML sequence of native tool names (view_file, grep_search,
// run_command, replace_file_content are the documented examples). A scalar or a
// wrong-vocabulary grant can hang the subagent per Antigravity's own warning.
describe('#4705 — Antigravity agent tools are native names in a YAML sequence', () => {
const input = `---
name: example-reviewer
description: Inspect source files
tools: Read, Grep, Bash
---
Inspect source files.`;
test('emits the documented native tools as a YAML sequence (#4705)', () => {
const result = convertClaudeAgentToAntigravityAgent(input);
assert.match(result, /tools:\n- view_file\n- grep_search\n- run_command\n/,
'tools must be a YAML sequence of Antigravity-native names (view_file, grep_search, run_command)');
assert.doesNotMatch(result, /tools:.*,/m, 'no comma-separated scalar may survive');
});
test('Edit maps to the documented replace_file_content (#4705)', () => {
const result = convertClaudeAgentToAntigravityAgent(
`---\nname: e\ndescription: d\ntools: Edit\n---\n\nbody`);
assert.match(result, /- replace_file_content/, 'Edit -> replace_file_content (documented native name)');
assert.ok(!/- replace\b/.test(result), 'the Gemini CLI name replace must not survive');
});
test('unmapped tools keep their lowercase grant (no silent restriction drop) (#4705)', () => {
const result = convertClaudeAgentToAntigravityAgent(
`---\nname: e\ndescription: d\ntools: Glob, WebFetch\n---\n\nbody`);
assert.match(result, /- glob/, 'undocumented tools keep their mapped/lowercase grant');
assert.match(result, /- web_fetch/, 'WebFetch stays granted');
});
});