ADR-1239 Phase B (parent #1679). getDirName was a hand-maintained 15-branch if-chain mapping each runtime to its local content-rewrite dot-dir. Relocate those values into a documented runtime.localConfigDir descriptor field; derive getDirName from registry.runtimes[id].runtime.localConfigDir (fallback .claude). - 16 capability.json gain runtime.localConfigDir (byte-identical values) - capability-validator.cjs requires it (non-empty dot-dir); registry regenerated - docs/reference/capability-manifest.md documents the field + the three divergent values (copilot=.github, antigravity=.agents, kimi=.kimi-code) - drift-guard test: golden value map + key-set equality both ways Byte-identical install output for all 16 runtimes (golden-parity harness #1730). Closes #1756 Co-authored-by: review-bot <review-bot@gsd> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/daring-otters-dart.md
Normal file
5
.changeset/daring-otters-dart.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 1757
|
||||
---
|
||||
**Internal: getDirName is now derived from a documented `runtime.localConfigDir` descriptor field** — each runtime's local content-rewrite directory (e.g. `cursor`→`.cursor`, `copilot`→`.github`) moved from a hand-maintained if-chain into its capability descriptor (ADR-1239 Phase B), so it can no longer drift from the registry. Install output is byte-identical for all 16 runtimes (golden-parity asserted); no user-facing change.
|
||||
@@ -24,6 +24,7 @@
|
||||
],
|
||||
"probeExists": "gsd-core/VERSION"
|
||||
},
|
||||
"localConfigDir": ".agents",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"AUGMENT_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".augment",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"CLAUDE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".claude",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"CLINE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".cline",
|
||||
"configFormat": "markdown-dir",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"CODEBUDDY_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".codebuddy",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"CODEX_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".codex",
|
||||
"configFormat": "toml",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"COPILOT_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".github",
|
||||
"configFormat": "markdown",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"CURSOR_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".cursor",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"GEMINI_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".gemini",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"HERMES_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".hermes",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"env": []
|
||||
}
|
||||
},
|
||||
"localConfigDir": ".kilo",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
],
|
||||
"probeExists": "skills"
|
||||
},
|
||||
"localConfigDir": ".kimi-code",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"XDG_CONFIG_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".opencode",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"QWEN_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".qwen",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"TRAE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".trae",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"WINDSURF_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".windsurf",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [],
|
||||
|
||||
@@ -143,6 +143,7 @@ Runtime capabilities describe how GSD projects its artefacts onto one host CLI.
|
||||
| Axis | Field | Type summary |
|
||||
|---|---|---|
|
||||
| Config home | `runtime.configHome` | Structured object with `kind` (`dot-home` \| `dot-home-nested` \| `xdg` \| `generic-agents-root`), `name`, optional `parent`, `env[]`, `probe[]`, `probeExists`, `skillsHome`. `probeExists` is an optional sub-path applied to probe candidates: for `generic-agents-root` it is a hard filter (a candidate qualifies only if `<candidate>/<probeExists>` exists); for `dot-home-nested` it is a preference that makes probing pick the candidate GSD owns (e.g. `gsd-core/VERSION`) over a bare-existing sibling before falling back — see ADR-1016 and #213/#217. |
|
||||
| Local config dir | `runtime.localConfigDir` | Required dot-prefixed string. The runtime's **local** content-rewrite directory — the `./` target GSD stamps into rewritten artefact bodies (e.g. `./.claude/` → `./<localConfigDir>/`) and the local install dir basename. Backs `getDirName()` (registry-derived, #1679). Usually `.<runtime>` (the runtime's home dot-dir), but **three runtimes diverge** because they read GSD's content from a non-home directory: `copilot` → `.github` (GitHub Copilot reads custom instructions from `.github/copilot-instructions.md` / `.github/instructions/`; see `convertClaudeToCopilotContent` rewrites in `src/runtime-artifact-conversion.cts`), `antigravity` → `.agents` (local agent/workflow dir; see the antigravity rewrites in `src/runtime-artifact-conversion.cts`), `kimi` → `.kimi-code`. Distinct from `configHome.name` (the **global** install home, which for these three is `.copilot` / `antigravity` / `agents`). Byte-parity-proven against the prior hand-maintained mapping by the golden-install-parity harness. |
|
||||
| Config format | `runtime.configFormat` | Closed enum: `settings-json` \| `toml` \| `markdown` \| `markdown-dir` \| `none`. |
|
||||
| Artefact layout | `runtime.artifactLayout` | Object with `global` and `local` arrays of `ArtifactKind` (`kind`, `destSubpath`, `prefix`, `nesting`, `recursive`, `stage`). |
|
||||
| Command style | `runtime.commandStyle` | Closed enum: `slash-hyphen` \| `shell-var`. |
|
||||
|
||||
@@ -86,6 +86,7 @@ const capabilities = {
|
||||
],
|
||||
"probeExists": "gsd-core/VERSION"
|
||||
},
|
||||
"localConfigDir": ".agents",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -193,6 +194,7 @@ const capabilities = {
|
||||
"AUGMENT_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".augment",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -279,6 +281,7 @@ const capabilities = {
|
||||
"CLAUDE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".claude",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -362,6 +365,7 @@ const capabilities = {
|
||||
"CLINE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".cline",
|
||||
"configFormat": "markdown-dir",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -483,6 +487,7 @@ const capabilities = {
|
||||
"CODEBUDDY_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".codebuddy",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -569,6 +574,7 @@ const capabilities = {
|
||||
"CODEX_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".codex",
|
||||
"configFormat": "toml",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -640,6 +646,7 @@ const capabilities = {
|
||||
"COPILOT_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".github",
|
||||
"configFormat": "markdown",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -709,6 +716,7 @@ const capabilities = {
|
||||
"CURSOR_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".cursor",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -914,6 +922,7 @@ const capabilities = {
|
||||
"GEMINI_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".gemini",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -1029,6 +1038,7 @@ const capabilities = {
|
||||
"HERMES_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".hermes",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -1158,6 +1168,7 @@ const capabilities = {
|
||||
"env": []
|
||||
}
|
||||
},
|
||||
"localConfigDir": ".kilo",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -1248,6 +1259,7 @@ const capabilities = {
|
||||
],
|
||||
"probeExists": "skills"
|
||||
},
|
||||
"localConfigDir": ".kimi-code",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -1542,6 +1554,7 @@ const capabilities = {
|
||||
"XDG_CONFIG_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".opencode",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -1758,6 +1771,7 @@ const capabilities = {
|
||||
"QWEN_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".qwen",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -2082,6 +2096,7 @@ const capabilities = {
|
||||
"TRAE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".trae",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -2247,6 +2262,7 @@ const capabilities = {
|
||||
"WINDSURF_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".windsurf",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [],
|
||||
@@ -3038,6 +3054,7 @@ const runtimes = {
|
||||
],
|
||||
"probeExists": "gsd-core/VERSION"
|
||||
},
|
||||
"localConfigDir": ".agents",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3108,6 +3125,7 @@ const runtimes = {
|
||||
"AUGMENT_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".augment",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3194,6 +3212,7 @@ const runtimes = {
|
||||
"CLAUDE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".claude",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3277,6 +3296,7 @@ const runtimes = {
|
||||
"CLINE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".cline",
|
||||
"configFormat": "markdown-dir",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3337,6 +3357,7 @@ const runtimes = {
|
||||
"CODEBUDDY_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".codebuddy",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3423,6 +3444,7 @@ const runtimes = {
|
||||
"CODEX_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".codex",
|
||||
"configFormat": "toml",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3494,6 +3516,7 @@ const runtimes = {
|
||||
"COPILOT_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".github",
|
||||
"configFormat": "markdown",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3563,6 +3586,7 @@ const runtimes = {
|
||||
"CURSOR_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".cursor",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3649,6 +3673,7 @@ const runtimes = {
|
||||
"GEMINI_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".gemini",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3723,6 +3748,7 @@ const runtimes = {
|
||||
"HERMES_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".hermes",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3800,6 +3826,7 @@ const runtimes = {
|
||||
"env": []
|
||||
}
|
||||
},
|
||||
"localConfigDir": ".kilo",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3890,6 +3917,7 @@ const runtimes = {
|
||||
],
|
||||
"probeExists": "skills"
|
||||
},
|
||||
"localConfigDir": ".kimi-code",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -3960,6 +3988,7 @@ const runtimes = {
|
||||
"XDG_CONFIG_HOME"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".opencode",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -4045,6 +4074,7 @@ const runtimes = {
|
||||
"QWEN_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".qwen",
|
||||
"configFormat": "settings-json",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -4119,6 +4149,7 @@ const runtimes = {
|
||||
"TRAE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".trae",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
@@ -4189,6 +4220,7 @@ const runtimes = {
|
||||
"WINDSURF_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".windsurf",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [],
|
||||
|
||||
@@ -1030,6 +1030,20 @@ function validateRuntimeBody(cap) {
|
||||
);
|
||||
}
|
||||
|
||||
// localConfigDir — REQUIRED non-empty dot-dir string (ADR-1239 Phase B #1679)
|
||||
// Must start with '.' (e.g. ".claude", ".cursor"). Validated here so the registry
|
||||
// generator catches any descriptor missing the field before regenerating.
|
||||
if (typeof r.localConfigDir !== 'string' || r.localConfigDir.length === 0) {
|
||||
errors.push(
|
||||
'runtime.localConfigDir is required and must be a non-empty string (e.g. ".claude"); ' +
|
||||
'got: ' + JSON.stringify(r.localConfigDir),
|
||||
);
|
||||
} else if (!r.localConfigDir.startsWith('.')) {
|
||||
errors.push(
|
||||
'runtime.localConfigDir must start with "." (a dot-dir); got: ' + JSON.stringify(r.localConfigDir),
|
||||
);
|
||||
}
|
||||
|
||||
// extendedHookEvents — required array; every element must be in closed enum
|
||||
if (!Array.isArray(r.extendedHookEvents)) {
|
||||
errors.push(
|
||||
|
||||
@@ -142,20 +142,12 @@ export function getProjectInstructionFile(runtime: unknown): string {
|
||||
* `bin/install.js` re-exports this same function for back-compat.
|
||||
*/
|
||||
export function getDirName(runtime: string): string {
|
||||
if (runtime === 'copilot') return '.github';
|
||||
if (runtime === 'opencode') return '.opencode';
|
||||
if (runtime === 'gemini') return '.gemini';
|
||||
if (runtime === 'kilo') return '.kilo';
|
||||
if (runtime === 'codex') return '.codex';
|
||||
if (runtime === 'antigravity') return '.agents';
|
||||
if (runtime === 'cursor') return '.cursor';
|
||||
if (runtime === 'windsurf') return '.windsurf';
|
||||
if (runtime === 'augment') return '.augment';
|
||||
if (runtime === 'trae') return '.trae';
|
||||
if (runtime === 'qwen') return '.qwen';
|
||||
if (runtime === 'hermes') return '.hermes';
|
||||
if (runtime === 'kimi') return '.kimi-code';
|
||||
if (runtime === 'codebuddy') return '.codebuddy';
|
||||
if (runtime === 'cline') return '.cline';
|
||||
if (!runtime) return '.claude';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { runtimes } = require('./capability-registry.cjs') as {
|
||||
runtimes: Record<string, { runtime?: { localConfigDir?: string } } | undefined>;
|
||||
};
|
||||
const dir = runtimes[runtime]?.runtime?.localConfigDir;
|
||||
if (typeof dir === 'string' && dir.length > 0) return dir;
|
||||
return '.claude';
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ function runtimeCap(overrides) {
|
||||
engines: { gsd: '>=1.6.0' },
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.demo', env: [] },
|
||||
localConfigDir: '.demo',
|
||||
configFormat: 'settings-json',
|
||||
artifactLayout: { global: [], local: [] },
|
||||
commandStyle: 'slash-hyphen',
|
||||
|
||||
@@ -1764,6 +1764,7 @@ describe('C3: role:runtime body validation', () => {
|
||||
tier: 'standard', requires: [],
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.cursor', env: ['CURSOR_CONFIG_DIR'] },
|
||||
localConfigDir: '.cursor',
|
||||
configFormat: 'settings-json',
|
||||
artifactLayout: { global: [], local: [] },
|
||||
commandStyle: 'slash-hyphen',
|
||||
@@ -3217,6 +3218,7 @@ function makeRuntimeCap(overrides) {
|
||||
requires: [],
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.test-rt', env: ['TEST_RT_DIR'] },
|
||||
localConfigDir: '.test-rt',
|
||||
configFormat: 'settings-json',
|
||||
artifactLayout: { global: [], local: [] },
|
||||
commandStyle: 'slash-hyphen',
|
||||
@@ -4292,6 +4294,7 @@ describe('ADR-857 phase 5f: cross-field consistency gate rejection tests (DEFECT
|
||||
requires: [],
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.test-runtime', env: [] },
|
||||
localConfigDir: '.test-runtime',
|
||||
configFormat: 'settings-json',
|
||||
artifactLayout: { global: [], local: [] },
|
||||
commandStyle: 'slash-hyphen',
|
||||
|
||||
93
tests/getdirname-registry-derivation.test.cjs
Normal file
93
tests/getdirname-registry-derivation.test.cjs
Normal file
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Drift-guard: getDirName must be derived from the capability registry.
|
||||
* Verifies:
|
||||
* 1. For every known runtime id, getDirName(id) equals the hardcoded golden
|
||||
* expected map — a pinned oracle that catches BOTH formula bugs AND
|
||||
* unintended registry drift (adding/removing a runtime or changing its
|
||||
* localConfigDir forces a deliberate golden-map update here).
|
||||
* 2. getDirName('unknown') and getDirName('') fall back to '.claude'.
|
||||
* 3. Every registry runtime entry has a non-empty dot-dir localConfigDir string —
|
||||
* cross-check from a different angle than the production derivation formula.
|
||||
*
|
||||
* ADR-1239 Phase B (#1679).
|
||||
* Behavioral tests only: assert on returned values, no source-grep.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const runtimeNamePolicy = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
||||
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
|
||||
const { getDirName } = runtimeNamePolicy;
|
||||
|
||||
// Golden oracle: hardcoded expected map of all 16 runtime ids to their local config dir.
|
||||
// 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',
|
||||
gemini: '.gemini',
|
||||
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 16 known runtime ids', () => {
|
||||
for (const [id, expected] of Object.entries(GOLDEN_DIR_MAP)) {
|
||||
const actual = getDirName(id);
|
||||
assert.strictEqual(
|
||||
actual,
|
||||
expected,
|
||||
`getDirName('${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)', () => {
|
||||
// Without this, a newly-added runtime would pass (its value never checked) and
|
||||
// removing `claude` could pass via the .claude fallback. Pin the set both ways.
|
||||
const registryIds = Object.keys(registry.runtimes).sort();
|
||||
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', () => {
|
||||
for (const [id, entry] of Object.entries(registry.runtimes)) {
|
||||
if (!entry || typeof entry !== 'object') continue;
|
||||
const runtimeBlock = entry.runtime;
|
||||
if (!runtimeBlock || typeof runtimeBlock !== 'object') continue;
|
||||
const dir = runtimeBlock.localConfigDir;
|
||||
assert.strictEqual(typeof dir, 'string',
|
||||
`registry.runtimes['${id}'].runtime.localConfigDir must be a string (got: ${typeof dir})`);
|
||||
assert.ok(dir.length > 0,
|
||||
`registry.runtimes['${id}'].runtime.localConfigDir must be non-empty`);
|
||||
assert.ok(dir.startsWith('.'),
|
||||
`registry.runtimes['${id}'].runtime.localConfigDir must start with '.' (got: ${JSON.stringify(dir)})`);
|
||||
}
|
||||
});
|
||||
@@ -51,6 +51,7 @@ function makeMinimalRuntimeCap(overrides = {}) {
|
||||
sandboxTier: 'none',
|
||||
supportTier: 1,
|
||||
installSurface: 'profile-marker-only',
|
||||
localConfigDir: '.test-runtime',
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
@@ -331,6 +332,7 @@ describe('Fix 3: reserved-key guard on hostIntegration and hostIntegration.dispa
|
||||
sandboxTier: 'none',
|
||||
supportTier: 1,
|
||||
installSurface: 'settings-json',
|
||||
localConfigDir: '.test-runtime',
|
||||
writesSharedSettings: true,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: ['SubagentStop', 'Stop', 'PreCompact', 'FileChanged'],
|
||||
|
||||
Reference in New Issue
Block a user