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

311 lines
14 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=[]),
* 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-' },
],
// ── 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-' },
],
// ── 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-' },
],
// ── zcode ────────────────────────────────────────────────────────────────────
// Same layout in both scopes: skills + commands + agents.
'zcode/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
'zcode/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'msd-' },
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'msd-' },
],
};
// ── Parametrized tests ────────────────────────────────────────────────────────
const RUNTIMES = [
'claude', 'cursor', 'codex',
'antigravity', 'opencode', 'zcode',
];
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);
}
});
});
// ── Non-vacuous check: verify at least one multi-kind runtime ─────────────────
describe('resolveRuntimeArtifactLayout — multi-kind runtimes non-vacuous (descriptor-driven)', () => {
test('opencode global returns 3 kinds (commands + skills + agents)', () => {
const layout = resolveRuntimeArtifactLayout('opencode', 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');
});
});