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:
@@ -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