chore(#57): add runtime install no-drift guard tests (#867)

Add tests/issue-57-runtime-install-no-drift.test.cjs protecting the Runtime
Install Policy Module boundary (ADR-58) and the explicit Runtime Config Adapter
Registry (#60), now that #58/#60/#56 have landed and the seam exists.

The guards fail when:
- (AC1) supported-runtime metadata is added to an installer/query call site
  (allRuntimes, the interactive runtimeMap menu) without a matching registry
  adapter entry — enforced by three-way set equality across allRuntimes,
  runtimeMap values, and ALLOWED_CONFIG_RUNTIMES.
- (AC2) config-mutation dispatch escapes the registry: every intent uses a
  registry-declared install surface, every permission writer is null or a
  registry-known runtime, unknown/prototype-key runtimes fail loudly, and a new
  inline 'runtime === "..."' branch against an unregistered runtime is rejected.

Assertions are behavioral (require + reflect on live exports) where behavior can
cover the contract (AC3); two annotated structural guards cover what it cannot.
Existing installer/runtime-policy/runtime-global-skills suites stay green (AC4).

Gates: eslint, lint-test-file-count, full Mac suite (12715 pass / 0 fail) and
Linux Docker (14709 pass / 0 fail) all green; Codex adversarial-review,
/code-review, and /security-review run with findings addressed.

Closes #57

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-08 11:46:19 -04:00
committed by GitHub
parent 9e2e71de7b
commit 35174ce9b0
2 changed files with 207 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
---
type: Changed
pr: 867
---
Added no-drift guard tests (`tests/issue-57-runtime-install-no-drift.test.cjs`) that protect the Runtime Install Policy Module boundary (ADR-58) and the explicit Runtime Config Adapter Registry (#60). They fail loudly when supported-runtime metadata is added to an installer call site (`allRuntimes`, the interactive `runtimeMap` menu) without a matching registry adapter entry, or when config-mutation dispatch escapes the registry's declared install surfaces — catching reintroduction of the scattered per-runtime branching those seams removed.
<!-- docs-exempt: test-only — adds architecture-guard tests, no user-facing behavior, command, or config change -->

View File

@@ -0,0 +1,201 @@
'use strict';
// Issue #57 — Runtime Install No-Drift Tests.
//
// Protects the Runtime Install Policy Module boundary (ADR-58) and the explicit
// Runtime Config Adapter Registry (#60) now that the policy boundary (#58),
// explicit adapter registry (#60), and legacy directory-helper retirement (#56)
// have landed. These guards FAIL when:
//
// (AC1) supported-runtime metadata is added to an installer/query call site
// without going through the runtime registry projection, or
// (AC2) config-mutation dispatch bypasses the explicit adapter registry.
//
// (AC3) Assertions are behavioral (require + reflect on live exports) wherever
// behavior can cover the contract; the two source-text assertions are structural
// guards that behavioral checks cannot replace, and are annotated per repo
// convention. (AC4) The existing installer / runtime-policy / runtime-global-skills
// suites must stay green — verified by running them alongside this file, not
// asserted here.
//
// Known INTENTIONAL asymmetries — these are not drift; do not "fix" them by
// tightening the invariants:
// - `grok` appears in runtime-homes.cjs's getGlobalConfigDir switch but NOT in
// the registry / artifact-layout supported sets (it resolves a config-dir home
// but is not an installable artifact target). So runtime-homes' full switch set
// is never tied into the equality invariant — it is only probed forward, per
// installable runtime.
// - getGlobalConfigDir() falls back to ~/.claude for an UNKNOWN runtime instead
// of throwing (a deliberately liberal projection). Only the registry and
// artifact-layout projections are loud gates, so only those are asserted to
// throw on an unknown runtime.
//
// Coverage boundary (deliberate, see #57 follow-up): the structural guard below
// catches a NEW inline `runtime === '...'` branch against an UNREGISTERED runtime.
// It cannot catch a duplicate inline config write added for an ALREADY-registered
// runtime — distinguishing that from the ~169 legitimate per-runtime comparisons in
// the installer requires driving install()/finishInstall() against a mocked
// filesystem and asserting the written surfaces match resolveRuntimeConfigIntent().
// That behavioral install-driver harness is out of scope for this no-drift pass.
//
// The forward invariant `allRuntimes ⊆ artifact-layout` is already covered by
// tests/install-runtime-artifacts.test.cjs; this file does not duplicate it.
process.env.GSD_TEST_MODE = '1'; // must precede require of bin/install.js
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const ROOT = path.join(__dirname, '..');
const LIB = path.join(ROOT, 'gsd-core', 'bin', 'lib');
const { allRuntimes, runtimeMap } = require(path.join(ROOT, 'bin', 'install.js'));
const {
resolveRuntimeConfigIntent,
ALLOWED_CONFIG_RUNTIMES,
INSTALL_SURFACES,
} = require(path.join(LIB, 'runtime-config-adapter-registry.cjs'));
const { resolveRuntimeArtifactLayout } = require(
path.join(LIB, 'runtime-artifact-layout.cjs'),
);
const { getGlobalConfigDir } = require(path.join(LIB, 'runtime-homes.cjs'));
const sorted = (iterable) => [...iterable].sort();
// A runtime name that is deliberately not real and is not a prototype-chain key.
const SENTINEL = '__drift_sentinel_runtime__';
describe('issue-57 AC1 — supported-runtime metadata has one projected source of truth', () => {
test('installer allRuntimes, interactive runtimeMap, and registry agree on the supported set', () => {
const installable = sorted(allRuntimes);
assert.deepStrictEqual(
installable,
sorted(Object.values(runtimeMap)),
'Drift: bin/install.js `allRuntimes` and the interactive `runtimeMap` selection menu '
+ 'diverged. A runtime selectable in the prompt but absent from allRuntimes (or vice '
+ 'versa) is a supported-runtime call site that skipped the projection.',
);
assert.deepStrictEqual(
installable,
sorted(ALLOWED_CONFIG_RUNTIMES),
'Drift: bin/install.js `allRuntimes` and `ALLOWED_CONFIG_RUNTIMES` (runtime config '
+ 'adapter registry) diverged. A runtime added to an installer call site without a '
+ 'registry adapter entry bypasses the registry projection — register it in '
+ 'src/runtime-config-adapter-registry.cts.',
);
});
test('every installable runtime resolves a config intent through the registry', () => {
for (const runtime of allRuntimes) {
const intent = resolveRuntimeConfigIntent(runtime);
assert.equal(
intent.runtime,
runtime,
`${runtime} must resolve its own config intent through resolveRuntimeConfigIntent`,
);
}
});
test('every installable runtime resolves a global config dir through runtime-homes', () => {
for (const runtime of allRuntimes) {
const dir = getGlobalConfigDir(runtime);
assert.equal(typeof dir, 'string', `${runtime} config dir must be a string`);
assert.ok(dir.length > 0, `${runtime} must resolve a non-empty global config dir`);
}
});
});
describe('issue-57 AC2 — config-mutation dispatch is closed over the explicit registry', () => {
test('every config intent uses a registry-declared install surface', () => {
const surfaces = new Set(INSTALL_SURFACES);
for (const runtime of allRuntimes) {
const { installSurface } = resolveRuntimeConfigIntent(runtime);
assert.ok(
surfaces.has(installSurface),
`${runtime} dispatches config via unregistered surface "${installSurface}" — add it `
+ 'to INSTALL_SURFACES in the registry instead of branching on it inline.',
);
}
});
test('every finishInstall permission writer is null or a registry-known runtime', () => {
// Registry-derived (no hand-maintained vocabulary): a permission writer either
// names a runtime that is itself in the registry, or is null. A writer pointing
// at an unregistered runtime would mean finishInstall dispatches a config mutation
// outside the registry's known set.
for (const runtime of allRuntimes) {
const { finishPermissionWriter } = resolveRuntimeConfigIntent(runtime);
assert.ok(
finishPermissionWriter === null || ALLOWED_CONFIG_RUNTIMES.has(finishPermissionWriter),
`${runtime} uses finishPermissionWriter "${finishPermissionWriter}", which is neither `
+ 'null nor a registry-known runtime — route it through a registered adapter.',
);
}
});
test('unknown runtime fails loudly through both strict projections (no silent fallthrough)', () => {
assert.throws(
() => resolveRuntimeConfigIntent(SENTINEL),
TypeError,
'config adapter registry must reject an unknown runtime, not dispatch it silently',
);
assert.throws(
() => resolveRuntimeArtifactLayout(SENTINEL, path.join(os.tmpdir(), 'gsd-57'), 'global'),
TypeError,
'artifact-layout projection must reject an unknown runtime',
);
});
test('registry rejects prototype-chain keys (no proto-pollution dispatch bypass)', () => {
for (const key of ['__proto__', 'constructor', 'prototype', 'toString']) {
assert.throws(
() => resolveRuntimeConfigIntent(key),
TypeError,
`${key} must throw, not resolve via the prototype chain`,
);
}
});
// allow-test-rule: structural guard over bin/install.js source. Behavioral assertions
// cannot observe inline `runtime === '...'` config branching, so this enforces that
// every inline per-runtime branch references a runtime the adapter registry knows
// about — a NEW branch against an unregistered runtime name fails here. It matches
// positive equality only (`runtime === '<name>'` / `runtime === "<name>"`, both quote
// styles), so `runtime !== 'string'`-style type guards are not implicated. See the
// "coverage boundary" note at the top of the file for what this can and cannot catch.
test('every inline `runtime === "..."` branch references a registry-known runtime', () => {
const src = fs.readFileSync(path.join(ROOT, 'bin', 'install.js'), 'utf8');
const literals = new Set(
[...src.matchAll(/runtime === (?:'([a-z][a-z0-9-]*)'|"([a-z][a-z0-9-]*)")/g)]
.map((m) => m[1] ?? m[2]),
);
assert.ok(literals.size > 0, 'expected to find inline runtime comparisons in bin/install.js');
const unregistered = [...literals].filter((r) => !ALLOWED_CONFIG_RUNTIMES.has(r));
assert.deepStrictEqual(
unregistered,
[],
`inline 'runtime === "..."' branch(es) reference runtimes absent from the config adapter `
+ `registry: ${unregistered.join(', ')} — register them in `
+ 'src/runtime-config-adapter-registry.cts or route the logic through '
+ 'resolveRuntimeConfigIntent instead of branching inline.',
);
});
// allow-test-rule: delegation-presence guard. Catches wholesale removal of the registry
// dispatch (a regression to scattered per-runtime config branching). Presence-style, not
// absence-grep, so it does not bite on incidental non-config `runtime === '...'` checks.
test('bin/install.js requires the config adapter registry and dispatches through it', () => {
const src = fs.readFileSync(path.join(ROOT, 'bin', 'install.js'), 'utf8');
assert.ok(
src.includes('runtime-config-adapter-registry'),
'bin/install.js no longer requires the runtime config adapter registry',
);
assert.ok(
src.includes('resolveRuntimeConfigIntent('),
'bin/install.js no longer dispatches config through resolveRuntimeConfigIntent',
);
});
});