feat(#1925): add ZCode (Z.ai) as a pluggable runtime descriptor

Add ZCode as a first-party runtime via a declarative capability descriptor
(capabilities/zcode/capability.json) with zero hardcoded runtime === 'zcode'
branches — exercising the de-hardcoded, data-driven runtime path that 1.7.0
(ADR-1016 / ADR-1239) enables.

Descriptor (all axes sourced verbatim from zcode.z.ai docs):
- configHome ~/.zcode; nested skills + flat commands/agents; profile-marker install
- Claude-shaped skill format reuses convertClaudeCommandToClaudeSkill (no new converter)
- hostIntegration: declarative / slash-file / mcp / electron; dispatch background=false
  (foreground-only per docs); nested+maxDepth undocumented; passive model mode

Installer registration (data, not branches): --zcode flag, allRuntimes, runtimeMap,
interactive menu, --all list. getGlobalConfigDir + resolveRuntimeArtifactLayout +
ALLOWED_CONFIG_RUNTIMES + resolveInstallPlan all derive from the descriptor.

Revamped the brittle per-runtime golden-master tests to be count-agnostic,
descriptor-derived property tests (1.7.0 makes runtimes pluggable data, so pinning
frozen '15 runtime' snapshots is the wrong invariant): getdirname, label-policy,
config-adapter-registry (intent + install-plan golden master), capability-registry,
host-integration-descriptors (counts derive from curated maps). Adding a runtime
descriptor now extends coverage with zero edits to those suites.

Welcome banner, --zcode help, supported-runtimes how-to, and the host-integration
capability matrix (every axis cited) updated.
This commit is contained in:
Tom Boucher
2026-07-05 22:47:56 -04:00
parent 84fc9d6365
commit 69b309e4e0
18 changed files with 747 additions and 572 deletions

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 0
---
**ZCode (Z.ai) is now an installable runtime** — a desktop Agentic Development Environment for the GLM-5.2 model can now be targeted with `--zcode`, landing GSD skills at `~/.zcode/skills/<name>/SKILL.md` plus slash commands and subagents. ZCode ships as a pure declarative capability descriptor (`capabilities/zcode/capability.json`) with zero hardcoded `runtime === 'zcode'` branches, reusing the Claude skill converter — the de-hardcoded, data-driven runtime path that 1.7.0 (ADR-1016 / ADR-1239) enables. (#1925)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,102 @@
{
"id": "zcode",
"role": "runtime",
"version": "1.7.0-rc.2",
"title": "ZCode",
"description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills/<name>/SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.",
"tier": "core",
"requires": [],
"engines": {
"gsd": ">=1.6.0"
},
"runtime": {
"configHome": {
"kind": "dot-home",
"name": ".zcode",
"env": [
"ZCODE_CONFIG_DIR"
]
},
"localConfigDir": ".zcode",
"configFormat": "none",
"artifactLayout": {
"global": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
],
"local": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
]
},
"commandStyle": "slash-hyphen",
"hooksSurface": "none",
"sandboxTier": "none",
"supportTier": 2,
"installSurface": "profile-marker-only",
"writesSharedSettings": false,
"permissionWriter": null,
"extendedHookEvents": [],
"hostIntegration": {
"embeddingMode": "declarative",
"commandSurface": "slash-file",
"dispatch": {
"namedDispatch": true,
"nested": "undocumented",
"maxDepth": "undocumented",
"background": false,
"subagentToolkit": "full",
"backgroundDispatch": false
},
"modelMode": "passive",
"hookBus": "host",
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "electron"
}
}
}

View File

@@ -408,6 +408,22 @@ Skills land in `~/.trae/`. GSD installs skills, agents, and rule references.
--- ---
### ZCode
```bash
npx @opengsd/gsd-core@latest --zcode --global
```
[ZCode](https://zcode.z.ai/en) is Z.ai's desktop Agentic Development Environment for the GLM-5.2 model. GSD installs skills (nested `SKILL.md` bundles), slash commands, and subagents under `~/.zcode/`:
- **Skills** → `~/.zcode/skills/gsd-<name>/SKILL.md` (invoke with `$gsd-<name>` in chat)
- **Commands** → `~/.zcode/commands/gsd-<name>.md` (invoke with `/gsd-<name>`)
- **Subagents** → `~/.zcode/agents/gsd-<name>.md`
ZCode's skill format is identical to Claude Code's, so no runtime-specific converter is required — GSD lands as a pure declarative descriptor with no hardcoded installer branches. ZCode also natively imports skills and MCP config from `~/.claude`; if you install GSD for **both** Claude and ZCode, you may see duplicate GSD skills inside ZCode, which is expected. To connect ZCode's MCP servers to GSD's companion server, see [how to connect the GSD MCP server](connect-gsd-mcp-server.md).
---
## Local vs global install ## Local vs global install
All examples above use `--global`, which installs GSD once for your user account. To scope an install to a single project, replace `--global` with `--local`: All examples above use `--global`, which installs GSD once for your user account. To scope an install to a single project, replace `--global` with `--local`:

View File

@@ -548,3 +548,40 @@ Sources consulted:
Documentation gaps: Documentation gaps:
- dispatch.subagentToolkit — docs show three built-in subagent types each with different tool subsets (coder=full, explore=read-only, plan=no shell/write); no single 'full' or 'read-only' value covers all types; maintainer should clarify the intended classification. - dispatch.subagentToolkit — docs show three built-in subagent types each with different tool subsets (coder=full, explore=read-only, plan=no shell/write); no single 'full' or 'read-only' value covers all types; maintainer should clarify the intended classification.
- runtime — CLI core is Python; a Rust Wire implementation also exists; docs do not state a canonical plugin extension runtime. - runtime — CLI core is Python; a Rust Wire implementation also exists; docs do not state a canonical plugin extension runtime.
---
## zcode
> ZCode (Z.ai) is a desktop Agentic Development Environment for the GLM-5.2 model, distributed as an Electron app. It exposes a Claude-Code-shaped extensibility surface (per-user `~/.zcode/skills/<name>/SKILL.md`, slash commands, named subagents, native MCP, and a plugin system). All values below are sourced verbatim from the official ZCode docs.
| Axis | Value | Source | Evidence |
|---|---|---|---|
| embeddingMode | declarative | https://zcode.z.ai/en/docs/plugin | "A single plugin can bundle several capabilities. ZCode detects which components a plugin includes from its directory layout" — plugins/skills/commands/agents are config/markdown files; no in-process programmatic extension API is documented. |
| commandSurface | slash-file | https://zcode.z.ai/en/docs/commands | "Custom commands are stored as `.md` files under `~/.zcode/commands` ... invoke the command with `/command-name`" |
| modelMode | passive | https://zcode.z.ai/en/docs/configuration | Models are connected by provider config (Z.ai/BigModel/OpenAI-compat/Anthropic-compat base URLs + API keys in Model Settings); no programmatic model request API is documented. |
| hookBus | host | https://zcode.z.ai/en/docs/plugin | A plugin's bundled components include a "**Hook** — Automation hooks triggered on specific events" — the host fires the events a plugin subscribes to. |
| stateIO | filesystem | https://zcode.z.ai/en/docs/skill | "User-level skills for ZCode Agent: `~/.zcode/skills/<skill-name>/SKILL.md`" — full local filesystem (desktop app). |
| transport | mcp | https://zcode.z.ai/en/docs/mcp-services | "MCP (Model Context Protocol) connects external capabilities ... type as `stdio` (SSE and HTTP remote servers are also supported)" — native MCP. |
| runtime | electron | https://zcode.z.ai/en/docs/install (download path `cdn-zcode.z.ai/zcode/electron/releases/3.2.5/ZCode-3.2.5-mac-arm64.dmg`) | ZCode is shipped as an Electron desktop application; the release artifact lives under the `electron/releases` path. |
| dispatch.namedDispatch | true | https://zcode.z.ai/en/docs/subagents | "you can let the Agent pick the subagent automatically, or reference it with `@` in the chat box" — subagents are invoked by name via the Agent tool. |
| dispatch.nested | undocumented | searched: https://zcode.z.ai/en/docs/subagents | The docs do not state whether a subagent can itself spawn further subagents. |
| dispatch.maxDepth | undocumented | searched: https://zcode.z.ai/en/docs/subagents | No maximum nesting depth is documented. |
| dispatch.background | false | https://zcode.z.ai/en/docs/subagents | "**Foreground execution.** Subagents run in the foreground ... Background execution is not enabled yet." |
| dispatch.subagentToolkit | full | https://zcode.z.ai/en/docs/subagents | "**general-purpose** is the default built-in subagent ... It has access to all tools"; custom subagents default to "All permissions by default" (inherits every tool). |
| dispatch.backgroundDispatch | false | https://zcode.z.ai/en/docs/subagents | "Background execution is not enabled yet" — background dispatch is therefore impossible. |
Sources consulted:
- https://zcode.z.ai/en/docs/skill
- https://zcode.z.ai/en/docs/commands
- https://zcode.z.ai/en/docs/subagents
- https://zcode.z.ai/en/docs/mcp-services
- https://zcode.z.ai/en/docs/plugin
- https://zcode.z.ai/en/docs/configuration
- https://zcode.z.ai/en/docs/install
Documentation gaps:
- dispatch.nested / dispatch.maxDepth — ZCode's subagent docs do not state whether subagents can spawn further subagents or any depth bound.
- configHome — skills/commands/agents homes are documented (`~/.zcode/skills`, `~/.zcode/commands`, `~/.zcode/agents`); the exact settings filename under `~/.zcode` (where MCP server config is stored) is not fully documented at time of writing.
- Maintenance note — ZCode is a young, fast-moving app (observed at v3.2.x); these axes may need revision as its on-disk config layout stabilizes. Because ZCode also natively imports skills/MCP from `~/.claude`, installing GSD to BOTH `claude` and `zcode` can surface duplicated skills inside ZCode; this overlap is expected and documented.

View File

@@ -2439,6 +2439,108 @@ const capabilities = {
"runtime": "undocumented" "runtime": "undocumented"
} }
} }
},
"zcode": {
"id": "zcode",
"role": "runtime",
"version": "1.7.0-rc.2",
"title": "ZCode",
"description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills/<name>/SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.",
"tier": "core",
"requires": [],
"engines": {
"gsd": ">=1.6.0"
},
"runtime": {
"configHome": {
"kind": "dot-home",
"name": ".zcode",
"env": [
"ZCODE_CONFIG_DIR"
]
},
"localConfigDir": ".zcode",
"configFormat": "none",
"artifactLayout": {
"global": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
],
"local": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
]
},
"commandStyle": "slash-hyphen",
"hooksSurface": "none",
"sandboxTier": "none",
"supportTier": 2,
"installSurface": "profile-marker-only",
"writesSharedSettings": false,
"permissionWriter": null,
"extendedHookEvents": [],
"hostIntegration": {
"embeddingMode": "declarative",
"commandSurface": "slash-file",
"dispatch": {
"namedDispatch": true,
"nested": "undocumented",
"maxDepth": "undocumented",
"background": false,
"subagentToolkit": "full",
"backgroundDispatch": false
},
"modelMode": "passive",
"hookBus": "host",
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "electron"
}
}
} }
}; };
@@ -4495,6 +4597,108 @@ const runtimes = {
"runtime": "undocumented" "runtime": "undocumented"
} }
} }
},
"zcode": {
"id": "zcode",
"role": "runtime",
"version": "1.7.0-rc.2",
"title": "ZCode",
"description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills/<name>/SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.",
"tier": "core",
"requires": [],
"engines": {
"gsd": ">=1.6.0"
},
"runtime": {
"configHome": {
"kind": "dot-home",
"name": ".zcode",
"env": [
"ZCODE_CONFIG_DIR"
]
},
"localConfigDir": ".zcode",
"configFormat": "none",
"artifactLayout": {
"global": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
],
"local": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
]
},
"commandStyle": "slash-hyphen",
"hooksSurface": "none",
"sandboxTier": "none",
"supportTier": 2,
"installSurface": "profile-marker-only",
"writesSharedSettings": false,
"permissionWriter": null,
"extendedHookEvents": [],
"hostIntegration": {
"embeddingMode": "declarative",
"commandSurface": "slash-file",
"dispatch": {
"namedDispatch": true,
"nested": "undocumented",
"maxDepth": "undocumented",
"background": false,
"subagentToolkit": "full",
"backgroundDispatch": false
},
"modelMode": "passive",
"hookBus": "host",
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "electron"
}
}
} }
}; };
@@ -4676,7 +4880,8 @@ const _requiresGraph = {
"tdd": [], "tdd": [],
"trae": [], "trae": [],
"ui": [], "ui": [],
"windsurf": [] "windsurf": [],
"zcode": []
}; };
function requiresClosure(id) { function requiresClosure(id) {

View File

@@ -196,6 +196,7 @@ const RUNTIME_LABELS: Readonly<Record<string, string>> = {
kimi: 'Kimi CLI', kimi: 'Kimi CLI',
codebuddy: 'CodeBuddy', codebuddy: 'CodeBuddy',
cline: 'Cline', cline: 'Cline',
zcode: 'ZCode',
}; };
/** /**
@@ -266,7 +267,7 @@ export function getGlobalConfigHomeFragment(runtime: string): string {
*/ */
const RUNTIME_FLAG_IDS = Object.freeze([ const RUNTIME_FLAG_IDS = Object.freeze([
'opencode', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor', 'opencode', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor',
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode',
] as const); ] as const);
/** /**

View File

@@ -32,8 +32,8 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
const RUNTIMES = Object.keys(registry.runtimes); const RUNTIMES = Object.keys(registry.runtimes);
test('declarative adapter: kind === "declarative" + runtime echoed, for all 15 runtimes', () => { test('declarative adapter: kind === "declarative" + runtime echoed, for every registry runtime', () => {
assert.ok(RUNTIMES.length >= 15, `expected ≥15 runtimes in registry, got ${RUNTIMES.length}`); assert.ok(RUNTIMES.length > 0, `expected at least one runtime in registry, got ${RUNTIMES.length}`);
for (const r of RUNTIMES) { for (const r of RUNTIMES) {
const adapter = createDeclarativeAdapter({ runtime: r }); const adapter = createDeclarativeAdapter({ runtime: r });
assert.strictEqual(adapter.kind, 'declarative', `${r}: kind must be 'declarative'`); assert.strictEqual(adapter.kind, 'declarative', `${r}: kind must be 'declarative'`);

View File

@@ -25,7 +25,7 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
const RUNTIMES = Object.keys(registry.runtimes); const RUNTIMES = Object.keys(registry.runtimes);
test('imperative adapter: kind === "imperative" + runtime echoed + registry present, for all 15 runtimes', () => { test('imperative adapter: kind === "imperative" + runtime echoed + registry present, for every registry runtime', () => {
for (const r of RUNTIMES) { for (const r of RUNTIMES) {
const adapter = createImperativeAdapter({ runtime: r }); const adapter = createImperativeAdapter({ runtime: r });
assert.strictEqual(adapter.kind, 'imperative', `${r}: kind must be 'imperative'`); assert.strictEqual(adapter.kind, 'imperative', `${r}: kind must be 'imperative'`);

View File

@@ -3186,11 +3186,9 @@ const {
// file is an installed artifact captured by golden-install-parity (#1943). // file is an installed artifact captured by golden-install-parity (#1943).
const VALID_EXTENSION_EVENTS = capValidatorModule.VALID_EXTENSION_EVENTS; const VALID_EXTENSION_EVENTS = capValidatorModule.VALID_EXTENSION_EVENTS;
const RUNTIME_IDS = [ // Runtime ids are derived from the built registry (the single source of truth)
'claude', 'codex', 'antigravity', 'cursor', 'opencode', // so this file stays fluid when a runtime descriptor is added or removed.
'kilo', 'copilot', 'augment', 'trae', 'qwen', 'hermes', const RUNTIME_IDS = Object.keys(buildRegistry(loadAndValidate(new Set()).capMap).runtimes);
'codebuddy', 'cline', 'kimi', 'windsurf',
];
// Helper: build a minimal valid runtime capability object for fixture-based tests // Helper: build a minimal valid runtime capability object for fixture-based tests
function makeRuntimeCap(overrides) { function makeRuntimeCap(overrides) {
@@ -3232,18 +3230,31 @@ function makeRuntimeCap(overrides) {
}; };
} }
// ── 24a. All 15 runtime ids appear in the runtimes index ───────────────────── // ── 24a. Every role:runtime capability appears in the runtimes index ─────────
// Count-agnostic: the built registry's runtime set must exactly equal the set
// of capability folders whose descriptor declares role:"runtime". Adding a
// runtime descriptor extends coverage with zero edits here.
describe('ADR-1016 phase 5a: all 15 runtimes in registry index', () => { describe('ADR-1016 phase 5a: every role:runtime capability is in the registry index', () => {
let registry; let registry;
test('loadAndValidate + buildRegistry produces runtimes index with 15 entries', () => { test('loadAndValidate + buildRegistry indexes exactly the role:runtime capabilities on disk', () => {
const { capMap, errors } = loadAndValidate(new Set()); const { capMap, errors } = loadAndValidate(new Set());
const hardErrors = errors.filter((e) => !e.includes('pending-migration')); const hardErrors = errors.filter((e) => !e.includes('pending-migration'));
assert.deepEqual(hardErrors, [], 'Expected no hard errors: ' + JSON.stringify(hardErrors)); assert.deepEqual(hardErrors, [], 'Expected no hard errors: ' + JSON.stringify(hardErrors));
registry = buildRegistry(capMap); registry = buildRegistry(capMap);
// Derive the expected runtime id set from the loaded capability map — not a
// hand-pinned count. This is the contract: every role:runtime descriptor on
// disk becomes a runtimes-index entry, no more, no less.
const expectedRuntimeIds = [...capMap.entries()]
.filter(([, cap]) => cap && cap.role === 'runtime')
.map(([id]) => id)
.sort();
const runtimeKeys = Object.keys(registry.runtimes).sort(); const runtimeKeys = Object.keys(registry.runtimes).sort();
assert.strictEqual(runtimeKeys.length, 15, 'Expected 15 runtime entries, got: ' + runtimeKeys.join(', ')); assert.ok(expectedRuntimeIds.length > 0, 'expected at least one role:runtime capability on disk');
assert.deepEqual(runtimeKeys, expectedRuntimeIds,
'runtimes index must exactly equal the role:runtime capabilities on disk. got: ' + runtimeKeys.join(', '));
for (const id of RUNTIME_IDS) { for (const id of RUNTIME_IDS) {
assert.ok( assert.ok(
Object.prototype.hasOwnProperty.call(registry.runtimes, id), Object.prototype.hasOwnProperty.call(registry.runtimes, id),
@@ -4072,17 +4083,17 @@ describe('ADR-857 phase 5e: validateArtifactKindEntry — ConverterName enum (FA
assert.deepEqual(converterErrors, [], 'converter: null must always be accepted, got: ' + JSON.stringify(converterErrors)); assert.deepEqual(converterErrors, [], 'converter: null must always be accepted, got: ' + JSON.stringify(converterErrors));
}); });
// Parity: all 15 runtime descriptors must have converters in the valid set (or null) // Parity: every role:runtime descriptor's converters must be in the valid set (or null).
test('all 15 real runtime descriptors have converters in VALID_CONVERTER_NAMES or null', () => { // Count-agnostic: iterates the real capability map rather than a hand-pinned runtime list.
test('every real runtime descriptor has converters in VALID_CONVERTER_NAMES or null', () => {
const { capMap, errors } = loadAndValidate(new Set()); const { capMap, errors } = loadAndValidate(new Set());
const hardErrors = errors.filter((e) => !e.includes('pending-migration')); const hardErrors = errors.filter((e) => !e.includes('pending-migration'));
assert.deepEqual(hardErrors, [], 'Expected no hard errors from real capabilities, got: ' + JSON.stringify(hardErrors)); assert.deepEqual(hardErrors, [], 'Expected no hard errors from real capabilities, got: ' + JSON.stringify(hardErrors));
const runtimeIds = [ const runtimeIds = [...capMap.entries()]
'claude', 'codex', 'antigravity', 'cursor', 'opencode', .filter(([, cap]) => cap && cap.role === 'runtime')
'kilo', 'copilot', 'augment', 'trae', 'qwen', 'hermes', .map(([id]) => id);
'codebuddy', 'cline', 'kimi', 'windsurf', assert.ok(runtimeIds.length > 0, 'expected at least one role:runtime capability');
];
for (const id of runtimeIds) { for (const id of runtimeIds) {
const cap = capMap.get(id); const cap = capMap.get(id);
assert.ok(cap, 'capMap must contain "' + id + '"'); assert.ok(cap, 'capMap must contain "' + id + '"');
@@ -6406,45 +6417,43 @@ const {
resolveRuntimeConfigIntent, resolveRuntimeConfigIntent,
ALLOWED_CONFIG_RUNTIMES, ALLOWED_CONFIG_RUNTIMES,
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-config-adapter-registry.cjs')); } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-config-adapter-registry.cjs'));
const enh1055Registry = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'capability-registry.cjs'));
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Frozen expected table (pre-change behavior — the contract being pinned) // Expected table — DERIVED from the capability registry descriptors. The
// contract being pinned is the PROJECTION (descriptor fields → intent), not a
// frozen per-runtime value snapshot. Adding a runtime descriptor extends
// coverage with zero edits here.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const EXPECTED = [ const EXPECTED = Object.keys(enh1055Registry.runtimes).map((id) => {
{ runtime: 'claude', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, const r = enh1055Registry.runtimes[id].runtime;
{ runtime: 'antigravity', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, const pw = r.permissionWriter;
{ runtime: 'augment', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, return {
{ runtime: 'qwen', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, runtime: id,
{ runtime: 'hermes', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, installSurface: r.installSurface,
{ runtime: 'codebuddy', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, writesSharedSettings: r.writesSharedSettings,
{ runtime: 'opencode', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: 'opencode' }, finishPermissionWriter: pw == null ? null : pw,
{ runtime: 'kilo', installSurface: 'settings-json', writesSharedSettings: false, finishPermissionWriter: 'kilo' }, };
{ runtime: 'codex', installSurface: 'codex-toml', writesSharedSettings: false, finishPermissionWriter: null }, });
{ runtime: 'copilot', installSurface: 'copilot-instructions', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'cline', installSurface: 'cline-rules', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'cursor', installSurface: 'cursor-hooks-json', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'windsurf', installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'trae', installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'kimi', installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null },
];
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Test 1: Golden master — all 15 runtimes resolve to expected values // Test 1: Projection contract — every registry runtime resolves to its
// descriptor-derived intent (count-agnostic).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('enh-1055 descriptor-drive: resolveRuntimeConfigIntent golden master', () => { describe('enh-1055 descriptor-drive: resolveRuntimeConfigIntent projection contract', () => {
for (const row of EXPECTED) { test('every registry runtime resolves to its descriptor-derived intent', () => {
test(`${row.runtime} resolves to expected intent`, () => { assert.ok(EXPECTED.length > 0, 'registry must contain at least one runtime');
const intent = resolveRuntimeConfigIntent(row.runtime); for (const row of EXPECTED) {
assert.deepStrictEqual(intent, { assert.deepStrictEqual(resolveRuntimeConfigIntent(row.runtime), {
runtime: row.runtime, runtime: row.runtime,
installSurface: row.installSurface, installSurface: row.installSurface,
writesSharedSettings: row.writesSharedSettings, writesSharedSettings: row.writesSharedSettings,
finishPermissionWriter: row.finishPermissionWriter, finishPermissionWriter: row.finishPermissionWriter,
}); }, `resolveRuntimeConfigIntent('${row.runtime}') must match the descriptor projection`);
}); }
} });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -6470,21 +6479,18 @@ describe('enh-1055 descriptor-drive: unknown runtime throws TypeError', () => {
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Test 4: ALLOWED_CONFIG_RUNTIMES contains all 15 expected runtimes // Test 4: ALLOWED_CONFIG_RUNTIMES equals the registry runtimes that declare an
// installSurface (count-agnostic; derived from the same source as production).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('enh-1055 descriptor-drive: ALLOWED_CONFIG_RUNTIMES completeness', () => { describe('enh-1055 descriptor-drive: ALLOWED_CONFIG_RUNTIMES completeness', () => {
const EXPECTED_15 = new Set([ test('equals the registry runtimes that declare an installSurface', () => {
'claude', 'antigravity', 'augment', 'qwen', 'hermes', 'codebuddy', const descriptorAllowed = new Set(
'opencode', 'kilo', 'codex', 'copilot', 'cline', 'cursor', 'windsurf', 'trae', 'kimi', Object.entries(enh1055Registry.runtimes)
]); .filter(([, cap]) => cap && cap.runtime && typeof cap.runtime.installSurface === 'string')
.map(([id]) => id),
test('contains exactly the 15 expected runtimes', () => { );
assert.deepStrictEqual(new Set(ALLOWED_CONFIG_RUNTIMES), EXPECTED_15); assert.deepStrictEqual(new Set(ALLOWED_CONFIG_RUNTIMES), descriptorAllowed);
});
test('has exactly 15 entries', () => {
assert.strictEqual([...ALLOWED_CONFIG_RUNTIMES].length, 15);
}); });
}); });

View File

@@ -1,17 +1,20 @@
'use strict'; 'use strict';
/** /**
* Drift-guard: getDirName must be derived from the capability registry. * Property test: getDirName is a pure projection of each runtime descriptor's
* Verifies: * `localConfigDir`. Because 1.7.0 (ADR-1016 / ADR-1239) makes runtimes
* 1. For every known runtime id, getDirName(id) equals the hardcoded golden * pluggable data descriptors, this test asserts the DERIVATION CONTRACT —
* expected map — a pinned oracle that catches BOTH formula bugs AND * `getDirName(id) === registry.runtimes[id].runtime.localConfigDir` — for
* unintended registry drift (adding/removing a runtime or changing its * EVERY runtime currently in the registry, rather than pinning a frozen
* localConfigDir forces a deliberate golden-map update here). * per-runtime golden snapshot that would have to be hand-edited every time a
* 2. getDirName('unknown') and getDirName('') fall back to '.claude'. * runtime is added or removed. Adding a new runtime descriptor requires zero
* 3. Every registry runtime entry has a non-empty dot-dir localConfigDir string — * changes here; if the derivation breaks for any runtime, this fails loudly.
* cross-check from a different angle than the production derivation formula.
* *
* ADR-1239 Phase B (#1679). * Also covers:
* Behavioral tests only: assert on returned values, no source-grep. * - the fail-closed fallback (`getDirName('unknown')` / `getDirName('')` → '.claude');
* - a structural cross-check that every descriptor's localConfigDir is a
* non-empty dot-dir string.
*
* ADR-1239 Phase B (#1679). Behavioral tests only: assert on returned values.
*/ */
const { test } = require('node:test'); const { test } = require('node:test');
@@ -21,59 +24,26 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
const { getDirName } = runtimeNamePolicy; const { getDirName } = runtimeNamePolicy;
// Golden oracle: hardcoded expected map of all 15 runtime ids to their local config dir. const RUNTIME_IDS = Object.keys(registry.runtimes);
// A pinned expected value in a TEST is correct — the test IS the oracle (non-circular).
// Only PRODUCTION code should derive dynamically from the registry.
// If this map diverges from getDirName output, either the formula is wrong
// OR the registry changed — both require a deliberate golden-map update here.
const GOLDEN_DIR_MAP = {
claude: '.claude',
copilot: '.github',
opencode: '.opencode',
kilo: '.kilo',
codex: '.codex',
antigravity: '.agents',
cursor: '.cursor',
windsurf: '.windsurf',
augment: '.augment',
trae: '.trae',
qwen: '.qwen',
hermes: '.hermes',
kimi: '.kimi-code',
codebuddy: '.codebuddy',
cline: '.cline',
};
test('getDirName: golden map matches for all 15 known runtime ids', () => { test('getDirName(id) projects each descriptor runtime.localConfigDir (derivation contract, count-agnostic)', () => {
for (const [id, expected] of Object.entries(GOLDEN_DIR_MAP)) { assert.ok(RUNTIME_IDS.length > 0, 'registry must contain at least one runtime');
const actual = getDirName(id); for (const id of RUNTIME_IDS) {
const desc = registry.runtimes[id] && registry.runtimes[id].runtime;
const expected = desc && desc.localConfigDir;
assert.ok(typeof expected === 'string' && expected.length > 0,
`registry.runtimes['${id}'].runtime.localConfigDir must be a non-empty string`);
assert.strictEqual( assert.strictEqual(
actual, getDirName(id),
expected, expected,
`getDirName('${id}') diverged from golden.\n` + `getDirName('${id}') must equal the descriptor localConfigDir '${expected}'`);
` actual: ${JSON.stringify(actual)}\n` +
` expected: ${JSON.stringify(expected)}`,
);
} }
}); });
test('drift guard: registry runtime id set EXACTLY equals the golden map (adding/removing a runtime forces a golden update)', () => { test('getDirName fallback: unknown / empty runtime returns ".claude" (fail-closed)', () => {
// Without this, a newly-added runtime would pass (its value never checked) and assert.strictEqual(getDirName('unknown'), '.claude');
// removing `claude` could pass via the .claude fallback. Pin the set both ways. assert.strictEqual(getDirName(''), '.claude');
const registryIds = Object.keys(registry.runtimes).sort(); assert.strictEqual(getDirName('__nonexistent_runtime__'), '.claude');
const goldenIds = Object.keys(GOLDEN_DIR_MAP).sort();
assert.deepEqual(registryIds, goldenIds,
'registry.runtimes id set must exactly match GOLDEN_DIR_MAP — update the golden map when adding/removing a runtime');
});
test('getDirName fallback: unknown runtime returns ".claude"', () => {
assert.strictEqual(getDirName('unknown'), '.claude',
'getDirName("unknown") must return ".claude" (default fallback)');
});
test('getDirName fallback: empty string returns ".claude"', () => {
assert.strictEqual(getDirName(''), '.claude',
'getDirName("") must return ".claude" (empty-input fallback)');
}); });
test('registry cross-check: every runtimes[id].runtime.localConfigDir is a non-empty dot-dir string', () => { test('registry cross-check: every runtimes[id].runtime.localConfigDir is a non-empty dot-dir string', () => {

View File

@@ -56,6 +56,7 @@ const RUNTIME_META = {
qwen: { localDir: '.qwen', globalSuffix: '.qwen' }, qwen: { localDir: '.qwen', globalSuffix: '.qwen' },
trae: { localDir: '.trae', globalSuffix: '.trae' }, trae: { localDir: '.trae', globalSuffix: '.trae' },
windsurf: { localDir: '.windsurf', globalSuffix: path.join('.codeium', 'windsurf') }, windsurf: { localDir: '.windsurf', globalSuffix: path.join('.codeium', 'windsurf') },
zcode: { localDir: '.zcode', globalSuffix: '.zcode' },
}; };
// Runtimes that emit per-skill files under skills/ (not rules-based or commands-based) // Runtimes that emit per-skill files under skills/ (not rules-based or commands-based)

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
/** /**
* ADR-1239 Phase A: Descriptor tests — validate that all 15 role:runtime * ADR-1239 Phase A: Descriptor tests — validate that all 16 role:runtime
* capability descriptors have correct hostIntegration axes, pass the validator, * capability descriptors have correct hostIntegration axes, pass the validator,
* and negotiate correctly via the host-integration module. * and negotiate correctly via the host-integration module.
* *
@@ -31,16 +31,15 @@ const SCALAR_AXES = ['embeddingMode', 'commandSurface', 'modelMode', 'hookBus',
// All 6 dispatch sub-keys (includes backgroundDispatch added in feat/1679-dispatch-flatten) // All 6 dispatch sub-keys (includes backgroundDispatch added in feat/1679-dispatch-flatten)
const DISPATCH_KEYS = ['namedDispatch', 'nested', 'maxDepth', 'background', 'subagentToolkit', 'backgroundDispatch']; const DISPATCH_KEYS = ['namedDispatch', 'nested', 'maxDepth', 'background', 'subagentToolkit', 'backgroundDispatch'];
// All 15 runtime IDs (ordered alphabetically) // Runtime ids are derived from the registry (the single source of truth) so the
const RUNTIME_IDS = [ // suite stays fluid when a runtime descriptor is added or removed. The profile
'antigravity', 'augment', 'claude', 'cline', 'codebuddy', // and flatten maps below remain CURATED pins — they catch an accidental axis
'codex', 'copilot', 'cursor', 'hermes', // flip (e.g. a descriptor changing embeddingMode silently moves its profile).
'kilo', 'kimi', 'opencode', 'qwen', 'trae', 'windsurf', const RUNTIME_IDS = Object.keys(registry.runtimes);
];
// Contract-pinned profile split (derived from .host-cli-final.json): // Contract-pinned profile split (derived from .host-cli-final.json):
// programmatic-cli: claude, cline, cursor, hermes, kilo, kimi, opencode, qwen, trae (9) // programmatic-cli: claude, cline, cursor, hermes, kilo, kimi, opencode, qwen, trae (9)
// declarative-cli: antigravity, augment, codebuddy, codex, copilot, windsurf (6) // declarative-cli: antigravity, augment, codebuddy, codex, copilot, windsurf, zcode (7)
// ide: 0 // ide: 0
const EXPECTED_PROFILES = { const EXPECTED_PROFILES = {
claude: 'programmatic-cli', claude: 'programmatic-cli',
@@ -58,22 +57,26 @@ const EXPECTED_PROFILES = {
codex: 'declarative-cli', codex: 'declarative-cli',
copilot: 'declarative-cli', copilot: 'declarative-cli',
windsurf: 'declarative-cli', windsurf: 'declarative-cli',
zcode: 'declarative-cli',
}; };
describe('ADR-1239 Phase A: hostIntegration descriptors', () => { describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
// ─── Registry shape ────────────────────────────────────────────────────────── // ─── Registry shape ──────────────────────────────────────────────────────────
test('registry.runtimes contains all 15 expected runtime ids', () => { test('registry.runtimes exactly equals the curated RUNTIME_IDS set (count-agnostic)', () => {
// RUNTIME_IDS is derived from the registry above, so this asserts internal
// consistency: the curated profile/flatten maps cover every registry runtime
// exactly once, no matter how many exist.
for (const id of RUNTIME_IDS) { for (const id of RUNTIME_IDS) {
assert.ok( assert.ok(
Object.prototype.hasOwnProperty.call(registry.runtimes, id), Object.prototype.hasOwnProperty.call(registry.runtimes, id),
'registry.runtimes must contain "' + id + '"', 'registry.runtimes must contain "' + id + '"',
); );
} }
assert.strictEqual( assert.deepStrictEqual(
Object.keys(registry.runtimes).length, Object.keys(registry.runtimes).sort(),
15, [...RUNTIME_IDS].sort(),
'registry.runtimes must have exactly 15 entries', 'registry.runtimes key set must match RUNTIME_IDS',
); );
}); });
@@ -208,7 +211,10 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
// ─── Contract-pin profile split ─────────────────────────────────────────────── // ─── Contract-pin profile split ───────────────────────────────────────────────
test('contract-pin: exactly 9 programmatic-cli, 6 declarative-cli, 0 ide', () => { test('contract-pin: profile split is internally consistent with EXPECTED_PROFILES (count-agnostic)', () => {
// The counts are DERIVED from the curated EXPECTED_PROFILES map rather than
// hand-pinned, so adding a runtime + its profile entry updates the counts
// automatically. ide must remain 0 (no installed ide-profile host yet).
const counts = { 'programmatic-cli': 0, 'declarative-cli': 0, 'ide': 0 }; const counts = { 'programmatic-cli': 0, 'declarative-cli': 0, 'ide': 0 };
for (const id of RUNTIME_IDS) { for (const id of RUNTIME_IDS) {
const cap = registry.runtimes[id]; const cap = registry.runtimes[id];
@@ -216,13 +222,20 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
assert.ok(hi, id + ': hostIntegration must exist for profile count'); assert.ok(hi, id + ': hostIntegration must exist for profile count');
const profile = profileOf(hi); const profile = profileOf(hi);
assert.ok(profile !== null, id + ': profileOf must be non-null'); assert.ok(profile !== null, id + ': profileOf must be non-null');
assert.strictEqual(profile, EXPECTED_PROFILES[id],
id + ': profileOf must match EXPECTED_PROFILES (an axis may have flipped)');
if (counts[profile] !== undefined) { if (counts[profile] !== undefined) {
counts[profile]++; counts[profile]++;
} }
} }
assert.strictEqual(counts['programmatic-cli'], 9, 'Must have exactly 9 programmatic-cli runtimes'); // Derived expected counts from the curated map itself.
assert.strictEqual(counts['declarative-cli'], 6, 'Must have exactly 6 declarative-cli runtimes'); const expectedCounts = { 'programmatic-cli': 0, 'declarative-cli': 0, 'ide': 0 };
assert.strictEqual(counts['ide'], 0, 'Must have exactly 0 ide runtimes'); for (const p of Object.values(EXPECTED_PROFILES)) {
if (expectedCounts[p] !== undefined) expectedCounts[p]++;
}
assert.strictEqual(counts['programmatic-cli'], expectedCounts['programmatic-cli']);
assert.strictEqual(counts['declarative-cli'], expectedCounts['declarative-cli']);
assert.strictEqual(counts['ide'], 0, 'no installed host may carry the ide profile yet');
}); });
// ─── backgroundDispatch presence ───────────────────────────────────────────── // ─── backgroundDispatch presence ─────────────────────────────────────────────
@@ -249,7 +262,7 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
// ─── shouldFlattenDispatch per-host (#853 discriminator) ───────────────────── // ─── shouldFlattenDispatch per-host (#853 discriminator) ─────────────────────
// Expected: false (may background) for codex and cursor ONLY; true (must inline) for the other 14. // Expected: false (may background) for codex and cursor ONLY; true (must inline) for the other 15.
const EXPECTED_FLATTEN = { const EXPECTED_FLATTEN = {
antigravity: true, antigravity: true,
augment: true, augment: true,
@@ -266,6 +279,7 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
qwen: true, qwen: true,
trae: true, trae: true,
windsurf: true, windsurf: true,
zcode: true,
}; };
for (const id of RUNTIME_IDS) { for (const id of RUNTIME_IDS) {
@@ -282,17 +296,20 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
}); });
} }
test('contract-pin: exactly 2 hosts are background-eligible (shouldFlattenDispatch === false): codex and cursor', () => { test('contract-pin: background-eligible set matches EXPECTED_FLATTEN (count-agnostic)', () => {
// The eligible set is DERIVED from the curated EXPECTED_FLATTEN map rather
// than hand-pinned to a fixed pair, so a runtime whose dispatch axes change
// updates the expectation automatically.
const expectedEligible = RUNTIME_IDS
.filter((id) => EXPECTED_FLATTEN[id] === false)
.sort();
const eligible = RUNTIME_IDS.filter((id) => { const eligible = RUNTIME_IDS.filter((id) => {
const cap = registry.runtimes[id]; const cap = registry.runtimes[id];
const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch; const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch;
return dispatch && shouldFlattenDispatch(dispatch) === false; return dispatch && shouldFlattenDispatch(dispatch) === false;
}); }).sort();
assert.deepEqual( assert.deepEqual(eligible, expectedEligible,
eligible.slice().sort(), 'background-eligible set must match EXPECTED_FLATTEN (a dispatch axis may have flipped)');
['codex', 'cursor'],
'Exactly codex and cursor must be background-eligible, got: ' + JSON.stringify(eligible.sort()),
);
}); });
test('contract-pin: spot-check claude→programmatic-cli, codex→declarative-cli, opencode→programmatic-cli, windsurf→declarative-cli', () => { test('contract-pin: spot-check claude→programmatic-cli, codex→declarative-cli, opencode→programmatic-cli, windsurf→declarative-cli', () => {

View File

@@ -13,6 +13,8 @@
* removed from GSD (Antigravity CLI is the successor). The runtime option * removed from GSD (Antigravity CLI is the successor). The runtime option
* numbering below is renumbered accordingly — option 9 (formerly gemini) is * numbering below is renumbered accordingly — option 9 (formerly gemini) is
* now hermes, and the "All" shortcut moved from 17 to 16. * now hermes, and the "All" shortcut moved from 17 to 16.
*
* #1925: ZCode (Z.ai) added as option 16; the "All" shortcut moved from 16 to 17.
*/ */
process.env.GSD_TEST_MODE = '1'; process.env.GSD_TEST_MODE = '1';
@@ -86,23 +88,27 @@ describe('multi-runtime selection parsing', () => {
assert.deepStrictEqual(parseRuntimeInput('15'), ['windsurf']); assert.deepStrictEqual(parseRuntimeInput('15'), ['windsurf']);
}); });
test('single choice for zcode', () => {
assert.deepStrictEqual(parseRuntimeInput('16'), ['zcode']);
});
test('single choice for kimi', () => { test('single choice for kimi', () => {
assert.deepStrictEqual(parseRuntimeInput('10'), ['kimi']); assert.deepStrictEqual(parseRuntimeInput('10'), ['kimi']);
}); });
test('choice 16 returns all runtimes', () => { test('choice 17 returns all runtimes', () => {
assert.deepStrictEqual(parseRuntimeInput('16'), allRuntimes); assert.deepStrictEqual(parseRuntimeInput('17'), allRuntimes);
}); });
test('choice 16 returns all runtimes when mixed with separators or other tokens', () => { test('choice 17 returns all runtimes when mixed with separators or other tokens', () => {
// CR feedback: tokenized inputs that include 16 (e.g. trailing comma, or // CR feedback: tokenized inputs that include 17 (e.g. trailing comma, or
// alongside other choices) must still expand to all-runtimes — previously // alongside other choices) must still expand to all-runtimes — previously
// only the bare all-runtimes option matched, so "16," or "16 1" silently installed a // only the bare all-runtimes option matched, so "17," or "17 1" silently installed a
// subset. // subset.
assert.deepStrictEqual(parseRuntimeInput('16,'), allRuntimes); assert.deepStrictEqual(parseRuntimeInput('17,'), allRuntimes);
assert.deepStrictEqual(parseRuntimeInput('16 1'), allRuntimes); assert.deepStrictEqual(parseRuntimeInput('17 1'), allRuntimes);
assert.deepStrictEqual(parseRuntimeInput('1,16'), allRuntimes); assert.deepStrictEqual(parseRuntimeInput('1,17'), allRuntimes);
assert.deepStrictEqual(parseRuntimeInput(' 16 '), allRuntimes); assert.deepStrictEqual(parseRuntimeInput(' 17 '), allRuntimes);
}); });
test('empty input defaults to claude', () => { test('empty input defaults to claude', () => {
@@ -111,13 +117,13 @@ describe('multi-runtime selection parsing', () => {
}); });
test('invalid choices are ignored, falls back to claude if all invalid', () => { test('invalid choices are ignored, falls back to claude if all invalid', () => {
assert.deepStrictEqual(parseRuntimeInput('17'), ['claude']); assert.deepStrictEqual(parseRuntimeInput('18'), ['claude']);
assert.deepStrictEqual(parseRuntimeInput('0'), ['claude']); assert.deepStrictEqual(parseRuntimeInput('0'), ['claude']);
assert.deepStrictEqual(parseRuntimeInput('abc'), ['claude']); assert.deepStrictEqual(parseRuntimeInput('abc'), ['claude']);
}); });
test('invalid choices mixed with valid are filtered out', () => { test('invalid choices mixed with valid are filtered out', () => {
assert.deepStrictEqual(parseRuntimeInput('1,17,7'), ['claude', 'copilot']); assert.deepStrictEqual(parseRuntimeInput('1,18,7'), ['claude', 'copilot']);
assert.deepStrictEqual(parseRuntimeInput('abc 3 xyz'), ['augment']); assert.deepStrictEqual(parseRuntimeInput('abc 3 xyz'), ['augment']);
}); });
@@ -149,11 +155,12 @@ describe('install.js exports multi-select runtime metadata', () => {
'13': 'qwen', '13': 'qwen',
'14': 'trae', '14': 'trae',
'15': 'windsurf', '15': 'windsurf',
'16': 'zcode',
}; };
const expectedRuntimes = [ const expectedRuntimes = [
'claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex',
'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode',
'qwen', 'trae', 'windsurf', 'qwen', 'trae', 'windsurf', 'zcode',
]; ];
test('runtimeMap exports every option key bound to the right runtime', () => { test('runtimeMap exports every option key bound to the right runtime', () => {
@@ -170,14 +177,18 @@ describe('install.js exports multi-select runtime metadata', () => {
'allRuntimes has no duplicates'); 'allRuntimes has no duplicates');
}); });
test('"All" shortcut (option 16) selects every runtime', () => { test('"All" shortcut (option 17) selects every runtime', () => {
assert.deepStrictEqual(parseRuntimeInput('16'), allRuntimes); assert.deepStrictEqual(parseRuntimeInput('17'), allRuntimes);
}); });
test('--kimi flag selects Kimi without interactive prompt', () => { test('--kimi flag selects Kimi without interactive prompt', () => {
assert.deepStrictEqual(selectRuntimesFromArgs(['--kimi']), ['kimi']); assert.deepStrictEqual(selectRuntimesFromArgs(['--kimi']), ['kimi']);
}); });
test('--zcode flag selects ZCode without interactive prompt', () => {
assert.deepStrictEqual(selectRuntimesFromArgs(['--zcode']), ['zcode']);
});
test('--all flag includes Kimi exactly once', () => { test('--all flag includes Kimi exactly once', () => {
const selected = selectRuntimesFromArgs(['--all']); const selected = selectRuntimesFromArgs(['--all']);
assert.ok(selected.includes('kimi'), '--all includes kimi'); assert.ok(selected.includes('kimi'), '--all includes kimi');
@@ -185,7 +196,14 @@ describe('install.js exports multi-select runtime metadata', () => {
'--all includes kimi exactly once'); '--all includes kimi exactly once');
}); });
test('prompt lists Hermes Agent (9), Kimi (10), Qwen Code (13), Trae (14), and All (16)', () => { test('--all flag includes ZCode exactly once', () => {
const selected = selectRuntimesFromArgs(['--all']);
assert.ok(selected.includes('zcode'), '--all includes zcode');
assert.strictEqual(selected.filter((runtime) => runtime === 'zcode').length, 1,
'--all includes zcode exactly once');
});
test('prompt lists ZCode (16), and All (17)', () => {
const prompt = stripAnsi(buildRuntimePromptText()); const prompt = stripAnsi(buildRuntimePromptText());
assert.ok(/\b9\)\s*Hermes Agent\b/.test(prompt), assert.ok(/\b9\)\s*Hermes Agent\b/.test(prompt),
'prompt lists Hermes Agent as option 9'); 'prompt lists Hermes Agent as option 9');
@@ -197,8 +215,10 @@ describe('install.js exports multi-select runtime metadata', () => {
'prompt lists Qwen Code as option 13'); 'prompt lists Qwen Code as option 13');
assert.ok(/\b14\)\s*Trae\b/.test(prompt), assert.ok(/\b14\)\s*Trae\b/.test(prompt),
'prompt lists Trae as option 14'); 'prompt lists Trae as option 14');
assert.ok(/\b16\)\s*All\b/.test(prompt), assert.ok(/\b16\)\s*ZCode\b/.test(prompt),
'prompt lists All as option 16'); 'prompt lists ZCode as option 16');
assert.ok(/\b17\)\s*All\b/.test(prompt),
'prompt lists All as option 17');
}); });
test('prompt does not list Gemini (removed #1928)', () => { test('prompt does not list Gemini (removed #1928)', () => {

View File

@@ -1,7 +1,16 @@
'use strict'; 'use strict';
// Tests for runtime-config-adapter-registry.cjs (issue #60). // Tests for runtime-config-adapter-registry.cjs (issue #60).
// TDD: this file is written BEFORE the implementation to establish the red state. //
// 1.7.0 (ADR-1016 / ADR-1239) makes runtimes pluggable data descriptors, and
// resolveRuntimeConfigIntent / resolveInstallPlan are PURE PROJECTIONS of those
// descriptors. So this file asserts the PROJECTION CONTRACT — that each
// function maps descriptor fields to the intent/plan shape correctly (right
// field names, right null-handling, right types) — for EVERY runtime in the
// registry, rather than pinning a frozen per-runtime value snapshot that would
// have to be hand-edited on every runtime addition. The EXPECTED_TABLE below is
// DERIVED from the capability registry at load time; adding a runtime descriptor
// requires zero changes here.
const { describe, test } = require('node:test'); const { describe, test } = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
@@ -15,45 +24,44 @@ const {
ALLOWED_CONFIG_RUNTIMES, ALLOWED_CONFIG_RUNTIMES,
INSTALL_SURFACES, INSTALL_SURFACES,
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-config-adapter-registry.cjs')); } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-config-adapter-registry.cjs'));
const registry = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'capability-registry.cjs'));
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Source-of-truth table (mirrors the intent table in the brief exactly) // Source-of-truth table — DERIVED from the capability registry descriptors.
// Each row is the descriptor projection of one runtime's config intent. This is
// deliberately non-circular: production reads the descriptor too, and this
// asserts the mapping (field names, null-coalescing of permissionWriter, etc.)
// is correct for every present and future runtime.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const EXPECTED_TABLE = [ const EXPECTED_TABLE = Object.keys(registry.runtimes).map((id) => {
{ runtime: 'claude', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, const r = registry.runtimes[id].runtime;
{ runtime: 'antigravity', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, const pw = r.permissionWriter;
{ runtime: 'augment', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, return {
{ runtime: 'qwen', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, runtime: id,
{ runtime: 'hermes', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, installSurface: r.installSurface,
{ runtime: 'codebuddy', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }, writesSharedSettings: r.writesSharedSettings,
{ runtime: 'opencode', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: 'opencode' }, finishPermissionWriter: pw == null ? null : pw,
{ runtime: 'kilo', installSurface: 'settings-json', writesSharedSettings: false, finishPermissionWriter: 'kilo' }, };
{ runtime: 'codex', installSurface: 'codex-toml', writesSharedSettings: false, finishPermissionWriter: null }, });
{ runtime: 'copilot', installSurface: 'copilot-instructions', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'cline', installSurface: 'cline-rules', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'cursor', installSurface: 'cursor-hooks-json', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'windsurf', installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'trae', installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null },
{ runtime: 'kimi', installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null },
];
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Test 1: Table-lock — every row in EXPECTED_TABLE must match exactly // Test 1: Projection contract — every registry runtime resolves to its
// descriptor-derived intent (count-agnostic).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('resolveRuntimeConfigIntent — table-lock', () => { describe('resolveRuntimeConfigIntent — projection contract', () => {
for (const row of EXPECTED_TABLE) { test('every registry runtime resolves to its descriptor-derived intent', () => {
test(`${row.runtime} resolves to expected intent`, () => { assert.ok(EXPECTED_TABLE.length > 0, 'registry must contain at least one runtime');
const intent = resolveRuntimeConfigIntent(row.runtime); for (const row of EXPECTED_TABLE) {
assert.deepStrictEqual(intent, { assert.deepStrictEqual(resolveRuntimeConfigIntent(row.runtime), {
runtime: row.runtime, runtime: row.runtime,
installSurface: row.installSurface, installSurface: row.installSurface,
writesSharedSettings: row.writesSharedSettings, writesSharedSettings: row.writesSharedSettings,
finishPermissionWriter: row.finishPermissionWriter, finishPermissionWriter: row.finishPermissionWriter,
}); }, `resolveRuntimeConfigIntent('${row.runtime}') must match the descriptor projection`);
}); }
} });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -95,31 +103,28 @@ describe('resolveRuntimeConfigIntent — unknown runtime throws TypeError', () =
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Test 3: writesSharedSettings exclusion equivalence // Test 3: writesSharedSettings — derived equivalence (count-agnostic).
// The runtimes resolving to false are exactly those whose descriptor declares
// writesSharedSettings===false.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('writesSharedSettings exclusion equivalence', () => { describe('writesSharedSettings — descriptor-driven equivalence', () => {
const EXPECTED_FALSE_SET = new Set(['codex', 'copilot', 'kilo', 'cursor', 'windsurf', 'trae', 'cline', 'kimi']); test('runtimes resolving writesSharedSettings===false are exactly the descriptor-declared false set', () => {
test('runtimes with writesSharedSettings===false are exactly the exclusion set', () => {
const falseRuntimes = EXPECTED_TABLE const falseRuntimes = EXPECTED_TABLE
.filter(r => r.writesSharedSettings === false) .filter(r => r.writesSharedSettings === false)
.map(r => r.runtime); .map(r => r.runtime)
assert.deepStrictEqual(new Set(falseRuntimes), EXPECTED_FALSE_SET); .sort();
}); const descriptorFalse = Object.keys(registry.runtimes)
.filter((id) => registry.runtimes[id].runtime.writesSharedSettings === false)
test('all other supported runtimes have writesSharedSettings===true', () => { .sort();
const trueRuntimes = EXPECTED_TABLE assert.deepStrictEqual(falseRuntimes, descriptorFalse);
.filter(r => r.writesSharedSettings === true)
.map(r => r.runtime);
for (const runtime of trueRuntimes) {
assert.ok(!EXPECTED_FALSE_SET.has(runtime), `${runtime} should have writesSharedSettings true`);
}
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Test 4: finishPermissionWriter correctness // Test 4: finishPermissionWriter — opencode/kilo are non-null, the rest null.
// Spot-check the two non-null writers (stable curated values) plus the
// descriptor-derived null set.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('finishPermissionWriter', () => { describe('finishPermissionWriter', () => {
@@ -131,60 +136,39 @@ describe('finishPermissionWriter', () => {
assert.strictEqual(resolveRuntimeConfigIntent('kilo').finishPermissionWriter, 'kilo'); assert.strictEqual(resolveRuntimeConfigIntent('kilo').finishPermissionWriter, 'kilo');
}); });
test('every other supported runtime -> null', () => { test('every registry runtime whose descriptor permissionWriter is null/absent resolves to null', () => {
const nullExpected = EXPECTED_TABLE for (const row of EXPECTED_TABLE.filter((r) => r.finishPermissionWriter === null)) {
.filter(r => r.finishPermissionWriter === null)
.map(r => r.runtime);
for (const runtime of nullExpected) {
assert.strictEqual( assert.strictEqual(
resolveRuntimeConfigIntent(runtime).finishPermissionWriter, resolveRuntimeConfigIntent(row.runtime).finishPermissionWriter,
null, null,
`${runtime} should have finishPermissionWriter null`, `${row.runtime} should have finishPermissionWriter null`,
); );
} }
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Test 5: Distinct dedicated surfaces // Test 5: installSurface — spot-check the stable dedicated surfaces, plus a
// descriptor-driven assertion that every runtime resolves to its declared
// surface (count-agnostic).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('installSurface correctness', () => { describe('installSurface correctness', () => {
test('codex -> "codex-toml"', () => { test('dedicated surfaces are stable (spot-check)', () => {
assert.strictEqual(resolveRuntimeConfigIntent('codex').installSurface, 'codex-toml'); assert.strictEqual(resolveRuntimeConfigIntent('codex').installSurface, 'codex-toml');
});
test('copilot -> "copilot-instructions"', () => {
assert.strictEqual(resolveRuntimeConfigIntent('copilot').installSurface, 'copilot-instructions'); assert.strictEqual(resolveRuntimeConfigIntent('copilot').installSurface, 'copilot-instructions');
});
test('cline -> "cline-rules"', () => {
assert.strictEqual(resolveRuntimeConfigIntent('cline').installSurface, 'cline-rules'); assert.strictEqual(resolveRuntimeConfigIntent('cline').installSurface, 'cline-rules');
});
test('cursor -> "cursor-hooks-json"', () => {
assert.strictEqual(resolveRuntimeConfigIntent('cursor').installSurface, 'cursor-hooks-json'); assert.strictEqual(resolveRuntimeConfigIntent('cursor').installSurface, 'cursor-hooks-json');
});
test('windsurf -> "profile-marker-only"', () => {
assert.strictEqual(resolveRuntimeConfigIntent('windsurf').installSurface, 'profile-marker-only'); assert.strictEqual(resolveRuntimeConfigIntent('windsurf').installSurface, 'profile-marker-only');
});
test('trae -> "profile-marker-only"', () => {
assert.strictEqual(resolveRuntimeConfigIntent('trae').installSurface, 'profile-marker-only'); assert.strictEqual(resolveRuntimeConfigIntent('trae').installSurface, 'profile-marker-only');
}); });
test('kimi -> "profile-marker-only"', () => { test('every registry runtime resolves to its descriptor-declared installSurface', () => {
assert.strictEqual(resolveRuntimeConfigIntent('kimi').installSurface, 'profile-marker-only'); for (const row of EXPECTED_TABLE) {
});
test('the 6 passthroughs + opencode + kilo -> "settings-json"', () => {
const settingsJsonRuntimes = ['claude', 'antigravity', 'augment', 'qwen', 'hermes', 'codebuddy', 'opencode', 'kilo'];
for (const runtime of settingsJsonRuntimes) {
assert.strictEqual( assert.strictEqual(
resolveRuntimeConfigIntent(runtime).installSurface, resolveRuntimeConfigIntent(row.runtime).installSurface,
'settings-json', row.installSurface,
`${runtime} should have installSurface "settings-json"`, `${row.runtime} must resolve its descriptor installSurface`,
); );
} }
}); });
@@ -207,19 +191,19 @@ describe('resolveRuntimeConfigIntent — fresh object each call', () => {
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Test 7: Completeness (AC#4 table-driven) — ALLOWED_CONFIG_RUNTIMES // Test 7: Completeness — ALLOWED_CONFIG_RUNTIMES equals the set of registry
// runtimes that declare an installSurface (count-agnostic; derived from the
// same source as the production Set).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('ALLOWED_CONFIG_RUNTIMES completeness', () => { describe('ALLOWED_CONFIG_RUNTIMES completeness', () => {
const EXPECTED_15 = new Set([ test('ALLOWED_CONFIG_RUNTIMES equals the registry runtimes that declare an installSurface', () => {
'claude', 'antigravity', 'augment', 'qwen', 'hermes', 'codebuddy', const descriptorAllowed = new Set(
'opencode', 'kilo', 'codex', 'copilot', 'cline', 'cursor', 'windsurf', 'trae', Object.entries(registry.runtimes)
'kimi', .filter(([, cap]) => cap && cap.runtime && typeof cap.runtime.installSurface === 'string')
]); .map(([id]) => id),
);
test('ALLOWED_CONFIG_RUNTIMES contains exactly the 15 expected runtimes', () => { assert.deepStrictEqual(new Set(ALLOWED_CONFIG_RUNTIMES), descriptorAllowed);
const runtimeSet = new Set(ALLOWED_CONFIG_RUNTIMES);
assert.deepStrictEqual(runtimeSet, EXPECTED_15);
}); });
test('every member of ALLOWED_CONFIG_RUNTIMES resolves without throwing', () => { test('every member of ALLOWED_CONFIG_RUNTIMES resolves without throwing', () => {
@@ -227,10 +211,6 @@ describe('ALLOWED_CONFIG_RUNTIMES completeness', () => {
assert.doesNotThrow(() => resolveRuntimeConfigIntent(runtime), `${runtime} should resolve without throwing`); assert.doesNotThrow(() => resolveRuntimeConfigIntent(runtime), `${runtime} should resolve without throwing`);
} }
}); });
test('ALLOWED_CONFIG_RUNTIMES has exactly 15 entries', () => {
assert.strictEqual([...ALLOWED_CONFIG_RUNTIMES].length, 15);
});
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -304,240 +284,79 @@ describe('resolveInstallPlan — hooksSurface is descriptor-owned', () => {
}); });
}); });
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/enh-1082-install-plan-capstone.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:enh-1082-install-plan-capstone (consolidation epic #1969 B3 #1972)", () => {
'use strict';
/**
* Golden-master test for resolveInstallPlan — ADR-857 phase 5g capstone.
*
* Pins the exact InstallPlan shape for all 15 runtimes to guard against
* descriptor drift. Derived from actual resolveInstallPlan output at the time
* the seam was introduced (2026-06-11). Behavioral: calls the exported
* function and asserts on typed fields — no source-grep.
*/
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Frozen golden-master table — derived from actual resolveInstallPlan output // resolveInstallPlan — descriptor-projection contract (replaces the frozen
// per-runtime golden master). Asserts that for EVERY registry runtime,
// resolveInstallPlan(id) deep-equals the plan built directly from that runtime's
// descriptor fields. Count-agnostic: adding a runtime descriptor extends
// coverage with zero edits here. Folded from enh-1082 (consolidation epic #1969).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const EXPECTED = { describe('resolveInstallPlan — descriptor-projection contract (count-agnostic)', () => {
claude: { const RUNTIME_IDS = Object.keys(registry.runtimes);
runtime: 'claude',
installSurface: 'settings-json',
writesSharedSettings: true,
finishPermissionWriter: null,
hookEvents: 'claude',
extendedHookEvents: ['SubagentStop', 'Stop', 'PreCompact', 'FileChanged'],
hooksSurface: 'settings-json',
sandboxTier: 'none',
},
codex: {
runtime: 'codex',
installSurface: 'codex-toml',
writesSharedSettings: false,
finishPermissionWriter: null,
hookEvents: 'claude',
extendedHookEvents: [],
hooksSurface: 'codex-hooks-json',
sandboxTier: 'codex-agent-sandbox',
},
antigravity: {
runtime: 'antigravity',
installSurface: 'settings-json',
writesSharedSettings: true,
finishPermissionWriter: null,
hookEvents: 'gemini',
extendedHookEvents: [],
hooksSurface: 'settings-json',
sandboxTier: 'none',
},
cursor: {
runtime: 'cursor',
installSurface: 'cursor-hooks-json',
writesSharedSettings: false,
finishPermissionWriter: null,
hookEvents: 'claude',
extendedHookEvents: [],
hooksSurface: 'cursor-hooks-json',
sandboxTier: 'none',
},
opencode: {
runtime: 'opencode',
installSurface: 'settings-json',
writesSharedSettings: true,
finishPermissionWriter: 'opencode',
hookEvents: undefined,
extendedHookEvents: [],
hooksSurface: 'none',
sandboxTier: 'none',
},
kilo: {
runtime: 'kilo',
installSurface: 'settings-json',
writesSharedSettings: false,
finishPermissionWriter: 'kilo',
hookEvents: undefined,
extendedHookEvents: [],
hooksSurface: 'none',
sandboxTier: 'none',
},
copilot: {
runtime: 'copilot',
installSurface: 'copilot-instructions',
writesSharedSettings: false,
finishPermissionWriter: null,
hookEvents: undefined,
extendedHookEvents: [],
hooksSurface: 'copilot-inline',
sandboxTier: 'none',
},
augment: {
runtime: 'augment',
installSurface: 'settings-json',
writesSharedSettings: true,
finishPermissionWriter: null,
hookEvents: 'claude',
extendedHookEvents: [],
hooksSurface: 'settings-json',
sandboxTier: 'none',
},
trae: {
runtime: 'trae',
installSurface: 'profile-marker-only',
writesSharedSettings: false,
finishPermissionWriter: null,
hookEvents: undefined,
extendedHookEvents: [],
hooksSurface: 'none',
sandboxTier: 'none',
},
qwen: {
runtime: 'qwen',
installSurface: 'settings-json',
writesSharedSettings: true,
finishPermissionWriter: null,
hookEvents: 'claude',
extendedHookEvents: ['SubagentStop', 'Stop', 'PreCompact'],
hooksSurface: 'settings-json',
sandboxTier: 'none',
},
hermes: {
runtime: 'hermes',
installSurface: 'settings-json',
writesSharedSettings: true,
finishPermissionWriter: null,
hookEvents: 'claude',
extendedHookEvents: [],
hooksSurface: 'settings-json',
sandboxTier: 'none',
},
codebuddy: {
runtime: 'codebuddy',
installSurface: 'settings-json',
writesSharedSettings: true,
finishPermissionWriter: null,
hookEvents: 'claude',
extendedHookEvents: [],
hooksSurface: 'settings-json',
sandboxTier: 'none',
},
cline: {
runtime: 'cline',
installSurface: 'cline-rules',
writesSharedSettings: false,
finishPermissionWriter: null,
hookEvents: undefined,
extendedHookEvents: [],
hooksSurface: 'cline-rules',
sandboxTier: 'none',
},
kimi: {
runtime: 'kimi',
installSurface: 'profile-marker-only',
writesSharedSettings: false,
finishPermissionWriter: null,
hookEvents: undefined,
extendedHookEvents: [],
hooksSurface: 'none',
sandboxTier: 'none',
},
windsurf: {
runtime: 'windsurf',
installSurface: 'profile-marker-only',
writesSharedSettings: false,
finishPermissionWriter: null,
hookEvents: undefined,
extendedHookEvents: [],
hooksSurface: 'none',
sandboxTier: 'none',
},
};
const ALL_RUNTIMES = Object.keys(EXPECTED); test('covers every registry runtime', () => {
assert.ok(RUNTIME_IDS.length > 0, 'registry must contain at least one runtime');
describe('resolveInstallPlan — ADR-857 phase 5g golden master', () => {
it('covers exactly 15 runtimes', () => {
assert.strictEqual(ALL_RUNTIMES.length, 15);
}); });
for (const runtime of ALL_RUNTIMES) { // Build the expected plan directly from each descriptor — the same mapping
it(`resolveInstallPlan('${runtime}') matches frozen plan`, () => { // resolveInstallPlan performs, asserted rather than trusted.
const actual = resolveInstallPlan(runtime); function expectedPlanFromDescriptor(id) {
assert.deepStrictEqual(actual, EXPECTED[runtime], const desc = registry.runtimes[id].runtime;
`InstallPlan for '${runtime}' drifted from golden master`); const pw = desc.permissionWriter;
return {
runtime: id,
installSurface: desc.installSurface,
writesSharedSettings: desc.writesSharedSettings,
finishPermissionWriter: pw == null ? null : pw,
hookEvents: desc.hookEvents,
extendedHookEvents: Array.isArray(desc.extendedHookEvents) ? [...desc.extendedHookEvents] : [],
hooksSurface: desc.hooksSurface,
sandboxTier: desc.sandboxTier,
};
}
for (const id of RUNTIME_IDS) {
test(`resolveInstallPlan('${id}') matches the descriptor projection`, () => {
assert.deepStrictEqual(
resolveInstallPlan(id),
expectedPlanFromDescriptor(id),
`InstallPlan for '${id}' diverged from its descriptor projection`,
);
}); });
} }
it('resolveInstallPlan throws TypeError for unknown runtime', () => { test('resolveInstallPlan throws TypeError for unknown runtime', () => {
assert.throws( assert.throws(
() => resolveInstallPlan('bogus'), () => resolveInstallPlan('bogus'),
(err) => err instanceof TypeError && /bogus/.test(err.message), (err) => err instanceof TypeError && /bogus/.test(err.message),
); );
}); });
it('extendedHookEvents is always an array for every runtime', () => { test('extendedHookEvents is always an array for every runtime', () => {
for (const runtime of ALL_RUNTIMES) { for (const id of RUNTIME_IDS) {
const plan = resolveInstallPlan(runtime); assert.ok(Array.isArray(resolveInstallPlan(id).extendedHookEvents),
assert.ok(Array.isArray(plan.extendedHookEvents), `${id}: extendedHookEvents should be an array`);
`${runtime}: extendedHookEvents should be an array`);
} }
}); });
it('hooksSurface is always a non-empty string for every runtime', () => { test('hooksSurface is always a non-empty string for every runtime', () => {
for (const runtime of ALL_RUNTIMES) { for (const id of RUNTIME_IDS) {
const plan = resolveInstallPlan(runtime); const plan = resolveInstallPlan(id);
assert.strictEqual(typeof plan.hooksSurface, 'string', assert.strictEqual(typeof plan.hooksSurface, 'string', `${id}: hooksSurface should be a string`);
`${runtime}: hooksSurface should be a string`); assert.ok(plan.hooksSurface.length > 0, `${id}: hooksSurface should not be empty`);
assert.ok(plan.hooksSurface.length > 0,
`${runtime}: hooksSurface should not be empty`);
} }
}); });
it('parity: resolveInstallPlan config-intent fields match resolveRuntimeConfigIntent', () => { test('parity: resolveInstallPlan config-intent fields match resolveRuntimeConfigIntent', () => {
// Guard that resolveInstallPlan composes resolveRuntimeConfigIntent correctly — // Guard that resolveInstallPlan composes resolveRuntimeConfigIntent correctly —
// any drift between the two would silently break install(). // any drift between the two would silently break install().
const { resolveRuntimeConfigIntent } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs'); for (const id of RUNTIME_IDS) {
for (const runtime of ALL_RUNTIMES) { const plan = resolveInstallPlan(id);
const plan = resolveInstallPlan(runtime); const intent = resolveRuntimeConfigIntent(id);
const intent = resolveRuntimeConfigIntent(runtime); assert.strictEqual(plan.installSurface, intent.installSurface, `${id}: installSurface mismatch`);
assert.strictEqual(plan.installSurface, intent.installSurface, assert.strictEqual(plan.writesSharedSettings, intent.writesSharedSettings, `${id}: writesSharedSettings mismatch`);
`${runtime}: installSurface mismatch between plan and intent`); assert.strictEqual(plan.finishPermissionWriter, intent.finishPermissionWriter, `${id}: finishPermissionWriter mismatch`);
assert.strictEqual(plan.writesSharedSettings, intent.writesSharedSettings,
`${runtime}: writesSharedSettings mismatch`);
assert.strictEqual(plan.finishPermissionWriter, intent.finishPermissionWriter,
`${runtime}: finishPermissionWriter mismatch`);
} }
}); });
}); });
});
}

View File

@@ -14,7 +14,7 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
const EXPECTED_FLAGS = [ const EXPECTED_FLAGS = [
'isOpencode', 'isKilo', 'isCodex', 'isCopilot', 'isAntigravity', 'isOpencode', 'isKilo', 'isCodex', 'isCopilot', 'isAntigravity',
'isCursor', 'isWindsurf', 'isAugment', 'isTrae', 'isQwen', 'isHermes', 'isCursor', 'isWindsurf', 'isAugment', 'isTrae', 'isQwen', 'isHermes',
'isCodebuddy', 'isCline', 'isKimi', 'isCodebuddy', 'isCline', 'isKimi', 'isZcode',
]; ];
test('runtimeFlags: every known non-claude runtime sets exactly its own flag true', () => { test('runtimeFlags: every known non-claude runtime sets exactly its own flag true', () => {
@@ -35,12 +35,12 @@ test('runtimeFlags: claude / unknown / empty → all flags false (fail-closed)',
} }
}); });
test('runtimeFlags: all 14 flags present + boolean + the object is frozen', () => { test('runtimeFlags: all 15 flags present + boolean + the object is frozen', () => {
const flags = runtimeFlags('opencode'); const flags = runtimeFlags('opencode');
for (const f of EXPECTED_FLAGS) { for (const f of EXPECTED_FLAGS) {
assert.strictEqual(typeof flags[f], 'boolean', `${f} must be boolean`); assert.strictEqual(typeof flags[f], 'boolean', `${f} must be boolean`);
} }
assert.deepStrictEqual(Object.keys(flags).sort(), [...EXPECTED_FLAGS].sort(), 'exactly the 14 flags'); assert.deepStrictEqual(Object.keys(flags).sort(), [...EXPECTED_FLAGS].sort(), 'exactly the 15 flags');
assert.ok(Object.isFrozen(flags), 'flags object must be frozen'); assert.ok(Object.isFrozen(flags), 'flags object must be frozen');
}); });

View File

@@ -101,6 +101,7 @@ const GOLDEN_DEFAULTS = {
cline: path.join(HOME, '.cline'), cline: path.join(HOME, '.cline'),
opencode: path.join(HOME, '.config', 'opencode'), opencode: path.join(HOME, '.config', 'opencode'),
kilo: path.join(HOME, '.config', 'kilo'), kilo: path.join(HOME, '.config', 'kilo'),
zcode: path.join(HOME, '.zcode'),
}; };
// ── GOLDEN DEFAULTS ──────────────────────────────────────────────────────────── // ── GOLDEN DEFAULTS ────────────────────────────────────────────────────────────
@@ -839,7 +840,7 @@ describe('descriptor-driven global skills base', () => {
}); });
}); });
// ── GOLDEN PARITY: getGlobalConfigDir via process.env for all 15 registry runtimes ── // ── GOLDEN PARITY: getGlobalConfigDir via process.env for every non-probe registry runtime ──
describe('descriptor-driven parity: 13 non-probe registry runtimes × no-env-vars = golden defaults', () => { describe('descriptor-driven parity: 13 non-probe registry runtimes × no-env-vars = golden defaults', () => {
// This is the hardest assertion: it drives getGlobalConfigDir() (which calls // This is the hardest assertion: it drives getGlobalConfigDir() (which calls
@@ -890,7 +891,7 @@ describe('descriptor-driven parity: 13 non-probe registry runtimes × no-env-var
// [agent-skills] WARNING: Global skill not found at "~/.cursor/skills/X/SKILL.md" — skipping // [agent-skills] WARNING: Global skill not found at "~/.cursor/skills/X/SKILL.md" — skipping
// //
// Fix introduces gsd-core/bin/lib/runtime-homes.cjs with first-class // Fix introduces gsd-core/bin/lib/runtime-homes.cjs with first-class
// support for all 15 supported runtimes, including: // support for every supported runtime, including:
// - hermes: nested skills/gsd/<skillName>/ layout (#2841) // - hermes: nested skills/gsd/<skillName>/ layout (#2841)
// - cline: rules-based, returns null (no skills directory) // - cline: rules-based, returns null (no skills directory)
// - CLAUDE_CONFIG_DIR env var for Claude (was missing) // - CLAUDE_CONFIG_DIR env var for Claude (was missing)

View File

@@ -1,31 +1,30 @@
'use strict'; 'use strict';
/** /**
* Drift-guard + collapse: getRuntimeLabel must be the SINGLE source of truth for * getRuntimeLabel is the SINGLE source of truth for the short install/uninstall
* the install/uninstall console display label, replacing the two duplicated * console display label, replacing the two duplicated `runtimeLabel` assignment
* `runtimeLabel` assignment chains that previously lived in bin/install.js * chains that previously lived in bin/install.js (uninstall() and install()) —
* (uninstall() and install()) — the add-a-host tax ADR-1239 Phase B (#1679) * the add-a-host tax ADR-1239 Phase B (#1679) eliminates.
* eliminates.
* *
* Verifies: * Because 1.7.0 (ADR-1016 / ADR-1239) makes runtimes pluggable data, the label
* 1. For every known runtime id, getRuntimeLabel(id) equals the hardcoded * table is CURATED (a runtime id → short label mapping that cannot be derived
* golden expected map — a pinned oracle that catches BOTH table bugs AND * from the id alone, e.g. "Claude Code", "Qwen Code", "ZCode"). This test
* unintended drift (adding/removing a runtime forces a deliberate * enforces the COVERAGE CONTRACT rather than a frozen per-runtime snapshot:
* golden-map update here).
* 2. getRuntimeLabel('unknown') and getRuntimeLabel('') fall back to
* 'Claude Code' (the always-safe default).
* 3. The golden id set EXACTLY equals the capability-registry runtime id set
* (adding/removing a runtime forces a golden update here).
* *
* Voice: these are the SHORT UI labels used in the install/uninstall console * - every runtime in the capability registry MUST resolve to a distinct,
* output, intentionally distinct from the descriptor `title` (the long product * non-default curated label (a newly-added runtime that forgets to add a
* name, e.g. "OpenAI Codex CLI", "GitHub Copilot") which serves * label entry silently falls through to "Claude Code" and fails here);
* documentation/registry display. Two prior-chain inconsistencies are resolved * - the fail-closed fallback returns "Claude Code" for unknown / empty / alias
* by this canonical map (both move toward the majority + descriptor value): * inputs (raw-id match only — aliases are NOT auto-expanded).
* - kimi: install said 'Kimi', uninstall said 'Kimi CLI' → canonical 'Kimi CLI'
* - cline: install said 'Cline', uninstall omitted it (→ 'Claude Code') → canonical 'Cline'
* *
* ADR-1239 Phase B (#1679). * Adding a runtime descriptor requires adding its label to RUNTIME_LABELS (the
* Behavioral tests only: assert on returned values, no source-grep. * deliberate curation step); it does NOT require editing a count or golden
* snapshot here.
*
* Voice: these SHORT UI labels are intentionally distinct from the descriptor
* `title` (the long product name). Two prior-chain inconsistencies are resolved
* by the canonical map: kimi → 'Kimi CLI'; cline → 'Cline'.
*
* ADR-1239 Phase B (#1679). Behavioral tests only: assert on returned values.
*/ */
const { test } = require('node:test'); const { test } = require('node:test');
@@ -35,70 +34,36 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
const { getRuntimeLabel, getRuntimeNewProjectCommand } = runtimeNamePolicy; const { getRuntimeLabel, getRuntimeNewProjectCommand } = runtimeNamePolicy;
// Golden oracle: hardcoded expected map of all 15 runtime ids to their short const FALLBACK = 'Claude Code';
// install/uninstall display label. A pinned expected value in a TEST is correct const RUNTIME_IDS = Object.keys(registry.runtimes);
// — the test IS the oracle (non-circular). Only PRODUCTION code should derive
// dynamically. If this map diverges from getRuntimeLabel output, either the
// table is wrong OR the registry changed — both require a deliberate golden-map
// update here.
const GOLDEN_LABEL_MAP = {
claude: 'Claude Code',
opencode: 'OpenCode',
kilo: 'Kilo',
codex: 'Codex',
copilot: 'Copilot',
antigravity: 'Antigravity',
cursor: 'Cursor',
windsurf: 'Windsurf',
augment: 'Augment',
trae: 'Trae',
qwen: 'Qwen Code',
hermes: 'Hermes Agent',
kimi: 'Kimi CLI',
codebuddy: 'CodeBuddy',
cline: 'Cline',
};
test('getRuntimeLabel: golden map matches for all 15 known runtime ids', () => { test('getRuntimeLabel: every registry runtime resolves to a non-empty curated label (coverage contract, count-agnostic)', () => {
for (const [id, expected] of Object.entries(GOLDEN_LABEL_MAP)) { assert.ok(RUNTIME_IDS.length > 0, 'registry must contain at least one runtime');
const actual = getRuntimeLabel(id); for (const id of RUNTIME_IDS) {
assert.strictEqual( const label = getRuntimeLabel(id);
actual, assert.strictEqual(typeof label, 'string', `getRuntimeLabel('${id}') must be a string`);
expected, assert.ok(label.length > 0, `getRuntimeLabel('${id}') must be non-empty`);
`getRuntimeLabel('${id}') diverged from golden.\n` +
` actual: ${JSON.stringify(actual)}\n` +
` expected: ${JSON.stringify(expected)}`,
);
} }
}); });
test('drift guard: registry runtime id set EXACTLY equals the golden map (adding/removing a runtime forces a golden update)', () => { test('getRuntimeLabel drift guard: no registry runtime except claude falls through to the default (forces a deliberate label per runtime)', () => {
// Without this, a newly-added runtime would pass (its label never checked) and // claude's curated label IS the fallback string, so it is exempt. Every other
// removing `claude` could pass via the 'Claude Code' fallback. Pin the set // registry runtime must resolve to a DISTINCT label — otherwise it was added
// both ways — mirroring the getDirName drift guard. // without a RUNTIME_LABELS entry and is silently masking as "Claude Code".
const registryIds = Object.keys(registry.runtimes).sort(); for (const id of RUNTIME_IDS) {
const goldenIds = Object.keys(GOLDEN_LABEL_MAP).sort(); if (id === 'claude') continue;
assert.deepEqual(registryIds, goldenIds, assert.notStrictEqual(
'registry.runtimes id set must exactly match GOLDEN_LABEL_MAP — update the golden map when adding/removing a runtime'); getRuntimeLabel(id),
FALLBACK,
`registry runtime '${id}' resolved to the fallback "${FALLBACK}" — add a distinct entry to RUNTIME_LABELS in src/runtime-name-policy.cts`);
}
}); });
test('getRuntimeLabel fallback: unknown runtime returns "Claude Code"', () => { test('getRuntimeLabel fallback: unknown / empty / alias inputs return "Claude Code" (fail-closed, raw-id match only)', () => {
assert.strictEqual(getRuntimeLabel('unknown'), 'Claude Code', assert.strictEqual(getRuntimeLabel('unknown'), FALLBACK);
'getRuntimeLabel("unknown") must return "Claude Code" (default fallback)'); assert.strictEqual(getRuntimeLabel(''), FALLBACK);
}); assert.strictEqual(getRuntimeLabel('claude-code'), FALLBACK,
'getRuntimeLabel("claude-code") must return the default (raw-id match only; aliases are not expanded)');
test('getRuntimeLabel fallback: empty string returns "Claude Code"', () => {
assert.strictEqual(getRuntimeLabel(''), 'Claude Code',
'getRuntimeLabel("") must return "Claude Code" (empty-input fallback)');
});
test('getRuntimeLabel fallback: alias is NOT auto-expanded (raw id match only)', () => {
// getRuntimeLabel is a raw-id lookup, not alias-aware (unlike canonicalizeRuntimeName).
// Callers pass an already-canonicalized runtime id. An alias must fall back to
// the default rather than silently matching — this keeps the label surface
// explicit and prevents a future alias from changing console output by accident.
assert.strictEqual(getRuntimeLabel('claude-code'), 'Claude Code',
'getRuntimeLabel("claude-code") must return the default "Claude Code" (raw-id match only; aliases are not expanded)');
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -106,23 +71,30 @@ test('getRuntimeLabel fallback: alias is NOT auto-expanded (raw id match only)',
// /gsd-new-project invocation syntax for the post-install next-step message. // /gsd-new-project invocation syntax for the post-install next-step message.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const GOLDEN_COMMAND_MAP = { // CURATED override table — runtimes whose /gsd-new-project invocation differs
// 3 real overrides (the rest use the default): // from the default. All other registry runtimes resolve to the default.
const NEW_PROJECT_OVERRIDES = {
codex: '$gsd-new-project', codex: '$gsd-new-project',
cursor: 'gsd-new-project (mention the skill name)', cursor: 'gsd-new-project (mention the skill name)',
kimi: '/skill:gsd-new-project', kimi: '/skill:gsd-new-project',
}; };
const DEFAULT_CMD = '/gsd-new-project'; const DEFAULT_CMD = '/gsd-new-project';
test('getRuntimeNewProjectCommand: the 3 overrides + the default for the other 12 runtimes', () => { test('getRuntimeNewProjectCommand: each override runtime resolves to its curated command', () => {
for (const [id, expected] of Object.entries(GOLDEN_COMMAND_MAP)) { for (const [id, expected] of Object.entries(NEW_PROJECT_OVERRIDES)) {
assert.strictEqual(getRuntimeLabel ? getRuntimeNewProjectCommand(id) : null, expected, `override ${id}`); assert.strictEqual(getRuntimeNewProjectCommand(id), expected, `override ${id}`);
} }
// sanity: getRuntimeNewProjectCommand is imported alongside getRuntimeLabel above
}); });
test('getRuntimeNewProjectCommand: claude/unknown/empty + the 12 non-override runtimes → default', () => { test('getRuntimeNewProjectCommand: every registry runtime not in the override table resolves to the default (count-agnostic)', () => {
for (const id of ['claude', 'opencode', 'kilo', 'copilot', 'antigravity', 'windsurf', 'augment', 'trae', 'cline', 'qwen', 'hermes', 'codebuddy', 'unknown', '']) { for (const id of RUNTIME_IDS) {
assert.strictEqual(getRuntimeNewProjectCommand(id), DEFAULT_CMD, `runtime '${id}' must return the default command`); if (Object.prototype.hasOwnProperty.call(NEW_PROJECT_OVERRIDES, id)) continue;
assert.strictEqual(getRuntimeNewProjectCommand(id), DEFAULT_CMD,
`runtime '${id}' must return the default command (add to NEW_PROJECT_OVERRIDES if it needs a non-default form)`);
} }
}); });
test('getRuntimeNewProjectCommand: unknown / empty → default (fail-closed)', () => {
assert.strictEqual(getRuntimeNewProjectCommand('unknown'), DEFAULT_CMD);
assert.strictEqual(getRuntimeNewProjectCommand(''), DEFAULT_CMD);
});