Some checks failed
Tests / PR mergeability (push) Successful in 18s
Tests / Base branch health (push) Successful in 9s
Tests / Detect test scope (push) Successful in 16s
Tests / lint-tests (push) Failing after 1m43s
Tests / plugin-validate (push) Successful in 58s
Tests / test (ubuntu-latest, 24, shard 1/3) (push) Failing after 19s
Tests / test (ubuntu-latest, 24, shard 2/3) (push) Failing after 20s
Tests / test (ubuntu-latest, 24, shard 3/3) (push) Failing after 20s
Tests / test (ubuntu-latest, 24) (push) Failing after 18s
Tests / test (inert CI) (push) Has been skipped
Tests / QA loop walk (smell ratchet) (push) Failing after 19s
Tests / Coverage gate (merged shards) (push) Has been skipped
Tests / Publish emitted-baseline artifact (push) Has been skipped
Duplicate auto-close sweep / sweep (push) Successful in 19s
CI timeout budget report / report (push) Failing after 14s
Close Draft PRs (sweep) / Sweep open draft PRs (push) Successful in 9s
Dismiss Unauthorized PR Approvals / dismiss-unauthorized-approval (push) Successful in 9s
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
Tests / Required tests (push) Has been cancelled
310 lines
14 KiB
JavaScript
310 lines
14 KiB
JavaScript
'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 several runtimes (cursor, codex,
|
||
* antigravity, opencode), 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, restoring the old switch's scope-agnostic behaviour.
|
||
*
|
||
* For runtimes that had explicit scope branches in the old switch
|
||
* (claude: distinct local=commands+agents),
|
||
* 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 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');
|
||
});
|
||
});
|