Files
msd-core/tests/runtime-artifact-layout-trigger-surface.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

454 lines
23 KiB
JavaScript

'use strict';
/**
* Failing-first suite for `resolveTriggerSurface` (#2871 Phase 2).
*
* `resolveTriggerSurface` does NOT exist yet in src/runtime-artifact-layout.cts
* (nor in its built msd-core/bin/lib/runtime-artifact-layout.cjs counterpart).
* Every test below is expected to FAIL until it lands — see
* .msd/phase/feat-2871-trigger-resolution/50-test-matrix.md (rows 1-23) and
* .msd/phase/feat-2871-trigger-resolution/40-design.md for the contract this
* suite locks.
*
* Idiom: per-runtime `describe` + explicit field assertions, matching the
* other four runtime-artifact-layout*.test.cjs files. No table-driven format.
*
* ── Stem-injection shape (design call made by this suite) ──────────────────
* `resolveTriggerSurface` is pure — no filesystem, no `configDir`. Stems are
* injected via `opts`:
*
* opts = {
* stems: string[], // source command/skill stems
* // present for this call, shared
* // across every trigger-bearing
* // kind entry (mirrors ResolvedProfile's
* // flat stem membership at staging
* // time — see install-profiles.cts).
* routerStems?: string[], // subset of `stems` that are
* // namespace routers (nested-router
* // runtimes only — #69).
* childToRouters?: Record<string, string[]>, // concrete stem -> owning
* // router stem(s); mirrors
* // buildNamespaceBundleMap's
* // childToRouters shape.
* registry?: { runtimes: {...} }, // full registry override —
* // the SAME seam
* // resolveRuntimeArtifactLayoutFromRegistry
* // already exposes in this file. Lets a
* // synthetic descriptor (row 12's
* // namespacedByDir fixture, row 22's
* // reordered triggerPrecedence) be
* // exercised without touching the real
* // capability-registry.
* }
*
* destPath (also a design call, since no in-tree fixture exercises row 12
* today): `${destSubpath}/${prefix}${stem}` for skills (the skill directory);
* `${destSubpath}/${prefix}${stem}.md` for a flat commands entry; and, when
* `_copyStaged`'s namespacedByDir branch applies (destSubpath's basename ===
* prefix minus its trailing '-'), `${destSubpath}/${stem}.md` — bare, no
* prefix on the filename, matching _copyStaged's actual write (row 12). The
* `trigger` string itself is ALWAYS `${prefix}${stem}` regardless of branch —
* it is what the user types, not a filesystem detail.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const runtimeArtifactLayout = require('../msd-core/bin/lib/runtime-artifact-layout.cjs');
const { resolveTriggerSurface } = runtimeArtifactLayout;
const capabilityRegistry = require('../msd-core/bin/lib/capability-registry.cjs');
const capValidator = require('../msd-core/bin/lib/capability-validator.cjs');
const REPO_ROOT = path.join(__dirname, '..');
/** A real shipped capability.json, parsed fresh (fixture-provenance rule #2371 —
* see tests/effort-surface-axis.test.cjs's shippedDescriptorWithout for the
* precedent this mirrors: a hand-built descriptor only ever encodes the
* author's mental model, which is how required-axis defects reach the runner). */
function shippedCap(runtimeId) {
return JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'capabilities', runtimeId, 'capability.json'), 'utf8'),
);
}
/** A real shipped capability.json with one `runtime` field genuinely deleted —
* mirrors tests/effort-surface-axis.test.cjs's shippedDescriptorWithout idiom
* (fixture-provenance rule #2371), adapted for a top-level `runtime.<field>`
* rather than a nested `runtime.hostIntegration.<axis>`. */
function shippedCapWithout(runtimeId, field) {
const cap = shippedCap(runtimeId);
delete cap.runtime[field];
return cap;
}
const VALID_KINDS = new Set(['commands', 'skills']);
const VALID_SCOPES = new Set(['global', 'local']);
const VALID_REGISTRATIONS = new Set(['direct', 'via-router']);
/** Structural + referential shape-check for a resolveTriggerSurface() result. */
function assertValidSurfaceArray(surfaces, label) {
assert.ok(Array.isArray(surfaces), `${label}: must return an array`);
for (const s of surfaces) {
assert.strictEqual(typeof s.trigger, 'string', `${label}: trigger must be a string`);
assert.ok(s.trigger.length > 0, `${label}: trigger must be non-empty`);
assert.ok(VALID_KINDS.has(s.kind), `${label}: kind must be commands|skills, got ${s.kind}`);
assert.ok(VALID_SCOPES.has(s.scope), `${label}: scope must be global|local, got ${s.scope}`);
assert.strictEqual(typeof s.destPath, 'string', `${label}: destPath must be a string`);
assert.ok(s.destPath.length > 0, `${label}: destPath must be non-empty`);
assert.ok(VALID_REGISTRATIONS.has(s.registration), `${label}: registration must be direct|via-router, got ${s.registration}`);
if (s.registration === 'via-router') {
assert.strictEqual(typeof s.routerTrigger, 'string', `${label}: via-router entries must name routerTrigger`);
} else {
assert.strictEqual(s.routerTrigger, null, `${label}: direct entries must have routerTrigger === null`);
}
if (s.shadowedBy !== null) {
assert.ok(VALID_KINDS.has(s.shadowedBy.kind), `${label}: shadowedBy.kind invalid`);
assert.ok(VALID_SCOPES.has(s.shadowedBy.scope), `${label}: shadowedBy.scope invalid`);
const sibling = surfaces.find(
(o) => o !== s && o.trigger === s.trigger && o.kind === s.shadowedBy.kind && o.scope === s.shadowedBy.scope,
);
assert.ok(sibling, `${label}: shadowedBy must name a real sibling entry for trigger ${s.trigger}`);
assert.strictEqual(sibling.shadowedBy, null, `${label}: the named shadowedBy sibling must itself be a winner`);
}
}
}
// ─── Behavior table (matrix rows 1-15) ──────────────────────────────────────
describe('resolveTriggerSurface — claude', () => {
test('claude reports every local command trigger as shadowed by the global skill', () => {
const stems = ['plan-phase', 'help'];
const result = resolveTriggerSurface('claude', ['global', 'local'], { stems });
assert.strictEqual(result.length, stems.length * 2);
for (const stem of stems) {
const trigger = `msd-${stem}`;
const winner = result.find((s) => s.trigger === trigger && s.kind === 'skills' && s.scope === 'global');
const loser = result.find((s) => s.trigger === trigger && s.kind === 'commands' && s.scope === 'local');
assert.ok(winner, `missing global skills entry for ${trigger}`);
assert.ok(loser, `missing local commands entry for ${trigger}`);
assert.strictEqual(winner.shadowedBy, null, `${trigger}: global skills entry must be unshadowed`);
assert.deepStrictEqual(loser.shadowedBy, { kind: 'skills', scope: 'global' }, `${trigger}: local commands entry must name the global skill as its shadower`);
assert.strictEqual(winner.destPath, `skills/${trigger}`);
assert.strictEqual(loser.destPath, `commands/${trigger}.md`);
assert.strictEqual(winner.registration, 'direct');
assert.strictEqual(loser.registration, 'direct');
assert.strictEqual(winner.routerTrigger, null);
assert.strictEqual(loser.routerTrigger, null);
}
});
test('claude global alone shadows nothing', () => {
const stems = ['plan-phase', 'help'];
const result = resolveTriggerSurface('claude', ['global'], { stems });
assert.strictEqual(result.length, stems.length);
for (const s of result) {
assert.strictEqual(s.kind, 'skills');
assert.strictEqual(s.scope, 'global');
assert.strictEqual(s.shadowedBy, null);
}
});
test('claude local alone shadows nothing', () => {
// #2218's healthy state: local reachable when nothing shadows it.
const stems = ['plan-phase', 'help'];
const result = resolveTriggerSurface('claude', ['local'], { stems });
assert.strictEqual(result.length, stems.length);
for (const s of result) {
assert.strictEqual(s.kind, 'commands');
assert.strictEqual(s.scope, 'local');
assert.strictEqual(s.shadowedBy, null);
}
});
test('no scopes yields no triggers', () => {
assert.deepStrictEqual(resolveTriggerSurface('claude', [], { stems: ['plan-phase'] }), []);
});
});
describe('resolveTriggerSurface — both-scope runtimes', () => {
test('both-scope runtimes report same-kind shadowing', () => {
// The both-scope trigger-bearing runtimes excluding claude (claude has
// its own dedicated rows above).
const BOTH_SCOPE_RUNTIMES = ['antigravity', 'codex', 'cursor', 'opencode', 'zcode'];
for (const runtime of BOTH_SCOPE_RUNTIMES) {
const result = resolveTriggerSurface(runtime, ['global', 'local'], { stems: ['plan-phase'] });
const group = result.filter((s) => s.trigger === 'msd-plan-phase');
assert.ok(group.length >= 2, `${runtime}: expected at least 2 candidate entries, got ${group.length}`);
const winners = group.filter((s) => s.shadowedBy === null);
assert.strictEqual(winners.length, 1, `${runtime}: expected exactly one unshadowed winner, got ${winners.length}`);
assert.strictEqual(winners[0].scope, 'global', `${runtime}: the winner must be the global entry`);
for (const s of group) {
if (s === winners[0]) continue;
assert.deepStrictEqual(
s.shadowedBy, { kind: winners[0].kind, scope: 'global' },
`${runtime}: every non-winner must name the global winner's kind`,
);
}
}
});
});
describe('resolveTriggerSurface — no-artifact-surface runtimes', () => {
test('runtimes with no artifact surface yield no triggers', () => {
for (const runtime of ['vscode']) {
assert.deepStrictEqual(
resolveTriggerSurface(runtime, ['global', 'local'], { stems: ['plan-phase'] }), [],
`${runtime}: expected no triggers`,
);
}
});
});
describe('resolveTriggerSurface — agents exclusion', () => {
test('agents are excluded from the trigger surface', () => {
// claude local emits both commands and agents from the same source tree.
const result = resolveTriggerSurface('claude', ['local'], { stems: ['plan-phase'] });
assert.ok(!result.some((s) => s.kind === 'agents'), 'agents must never appear in the trigger surface');
assert.ok(result.some((s) => s.kind === 'commands'), 'commands must still be present');
});
});
describe('resolveTriggerSurface — nested-router runtimes (#69)', () => {
test('nested runtimes distinguish direct from router-reachable registration', () => {
const NESTED_ROUTER_RUNTIMES = ['zcode'];
const opts = {
stems: ['ns-plan', 'plan-phase'],
routerStems: ['ns-plan'],
childToRouters: { 'plan-phase': ['ns-plan'] },
};
for (const runtime of NESTED_ROUTER_RUNTIMES) {
const result = resolveTriggerSurface(runtime, ['global'], opts);
const skillsEntries = result.filter((s) => s.kind === 'skills');
const router = skillsEntries.find((s) => s.trigger === 'msd-ns-plan');
const child = skillsEntries.find((s) => s.trigger === 'msd-plan-phase');
assert.ok(router, `${runtime}: missing router skills entry`);
assert.ok(child, `${runtime}: missing child skills entry`);
assert.strictEqual(router.registration, 'direct', `${runtime}: the router itself registers direct`);
assert.strictEqual(router.routerTrigger, null, `${runtime}: a router has no routerTrigger of its own`);
assert.strictEqual(child.registration, 'via-router', `${runtime}: the child is only reachable via its router`);
assert.strictEqual(child.routerTrigger, 'msd-ns-plan', `${runtime}: the child must name its owning router`);
}
});
});
describe('resolveTriggerSurface — namespacedByDir (hostile fixture, #row 12)', () => {
test('trigger honors the namespaced-by-directory layout', () => {
// No in-tree descriptor trips _copyStaged's namespacedByDir branch today
// (destSubpath's basename === prefix minus its trailing '-'). Synthetic
// fixture via the registry-override seam, injected the same way
// resolveRuntimeArtifactLayoutFromRegistry already accepts a registry.
const registry = {
runtimes: {
'fixture-namespaced': {
runtime: {
artifactLayout: {
global: [
{ kind: 'commands', destSubpath: 'commands/msd', prefix: 'msd-', nesting: 'flat', recursive: false, converter: null },
],
local: [],
},
},
},
},
};
const result = resolveTriggerSurface('fixture-namespaced', ['global'], { stems: ['plan-phase'], registry });
assert.strictEqual(result.length, 1);
const [entry] = result;
// The trigger (what the user types) is unaffected by the destPath branch.
assert.strictEqual(entry.trigger, 'msd-plan-phase');
// destPath must go through the namespacedByDir branch: bare filename, no
// double-applied prefix — NOT the naive `${destSubpath}/${prefix}${stem}.md`.
assert.strictEqual(entry.destPath, 'commands/msd/plan-phase.md');
assert.notStrictEqual(entry.destPath, 'commands/msd/msd-plan-phase.md');
});
});
describe('resolveTriggerSurface — unknown runtime', () => {
test('rejects an unknown runtime with the established error contract', () => {
assert.throws(
() => resolveTriggerSurface('zorptron', ['global'], { stems: ['plan-phase'] }),
(err) => {
assert.ok(err instanceof TypeError);
assert.ok(err.message.includes('zorptron'), 'error message must contain the runtime name');
return true;
},
);
});
});
describe('resolveTriggerSurface — unrecognized scope', () => {
test('rejects an unrecognized scope entry with the shared validateScopeId contract', () => {
// Sibling parity (#2871 Phase 2 review finding): scopeRank/resolveScope/
// isGlobalScope (install-scope.cts) already throw TypeError via
// validateScopeId for a bad scope id. resolveTriggerSurface must not
// fail open on the same input.
assert.throws(
() => resolveTriggerSurface('claude', ['bogus-scope'], { stems: ['plan-phase'] }),
(err) => {
assert.ok(err instanceof TypeError);
assert.ok(err.message.includes('bogus-scope'), 'error message must name the offending scope');
return true;
},
);
});
test('an empty scopes array still returns [] rather than throwing', () => {
assert.deepStrictEqual(resolveTriggerSurface('claude', [], { stems: ['plan-phase'] }), []);
});
});
describe('resolveTriggerSurface — purity', () => {
test('is pure and does not mutate its input', () => {
const scopes = ['global', 'local'];
const opts = { stems: ['plan-phase', 'help'] };
const scopesSnapshot = JSON.stringify(scopes);
const optsSnapshot = JSON.stringify(opts);
const first = resolveTriggerSurface('claude', scopes, opts);
assert.strictEqual(JSON.stringify(scopes), scopesSnapshot, 'scopes array must not be mutated');
assert.strictEqual(JSON.stringify(opts), optsSnapshot, 'opts object must not be mutated');
const pristine = JSON.parse(JSON.stringify(first));
// A caller mutating the returned array/objects must not corrupt a later call.
first[0].trigger = 'HACKED';
first.push({ trigger: 'INJECTED' });
const second = resolveTriggerSurface('claude', scopes, opts);
assert.deepStrictEqual(second, pristine, 'a second call must be unaffected by mutation of the first result');
});
});
describe('resolveTriggerSurface — exhaustive sweep', () => {
test('resolves for every runtime at every scope combination', () => {
const runtimes = Object.keys(capabilityRegistry.runtimes);
const scopeSubsets = [[], ['global'], ['local'], ['global', 'local']];
for (const runtime of runtimes) {
for (const scopes of scopeSubsets) {
let result;
assert.doesNotThrow(() => {
result = resolveTriggerSurface(runtime, scopes, { stems: ['plan-phase', 'help'] });
}, `${runtime} @ [${scopes.join(',')}] must not throw`);
assertValidSurfaceArray(result, `${runtime} @ [${scopes.join(',')}]`);
}
}
});
});
// ─── Precedence axis rows (matrix rows 16-23) ───────────────────────────────
describe('triggerPrecedence axis — validator', () => {
test('a descriptor without the axis remains valid', () => {
// claude's shipped descriptor now DECLARES triggerPrecedence (this PR added
// it), so the fixture must genuinely omit the key to exercise the omission
// path rather than merely echoing what is already on disk.
const cap = shippedCapWithout('claude', 'triggerPrecedence');
assert.ok(!('triggerPrecedence' in cap.runtime), 'fixture must not carry triggerPrecedence');
assert.deepStrictEqual(capValidator.validateRuntimeBody(cap), []);
assert.ok(Array.isArray(capValidator.DEFAULT_TRIGGER_PRECEDENCE), 'DEFAULT_TRIGGER_PRECEDENCE must be exported');
assert.deepStrictEqual(capValidator.DEFAULT_TRIGGER_PRECEDENCE, ['skills', 'commands']);
// Non-vacuous half: resolution must still pick the DEFAULT_TRIGGER_PRECEDENCE
// winner (skills over commands) when the axis is absent from the registry
// entry actually consulted by resolveTriggerSurface — same registry-override
// seam the 'reordering the axis changes which artifact wins' test below uses.
const registry = { runtimes: { claude: cap } };
const stems = ['plan-phase'];
const result = resolveTriggerSurface('claude', ['global', 'local'], { stems, registry });
const trigger = 'msd-plan-phase';
const winner = result.find((s) => s.trigger === trigger && s.shadowedBy === null);
const loser = result.find((s) => s.trigger === trigger && s.shadowedBy !== null);
assert.ok(winner, `missing unshadowed winner for ${trigger}`);
assert.strictEqual(winner.kind, 'skills', 'default precedence (skills > commands) must pick skills');
assert.strictEqual(winner.scope, 'global');
assert.ok(loser, `missing shadowed loser for ${trigger}`);
assert.deepStrictEqual(loser.shadowedBy, { kind: 'skills', scope: 'global' });
});
test('every shipped descriptor validates with the new axis', () => {
assert.ok(Array.isArray(capValidator.DEFAULT_TRIGGER_PRECEDENCE), 'DEFAULT_TRIGGER_PRECEDENCE must be exported');
for (const runtime of Object.keys(capabilityRegistry.runtimes)) {
const cap = shippedCap(runtime);
const errors = capValidator.validateRuntimeBody(cap);
assert.deepStrictEqual(errors, [], `${runtime}: expected clean validation, got ${JSON.stringify(errors)}`);
}
});
test('rejects an empty precedence list', () => {
const cap = shippedCap('claude');
cap.runtime.triggerPrecedence = [];
const errors = capValidator.validateRuntimeBody(cap);
assert.ok(
errors.some((e) => String(e).includes('triggerPrecedence')),
`expected a triggerPrecedence error, got ${JSON.stringify(errors)}`,
);
});
test('rejects an unknown kind in the precedence list', () => {
const cap = shippedCap('claude');
cap.runtime.triggerPrecedence = ['skills', 'bogus'];
const errors = capValidator.validateRuntimeBody(cap);
assert.ok(
errors.some((e) => String(e).includes('triggerPrecedence') && String(e).includes('bogus')),
`expected the offending kind named in the error, got ${JSON.stringify(errors)}`,
);
});
test('rejects a duplicate kind in the precedence list', () => {
const cap = shippedCap('claude');
cap.runtime.triggerPrecedence = ['skills', 'skills'];
const errors = capValidator.validateRuntimeBody(cap);
assert.ok(
errors.some((e) => String(e).includes('triggerPrecedence')),
`expected a triggerPrecedence error, got ${JSON.stringify(errors)}`,
);
});
test('rejects a non-array precedence value', () => {
for (const bad of ['skills-then-commands', {}, null]) {
const cap = shippedCap('claude');
cap.runtime.triggerPrecedence = bad;
const errors = capValidator.validateRuntimeBody(cap);
assert.ok(
errors.some((e) => String(e).includes('triggerPrecedence')),
`bad value ${JSON.stringify(bad)}: expected a triggerPrecedence error, got ${JSON.stringify(errors)}`,
);
}
});
});
describe('triggerPrecedence axis — resolution', () => {
test('reordering the axis changes which artifact wins', () => {
// opencode declares BOTH commands and skills at the SAME scope from the
// same source stems — the one runtime shape where kind precedence (not
// scope rank) decides the winner.
const defaultResult = resolveTriggerSurface('opencode', ['global'], { stems: ['plan-phase'] });
const defaultGroup = defaultResult.filter((s) => s.trigger === 'msd-plan-phase');
const defaultWinner = defaultGroup.find((s) => s.shadowedBy === null);
assert.ok(defaultWinner, 'expected a winner under the default precedence');
assert.strictEqual(defaultWinner.kind, 'skills', 'default precedence (skills > commands) must pick skills');
const overriddenCap = JSON.parse(JSON.stringify(capabilityRegistry.runtimes.opencode));
overriddenCap.runtime.triggerPrecedence = ['commands', 'skills'];
const overriddenRegistry = { runtimes: { opencode: overriddenCap } };
const overriddenResult = resolveTriggerSurface('opencode', ['global'], { stems: ['plan-phase'], registry: overriddenRegistry });
const overriddenGroup = overriddenResult.filter((s) => s.trigger === 'msd-plan-phase');
const overriddenWinner = overriddenGroup.find((s) => s.shadowedBy === null);
assert.ok(overriddenWinner, 'expected a winner under the overridden precedence');
assert.strictEqual(overriddenWinner.kind, 'commands', 'reordered precedence (commands > skills) must flip the winner');
});
test('the default precedence matches what claude declares', () => {
assert.ok(Array.isArray(capValidator.DEFAULT_TRIGGER_PRECEDENCE), 'DEFAULT_TRIGGER_PRECEDENCE must be exported');
assert.deepStrictEqual(capValidator.DEFAULT_TRIGGER_PRECEDENCE, ['skills', 'commands']);
const claudeCap = shippedCap('claude');
assert.deepStrictEqual(
claudeCap.runtime.triggerPrecedence, capValidator.DEFAULT_TRIGGER_PRECEDENCE,
"claude's declared triggerPrecedence must equal the validator's default",
);
});
});