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

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

458 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Equivalence proof for ADR-857 phase 5d: descriptor-driven resolveRuntimeArtifactLayout.
*
* For every runtime in the 15-entry capability registry × {global, local} scopes,
* this test asserts that:
* 1. kind.kind, kind.destSubpath, kind.prefix are byte-identical to the STEP-0
* golden captured from the old switch() before any edits.
* 2. typeof kind.stage === 'function' for every kind.
* 3. layout.runtime === runtime, layout.configDir === configDir, layout.scope === scope.
*
* SCOPE-FALL-THROUGH NOTE:
* The old switch() had no scope branches for 12 runtimes (cursor, codex,
* copilot, antigravity, windsurf, augment, trae, qwen, hermes, codebuddy, opencode,
* kilo), meaning scope='local' returned the same kinds as scope='global'. The 5a
* descriptors incorrectly set local:[] for those runtimes, causing 31 local-install
* test regressions. The 5b backfill sets local == global for these 12, restoring
* the old switch's scope-agnostic behaviour.
*
* For runtimes that had explicit scope branches in the old switch
* (claude: distinct local=commands+agents; cline: local=[]; kimi: local=[]),
* the STEP-0 golden matches the descriptor exactly and is left unchanged.
*
* Unknown runtime case:
* Missing runtime descriptors throw the same TypeError as the old table:
* TypeError: Unknown runtime: 'grok' — add to runtime-artifact-layout.cjs table
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const {
resolveRuntimeArtifactLayout,
resolveRuntimeArtifactLayoutFromRegistry,
} = require(
path.join(ROOT, 'msd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'),
);
const FAKE_DIR = '/tmp/fake-config-dir-dd';
// ── STEP-0 golden (captured from switch BEFORE edits) ────────────────────────
// Format: { kind, destSubpath, prefix } for each entry in kinds[].
// 'function' means we assert typeof kind.stage === 'function'.
// ADR-1235 step 1 (#1763): cursor, windsurf, augment, trae, codebuddy each gained
// an `agents` kind (appended last). Goldens consciously updated post-cutover.
const GOLDEN = {
// ── claude ──────────────────────────────────────────────────────────────────
// #2875 Part 2 (the agents-descriptor migration): claude/global gains an
// `agents` kind. This is NOT new on-disk behavior — a `claude --global`
// install always wrote `<configDir>/agents/msd-*.md` via the now-deleted
// inline agent-staging loop in bin/install.js (claude was never in
// `_DESCRIPTOR_AGENTS_RUNTIMES`, and that loop ran unconditionally,
// independent of `_isSkillsRuntime`/scope). The descriptor previously never
// modeled that write; it now does, matching what was always materialized.
'claude/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'claude/local': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-' }, // #1367: flat msd-<cmd>.md
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── cursor ───────────────────────────────────────────────────────────────────
// Old switch: BOTH scopes returned [skills, commands] (no scope branch).
// 5b backfill: local == global.
// ADR-1235 step 1 (#1763): agents kind added.
// #2644: commands are deliberately retired so Cursor exposes a single skills surface.
'cursor/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'cursor/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── codex ────────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// #2875 Part 2: agents kind added (the inline loop wrote codex agents too —
// codex was never in `_DESCRIPTOR_AGENTS_RUNTIMES`; the config.toml
// [agents.msd-*] strip on a full→minimal downgrade is a SEPARATE, still
// hand-rolled write this migration deliberately does not touch).
'codex/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'codex/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── copilot ──────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// #1575: agents kind added (copilot cutover — .agent.md rename handled by _copyStaged).
'copilot/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'copilot/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── antigravity ──────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// #1575: agents kind added (antigravity cutover — scope-aware converter).
'antigravity/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'antigravity/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── windsurf ─────────────────────────────────────────────────────────────────
// ADR-1235 step 1 (#1763): agents kind added to both scopes.
'windsurf/global': [
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'windsurf/local': [
{ kind: 'commands', destSubpath: 'workflows', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── augment ──────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// ADR-1235 step 1 (#1763): agents kind added.
'augment/global': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'augment/local': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── trae ─────────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// ADR-1235 step 1 (#1763): agents kind added.
'trae/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'trae/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── qwen ─────────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// ADR-1239 / #2092 Phase B Upgrade 1: agents kind added (native
// .qwen/agents/*.md subagent projection).
'qwen/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'qwen/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── hermes ───────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// #2875 Part 2: agents kind added (the inline loop wrote hermes agents too,
// via a new named branding converter — convertClaudeAgentToHermesAgent).
'hermes/global': [
{ kind: 'skills', destSubpath: 'skills/msd', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'hermes/local': [
{ kind: 'skills', destSubpath: 'skills/msd', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── codebuddy ────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// ADR-1235 step 1 (#1763): agents kind added.
'codebuddy/global': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'codebuddy/local': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── cline ────────────────────────────────────────────────────────────────────
// Old switch: scope='global' → [skills]; scope='local' → []. Matches descriptor.
// #2875 Part 2: agents kind added to global (the inline loop wrote cline
// agents too, via convertClaudeAgentToClineAgent).
// #2875 Part 2 defect fix (cline-local agents-drop regression, closed):
// cline was never scope-gated in the deleted inline loop either, so a real
// `cline --local` install ALSO wrote agent files pre-migration (to the
// project root, per hostBehaviors.localTargetIsProjectRoot). `local` now
// ALSO declares the agents kind (capabilities/cline/capability.json),
// restoring that behavior — see tests/agent-descriptor-parity.test.cjs's
// cline (local) H row and tests/cline-install.test.cjs's local-install
// regression test for the byte-parity / end-to-end proofs. `local` still
// declares no `skills` kind: cline-local commands are embedded in
// .clinerules, never materialized as skill files
// (hostBehaviors.localCommandsViaRules).
'cline/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'cline/local': [
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── kimi ─────────────────────────────────────────────────────────────────────
// Old switch: scope='global' → [skills, kimi-agents]; scope='local' → []. Matches descriptor.
'kimi/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'kimi-agents', destSubpath: 'agents', prefix: 'msd' },
],
'kimi/local': [],
// ── opencode ─────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// #2329: destSubpath corrected from singular 'command' to plural 'commands' —
// OpenCode discovers slash commands from commands/ (plural); the singular
// command/ dir MSD previously wrote to is not scanned by OpenCode 1.17.13,
// so none of the ~71 /msd-* commands ever appeared in the OpenCode TUI.
// #2875 Part 2: agents kind added (the inline loop wrote opencode agents
// too — opencode's agents materialization previously went through the
// separate installOpencodeFamilySkills-adjacent inline loop, not through
// this layout at all; installAgentsKindStandalone now covers it).
'opencode/global': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'opencode/local': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
// ── kilo ─────────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
// #2875 Part 2: agents kind added (mirrors opencode — same combined-family
// install shape, same install-engine.cts installOpencodeFamilyArtifacts /
// installAgentsKindStandalone coverage).
'kilo/global': [
{ kind: 'commands', destSubpath: 'command', prefix: 'msd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'kilo/local': [
{ kind: 'commands', destSubpath: 'command', prefix: 'msd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
};
// ── Parametrized tests ────────────────────────────────────────────────────────
const RUNTIMES = [
'claude', 'cursor', 'codex', 'copilot',
'antigravity', 'windsurf', 'augment', 'trae', 'qwen',
'hermes', 'codebuddy', 'cline', 'kimi', 'opencode', 'kilo',
];
for (const runtime of RUNTIMES) {
for (const scope of ['global', 'local']) {
const key = `${runtime}/${scope}`;
const expected = GOLDEN[key];
assert.ok(
expected !== undefined,
`GOLDEN missing entry for ${key} — update the golden table`,
);
describe(`resolveRuntimeArtifactLayout — ${runtime} ${scope} (descriptor-driven)`, () => {
test(`kinds array matches STEP-0 golden for ${runtime}/${scope}`, () => {
const layout = resolveRuntimeArtifactLayout(runtime, FAKE_DIR, /** @type {'local'|'global'} */ (scope));
// Structural fields
assert.strictEqual(layout.runtime, runtime, 'layout.runtime');
assert.strictEqual(layout.configDir, FAKE_DIR, 'layout.configDir');
assert.strictEqual(layout.scope, scope, 'layout.scope');
// kinds length matches golden
assert.strictEqual(
layout.kinds.length,
expected.length,
`kinds.length for ${key}: expected ${expected.length}, got ${layout.kinds.length}`,
);
// Per-kind field checks
for (let i = 0; i < expected.length; i++) {
const actual = layout.kinds[i];
const exp = expected[i];
assert.strictEqual(
actual.kind,
exp.kind,
`kinds[${i}].kind for ${key}`,
);
assert.strictEqual(
actual.destSubpath,
exp.destSubpath,
`kinds[${i}].destSubpath for ${key}`,
);
assert.strictEqual(
actual.prefix,
exp.prefix,
`kinds[${i}].prefix for ${key}`,
);
assert.strictEqual(
typeof actual.stage,
'function',
`kinds[${i}].stage must be a function for ${key}`,
);
}
});
});
}
}
// ── Unknown runtime ───────────────────────────────────────────────────────────
// Missing descriptors reproduce the old loud-fail behaviour.
describe('resolveRuntimeArtifactLayout — unknown runtime (descriptor-driven)', () => {
test('throws TypeError for grok (no artifact layout descriptor)', () => {
assert.throws(
() => resolveRuntimeArtifactLayout('grok', FAKE_DIR, 'global'),
(err) => {
assert.ok(err instanceof TypeError, 'must be TypeError');
assert.ok(
err.message.includes("Unknown runtime: 'grok'"),
`message must include "Unknown runtime: 'grok'" — got: ${err.message}`,
);
return true;
},
);
});
test('throws TypeError for an arbitrary unknown string', () => {
assert.throws(
() => resolveRuntimeArtifactLayout('notaruntime', FAKE_DIR, 'global'),
(err) => {
assert.ok(err instanceof TypeError);
assert.ok(err.message.includes("Unknown runtime: 'notaruntime'"));
return true;
},
);
});
});
describe('resolveRuntimeArtifactLayout — descriptor-only future runtime', () => {
test('accepts a synthetic descriptor-backed runtime without a parallel allowlist update', () => {
const registry = {
runtimes: {
futurecli: {
runtime: {
artifactLayout: {
global: [
{
kind: 'commands',
destSubpath: 'commands',
prefix: 'msd-',
nesting: 'flat',
recursive: false,
converter: null,
},
],
local: [],
},
},
},
},
};
const layout = resolveRuntimeArtifactLayoutFromRegistry(
registry,
'futurecli',
FAKE_DIR,
'global',
);
assert.strictEqual(layout.runtime, 'futurecli');
assert.strictEqual(layout.configDir, FAKE_DIR);
assert.strictEqual(layout.scope, 'global');
assert.strictEqual(layout.kinds.length, 1);
assert.strictEqual(layout.kinds[0].kind, 'commands');
assert.strictEqual(layout.kinds[0].destSubpath, 'commands');
assert.strictEqual(layout.kinds[0].prefix, 'msd-');
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
});
});
// ── Scope default ─────────────────────────────────────────────────────────────
// resolveRuntimeArtifactLayout(runtime, configDir) with no scope arg → 'global'.
describe('resolveRuntimeArtifactLayout — scope defaults to global (descriptor-driven)', () => {
test('omitting scope yields global layout for claude', () => {
const withDefault = resolveRuntimeArtifactLayout('claude', FAKE_DIR);
const withExplicit = resolveRuntimeArtifactLayout('claude', FAKE_DIR, 'global');
assert.strictEqual(withDefault.scope, 'global', 'default scope must be "global"');
assert.strictEqual(withDefault.kinds.length, withExplicit.kinds.length);
for (let i = 0; i < withDefault.kinds.length; i++) {
assert.strictEqual(withDefault.kinds[i].kind, withExplicit.kinds[i].kind);
assert.strictEqual(withDefault.kinds[i].destSubpath, withExplicit.kinds[i].destSubpath);
assert.strictEqual(withDefault.kinds[i].prefix, withExplicit.kinds[i].prefix);
}
});
test('omitting scope yields global layout for kimi (2 kinds)', () => {
const layout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR);
assert.strictEqual(layout.scope, 'global');
assert.strictEqual(layout.kinds.length, 2);
assert.strictEqual(layout.kinds[0].kind, 'skills');
assert.strictEqual(layout.kinds[1].kind, 'kimi-agents');
});
});
// ── Non-vacuous check: verify at least one multi-kind runtime ─────────────────
describe('resolveRuntimeArtifactLayout — multi-kind runtimes non-vacuous (descriptor-driven)', () => {
test('augment global returns 3 kinds (commands + skills + agents)', () => {
const layout = resolveRuntimeArtifactLayout('augment', FAKE_DIR, 'global');
assert.strictEqual(layout.kinds.length, 3);
assert.strictEqual(layout.kinds[0].kind, 'commands');
assert.strictEqual(layout.kinds[1].kind, 'skills');
assert.strictEqual(layout.kinds[2].kind, 'agents');
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
assert.strictEqual(typeof layout.kinds[1].stage, 'function');
assert.strictEqual(typeof layout.kinds[2].stage, 'function');
});
test('kimi global returns skills then kimi-agents', () => {
const layout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR, 'global');
assert.strictEqual(layout.kinds.length, 2);
assert.strictEqual(layout.kinds[0].kind, 'skills');
assert.strictEqual(layout.kinds[1].kind, 'kimi-agents');
assert.strictEqual(layout.kinds[1].destSubpath, 'agents');
assert.strictEqual(layout.kinds[1].prefix, 'msd');
});
test('codebuddy global returns commands then skills then agents', () => {
const layout = resolveRuntimeArtifactLayout('codebuddy', FAKE_DIR, 'global');
assert.strictEqual(layout.kinds.length, 3);
assert.strictEqual(layout.kinds[0].kind, 'commands');
assert.strictEqual(layout.kinds[1].kind, 'skills');
assert.strictEqual(layout.kinds[2].kind, 'agents');
});
});