Files
msd-core/tests/runtime-artifact-layout-descriptor-drive.test.cjs

391 lines
18 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 16-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 13 runtimes (cursor, gemini, 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 13, 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, 'gsd-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'.
const GOLDEN = {
// ── claude ──────────────────────────────────────────────────────────────────
'claude/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'claude/local': [
{ kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'gsd-' },
],
// ── cursor ───────────────────────────────────────────────────────────────────
// Old switch: BOTH scopes returned [skills, commands] (no scope branch).
// 5b backfill: local == global.
'cursor/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
],
'cursor/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
],
// ── gemini ───────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'gemini/global': [
{ kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' },
],
'gemini/local': [
{ kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' },
],
// ── codex ────────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'codex/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'codex/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
// ── copilot ──────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'copilot/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'copilot/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
// ── antigravity ──────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'antigravity/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'antigravity/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
// ── windsurf ─────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'windsurf/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'windsurf/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
// ── augment ──────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'augment/global': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'augment/local': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
// ── trae ─────────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'trae/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'trae/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
// ── qwen ─────────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'qwen/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'qwen/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
// ── hermes ───────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'hermes/global': [
{ kind: 'skills', destSubpath: 'skills/gsd', prefix: 'gsd-' },
],
'hermes/local': [
{ kind: 'skills', destSubpath: 'skills/gsd', prefix: 'gsd-' },
],
// ── codebuddy ────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'codebuddy/global': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'codebuddy/local': [
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
// ── cline ────────────────────────────────────────────────────────────────────
// Old switch: scope='global' → [skills]; scope='local' → []. Matches descriptor.
'cline/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'cline/local': [],
// ── kimi ─────────────────────────────────────────────────────────────────────
// Old switch: scope='global' → [skills, kimi-agents]; scope='local' → []. Matches descriptor.
'kimi/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
{ kind: 'kimi-agents', destSubpath: 'agents', prefix: 'gsd' },
],
'kimi/local': [],
// ── opencode ─────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'opencode/global': [
{ kind: 'commands', destSubpath: 'command', prefix: 'gsd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'opencode/local': [
{ kind: 'commands', destSubpath: 'command', prefix: 'gsd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
// ── kilo ─────────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this.
'kilo/global': [
{ kind: 'commands', destSubpath: 'command', prefix: 'gsd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'kilo/local': [
{ kind: 'commands', destSubpath: 'command', prefix: 'gsd-' },
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
};
// ── Parametrized tests ────────────────────────────────────────────────────────
const RUNTIMES = [
'claude', 'cursor', 'gemini', '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: 'gsd-',
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, 'gsd-');
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 2 kinds (commands + skills)', () => {
const layout = resolveRuntimeArtifactLayout('augment', FAKE_DIR, 'global');
assert.strictEqual(layout.kinds.length, 2);
assert.strictEqual(layout.kinds[0].kind, 'commands');
assert.strictEqual(layout.kinds[1].kind, 'skills');
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
assert.strictEqual(typeof layout.kinds[1].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, 'gsd');
});
test('codebuddy global returns commands then skills', () => {
const layout = resolveRuntimeArtifactLayout('codebuddy', FAKE_DIR, 'global');
assert.strictEqual(layout.kinds.length, 2);
assert.strictEqual(layout.kinds[0].kind, 'commands');
assert.strictEqual(layout.kinds[1].kind, 'skills');
});
});