* refactor(#60): make runtime config adapter registry explicit Replace scattered inline `runtime === '...'` config-mutation branching in bin/install.js with an explicit, typed adapter registry. The new src/runtime-config-adapter-registry.cts maps each of the 15 supported runtimes to a config intent { installSurface, writesSharedSettings, finishPermissionWriter }; install()/finishInstall() dispatch by resolved intent instead of runtime-name checks (cursor/windsurf/trae collapse to one profile-marker-only branch). Behavior-preserving: the same config files are written for the same runtimes (opencode still writes both settings.json and its permissions; kilo writes only its permissions; codex minimal-mode and opencode GSD_TEST_MODE guards unchanged). Unknown runtimes fail loudly via TypeError, with an Object.hasOwn barrier so prototype-chain keys (__proto__/constructor) also throw rather than returning a bogus intent. Leads the installer-refactor chain (#58 -> #60 -> #56), building on ADR-58. Closes #60 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#60): add changeset for runtime config adapter registry Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#60): register Runtime Config Adapter Registry in CONTEXT.md glossary Per docs/contributor-standards.md, every new Module/seam must get a `### <Name>` entry under the domain glossary. Adds the entry for the runtime-config-adapter-registry seam introduced in this PR (interface, policy boundary, source file, ADR-58 / #60 cross-references). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
245
tests/runtime-config-adapter-registry.test.cjs
Normal file
245
tests/runtime-config-adapter-registry.test.cjs
Normal file
@@ -0,0 +1,245 @@
|
||||
'use strict';
|
||||
|
||||
// Tests for runtime-config-adapter-registry.cjs (issue #60).
|
||||
// TDD: this file is written BEFORE the implementation to establish the red state.
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const {
|
||||
resolveRuntimeConfigIntent,
|
||||
ALLOWED_CONFIG_RUNTIMES,
|
||||
INSTALL_SURFACES,
|
||||
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-config-adapter-registry.cjs'));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Source-of-truth table (mirrors the intent table in the brief exactly)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const EXPECTED_TABLE = [
|
||||
{ runtime: 'claude', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null },
|
||||
{ runtime: 'gemini', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null },
|
||||
{ runtime: 'antigravity', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null },
|
||||
{ runtime: 'augment', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null },
|
||||
{ runtime: 'qwen', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null },
|
||||
{ runtime: 'hermes', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null },
|
||||
{ runtime: 'codebuddy', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null },
|
||||
{ runtime: 'opencode', installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: 'opencode' },
|
||||
{ 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: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null },
|
||||
{ runtime: 'windsurf', installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null },
|
||||
{ runtime: 'trae', installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 1: Table-lock — every row in EXPECTED_TABLE must match exactly
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('resolveRuntimeConfigIntent — table-lock', () => {
|
||||
for (const row of EXPECTED_TABLE) {
|
||||
test(`${row.runtime} resolves to expected intent`, () => {
|
||||
const intent = resolveRuntimeConfigIntent(row.runtime);
|
||||
assert.deepStrictEqual(intent, {
|
||||
runtime: row.runtime,
|
||||
installSurface: row.installSurface,
|
||||
writesSharedSettings: row.writesSharedSettings,
|
||||
finishPermissionWriter: row.finishPermissionWriter,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 2: Unknown runtime fails loudly (AC#2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('resolveRuntimeConfigIntent — unknown runtime throws TypeError', () => {
|
||||
test('throws TypeError for unknown string "grok"', () => {
|
||||
assert.throws(() => resolveRuntimeConfigIntent('grok'), TypeError);
|
||||
});
|
||||
|
||||
test('throws TypeError for unknown string "xyzunknown"', () => {
|
||||
assert.throws(() => resolveRuntimeConfigIntent('xyzunknown'), TypeError);
|
||||
});
|
||||
|
||||
test('throws TypeError for empty string ""', () => {
|
||||
assert.throws(() => resolveRuntimeConfigIntent(''), TypeError);
|
||||
});
|
||||
|
||||
test('throws TypeError for undefined', () => {
|
||||
assert.throws(() => resolveRuntimeConfigIntent(undefined), TypeError);
|
||||
});
|
||||
|
||||
test('throws TypeError for "__proto__" (prototype-chain key)', () => {
|
||||
assert.throws(() => resolveRuntimeConfigIntent('__proto__'), TypeError);
|
||||
});
|
||||
|
||||
test('throws TypeError for "constructor" (prototype-chain key)', () => {
|
||||
assert.throws(() => resolveRuntimeConfigIntent('constructor'), TypeError);
|
||||
});
|
||||
|
||||
test('throws TypeError for "hasOwnProperty" (prototype-chain key)', () => {
|
||||
assert.throws(() => resolveRuntimeConfigIntent('hasOwnProperty'), TypeError);
|
||||
});
|
||||
|
||||
test('throws TypeError for "toString" (prototype-chain key)', () => {
|
||||
assert.throws(() => resolveRuntimeConfigIntent('toString'), TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 3: writesSharedSettings exclusion equivalence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('writesSharedSettings exclusion equivalence', () => {
|
||||
const EXPECTED_FALSE_SET = new Set(['codex', 'copilot', 'kilo', 'cursor', 'windsurf', 'trae', 'cline']);
|
||||
|
||||
test('runtimes with writesSharedSettings===false are exactly the exclusion set', () => {
|
||||
const falseRuntimes = EXPECTED_TABLE
|
||||
.filter(r => r.writesSharedSettings === false)
|
||||
.map(r => r.runtime);
|
||||
assert.deepStrictEqual(new Set(falseRuntimes), EXPECTED_FALSE_SET);
|
||||
});
|
||||
|
||||
test('all other supported runtimes have writesSharedSettings===true', () => {
|
||||
const trueRuntimes = EXPECTED_TABLE
|
||||
.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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('finishPermissionWriter', () => {
|
||||
test('opencode -> "opencode"', () => {
|
||||
assert.strictEqual(resolveRuntimeConfigIntent('opencode').finishPermissionWriter, 'opencode');
|
||||
});
|
||||
|
||||
test('kilo -> "kilo"', () => {
|
||||
assert.strictEqual(resolveRuntimeConfigIntent('kilo').finishPermissionWriter, 'kilo');
|
||||
});
|
||||
|
||||
test('every other supported runtime -> null', () => {
|
||||
const nullExpected = EXPECTED_TABLE
|
||||
.filter(r => r.finishPermissionWriter === null)
|
||||
.map(r => r.runtime);
|
||||
for (const runtime of nullExpected) {
|
||||
assert.strictEqual(
|
||||
resolveRuntimeConfigIntent(runtime).finishPermissionWriter,
|
||||
null,
|
||||
`${runtime} should have finishPermissionWriter null`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 5: Distinct dedicated surfaces
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('installSurface correctness', () => {
|
||||
test('codex -> "codex-toml"', () => {
|
||||
assert.strictEqual(resolveRuntimeConfigIntent('codex').installSurface, 'codex-toml');
|
||||
});
|
||||
|
||||
test('copilot -> "copilot-instructions"', () => {
|
||||
assert.strictEqual(resolveRuntimeConfigIntent('copilot').installSurface, 'copilot-instructions');
|
||||
});
|
||||
|
||||
test('cline -> "cline-rules"', () => {
|
||||
assert.strictEqual(resolveRuntimeConfigIntent('cline').installSurface, 'cline-rules');
|
||||
});
|
||||
|
||||
test('cursor -> "profile-marker-only"', () => {
|
||||
assert.strictEqual(resolveRuntimeConfigIntent('cursor').installSurface, 'profile-marker-only');
|
||||
});
|
||||
|
||||
test('windsurf -> "profile-marker-only"', () => {
|
||||
assert.strictEqual(resolveRuntimeConfigIntent('windsurf').installSurface, 'profile-marker-only');
|
||||
});
|
||||
|
||||
test('trae -> "profile-marker-only"', () => {
|
||||
assert.strictEqual(resolveRuntimeConfigIntent('trae').installSurface, 'profile-marker-only');
|
||||
});
|
||||
|
||||
test('the 7 passthroughs + opencode + kilo -> "settings-json"', () => {
|
||||
const settingsJsonRuntimes = ['claude', 'gemini', 'antigravity', 'augment', 'qwen', 'hermes', 'codebuddy', 'opencode', 'kilo'];
|
||||
for (const runtime of settingsJsonRuntimes) {
|
||||
assert.strictEqual(
|
||||
resolveRuntimeConfigIntent(runtime).installSurface,
|
||||
'settings-json',
|
||||
`${runtime} should have installSurface "settings-json"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 6: Returned intent is a fresh object (no shared reference mutation)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('resolveRuntimeConfigIntent — fresh object each call', () => {
|
||||
test('mutating the returned object does not affect a subsequent resolve', () => {
|
||||
const first = resolveRuntimeConfigIntent('claude');
|
||||
first.installSurface = 'MUTATED';
|
||||
first.writesSharedSettings = false;
|
||||
|
||||
const second = resolveRuntimeConfigIntent('claude');
|
||||
assert.strictEqual(second.installSurface, 'settings-json');
|
||||
assert.strictEqual(second.writesSharedSettings, true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 7: Completeness (AC#4 table-driven) — ALLOWED_CONFIG_RUNTIMES
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('ALLOWED_CONFIG_RUNTIMES completeness', () => {
|
||||
const EXPECTED_15 = new Set([
|
||||
'claude', 'gemini', 'antigravity', 'augment', 'qwen', 'hermes', 'codebuddy',
|
||||
'opencode', 'kilo', 'codex', 'copilot', 'cline', 'cursor', 'windsurf', 'trae',
|
||||
]);
|
||||
|
||||
test('ALLOWED_CONFIG_RUNTIMES contains exactly the 15 expected runtimes', () => {
|
||||
const runtimeSet = new Set(ALLOWED_CONFIG_RUNTIMES);
|
||||
assert.deepStrictEqual(runtimeSet, EXPECTED_15);
|
||||
});
|
||||
|
||||
test('every member of ALLOWED_CONFIG_RUNTIMES resolves without throwing', () => {
|
||||
for (const runtime of ALLOWED_CONFIG_RUNTIMES) {
|
||||
assert.doesNotThrow(() => resolveRuntimeConfigIntent(runtime), `${runtime} should resolve without throwing`);
|
||||
}
|
||||
});
|
||||
|
||||
test('ALLOWED_CONFIG_RUNTIMES has exactly 15 entries', () => {
|
||||
assert.strictEqual([...ALLOWED_CONFIG_RUNTIMES].length, 15);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 8: INSTALL_SURFACES export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('INSTALL_SURFACES export', () => {
|
||||
const EXPECTED_SURFACES = new Set([
|
||||
'settings-json',
|
||||
'codex-toml',
|
||||
'copilot-instructions',
|
||||
'cline-rules',
|
||||
'profile-marker-only',
|
||||
]);
|
||||
|
||||
test('INSTALL_SURFACES contains exactly the 5 surface strings', () => {
|
||||
assert.deepStrictEqual(new Set(INSTALL_SURFACES), EXPECTED_SURFACES);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user