Files
msd-core/tests/host-integration-validator-parity.test.cjs
Tom Boucher 30d4b85de5 feat(#1684): negotiated host-integration interface (ADR-1239 Phase A) (#1690)
* feat(#1684): add negotiated host-integration interface module

ADR-1239 Phase A: a pure, additive, no-I/O module exposing PROTOCOL_VERSION, the 8-axis HOST_INTEGRATION_AXES closed vocabulary, the UNDOCUMENTED fail-closed sentinel, negotiateHostCapabilities (effective subset of host-declared and engine-known), a typed degradation ladder, and host-capability profiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#1684): validate and document host-integration axes (16 runtimes)

Extend validateRuntimeBody to validate the 8 hostIntegration axes (closed enums + undocumented sentinel + dispatch struct + reserved-key guards) and the widened runtime vocabulary; author a documentation-sourced hostIntegration block in all 16 runtime descriptors; regenerate the registry. Every per-CLI value is documented (cited) or the explicit undocumented sentinel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1684): add host-integration capability matrix and adr amendment

New per-CLI, per-axis citation reference (value/source/evidence for all 16 CLIs); ADR-1239 Phase-A-implemented amendment; CONTEXT.md glossary seam entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1684): harden dispatch negotiation edge cases

Code-review hardening: treat NaN/Infinity maxDepth as missing (fail-closed, +warning); reset nested/background when namedDispatch collapses to false (struct consistency); SAFE_DEFAULTS dispatch floor to read-only; warn on non-finite protocolVersion; symmetric undocumented warnings for dispatch fields. Pure module — no consumers; behaviour fail-closed throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1684): register host-integration.cjs in lint-ignore and inventory

New tsc-generated bin/lib artifact: add to the eslint ignore list (ADR-457 — lint the .cts source), regenerate docs/INVENTORY-MANIFEST.json, and add the docs/INVENTORY.md CLI-modules row. Fixes the 3 gsd-test failures (551-eslint-bin-lib-coverage x2 + inventory-manifest-sync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1684): add changeset fragment for host-integration interface

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1684): add how-to for sourcing a host's integration axes

Diataxis how-to guide for adding/updating a host's runtime.hostIntegration axes from authoritative docs, the undocumented-sentinel rule, validation, and extending the closed vocabulary. Completes the Step-5 doc quadrants (reference + explanation + how-to). Indexed in docs/README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:33:06 -04:00

357 lines
14 KiB
JavaScript

'use strict';
/**
* ADR-1239 Phase A: Parity guard — validator VALID_* sets MUST exactly match
* HOST_INTEGRATION_AXES arrays from host-integration.cjs.
*
* If either side drifts, this test fails immediately (not silently).
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const {
HOST_INTEGRATION_AXES,
} = require(path.join(__dirname, '../gsd-core/bin/lib/host-integration.cjs'));
const {
_HOST_INTEGRATION_VOCAB,
validateCapability,
} = require(path.join(__dirname, '../gsd-core/bin/lib/capability-validator.cjs'));
// Sort for deterministic comparison
function sorted(arr) {
return [...arr].sort();
}
// Minimal valid runtime capability descriptor (all documented values)
function makeMinimalRuntimeCap(overrides = {}) {
return {
id: 'test-runtime',
role: 'runtime',
title: 'Test Runtime',
description: 'Test runtime capability for parity tests',
tier: 'core',
requires: [],
version: '1.0.0',
runtime: {
configHome: {
kind: 'dot-home',
name: '.test-runtime',
env: [],
},
configFormat: 'markdown',
artifactLayout: {
global: [],
local: [],
},
commandStyle: 'slash-hyphen',
hooksSurface: 'none',
sandboxTier: 'none',
supportTier: 1,
installSurface: 'profile-marker-only',
writesSharedSettings: false,
permissionWriter: null,
extendedHookEvents: [],
hostIntegration: {
embeddingMode: 'imperative',
commandSurface: 'slash-file',
modelMode: 'passive',
hookBus: 'host',
stateIO: 'filesystem',
transport: 'mcp',
runtime: 'node',
dispatch: {
namedDispatch: true,
nested: false,
maxDepth: 1,
background: false,
subagentToolkit: 'full',
},
},
...overrides,
},
};
}
describe('ADR-1239 Phase A: host-integration validator parity', () => {
test('_HOST_INTEGRATION_VOCAB is exported from capability-validator.cjs', () => {
assert.ok(
_HOST_INTEGRATION_VOCAB !== undefined && _HOST_INTEGRATION_VOCAB !== null,
'_HOST_INTEGRATION_VOCAB must be exported from capability-validator.cjs',
);
assert.strictEqual(typeof _HOST_INTEGRATION_VOCAB, 'object');
});
test('embeddingMode: validator set === HOST_INTEGRATION_AXES.embeddingMode', () => {
assert.deepEqual(
sorted(_HOST_INTEGRATION_VOCAB.embeddingMode),
sorted(HOST_INTEGRATION_AXES.embeddingMode),
'validator VALID_EMBEDDING_MODES must exactly match HOST_INTEGRATION_AXES.embeddingMode',
);
});
test('commandSurface: validator set === HOST_INTEGRATION_AXES.commandSurface', () => {
assert.deepEqual(
sorted(_HOST_INTEGRATION_VOCAB.commandSurface),
sorted(HOST_INTEGRATION_AXES.commandSurface),
'validator VALID_COMMAND_SURFACES must exactly match HOST_INTEGRATION_AXES.commandSurface',
);
});
test('modelMode: validator set === HOST_INTEGRATION_AXES.modelMode', () => {
assert.deepEqual(
sorted(_HOST_INTEGRATION_VOCAB.modelMode),
sorted(HOST_INTEGRATION_AXES.modelMode),
'validator VALID_MODEL_MODES must exactly match HOST_INTEGRATION_AXES.modelMode',
);
});
test('hookBus: validator set === HOST_INTEGRATION_AXES.hookBus', () => {
assert.deepEqual(
sorted(_HOST_INTEGRATION_VOCAB.hookBus),
sorted(HOST_INTEGRATION_AXES.hookBus),
'validator VALID_HOOK_BUSES must exactly match HOST_INTEGRATION_AXES.hookBus',
);
});
test('stateIO: validator set === HOST_INTEGRATION_AXES.stateIO', () => {
assert.deepEqual(
sorted(_HOST_INTEGRATION_VOCAB.stateIO),
sorted(HOST_INTEGRATION_AXES.stateIO),
'validator VALID_STATE_IO must exactly match HOST_INTEGRATION_AXES.stateIO',
);
});
test('transport: validator set === HOST_INTEGRATION_AXES.transport', () => {
assert.deepEqual(
sorted(_HOST_INTEGRATION_VOCAB.transport),
sorted(HOST_INTEGRATION_AXES.transport),
'validator VALID_TRANSPORTS must exactly match HOST_INTEGRATION_AXES.transport',
);
});
test('runtime (axis): validator set === HOST_INTEGRATION_AXES.runtime (8 documented values)', () => {
assert.deepEqual(
sorted(_HOST_INTEGRATION_VOCAB.runtime),
sorted(HOST_INTEGRATION_AXES.runtime),
'validator VALID_HOST_RUNTIMES must exactly match HOST_INTEGRATION_AXES.runtime',
);
});
test('subagentToolkit: validator set === HOST_INTEGRATION_AXES.subagentToolkit', () => {
assert.deepEqual(
sorted(_HOST_INTEGRATION_VOCAB.subagentToolkit),
sorted(HOST_INTEGRATION_AXES.subagentToolkit),
'validator VALID_SUBAGENT_TOOLKITS must exactly match HOST_INTEGRATION_AXES.subagentToolkit',
);
});
test('all axis keys in HOST_INTEGRATION_AXES are covered by _HOST_INTEGRATION_VOCAB', () => {
const axisKeys = Object.keys(HOST_INTEGRATION_AXES).sort();
const vocabKeys = Object.keys(_HOST_INTEGRATION_VOCAB).sort();
assert.deepEqual(
vocabKeys,
axisKeys,
'_HOST_INTEGRATION_VOCAB must cover exactly the same axis keys as HOST_INTEGRATION_AXES',
);
});
test('_HOST_INTEGRATION_VOCAB does NOT include "undocumented" (documented vocab only)', () => {
for (const [axis, values] of Object.entries(_HOST_INTEGRATION_VOCAB)) {
assert.ok(
!values.includes('undocumented'),
`_HOST_INTEGRATION_VOCAB.${axis} must not include "undocumented" (sentinel is NOT documented vocab)`,
);
}
});
});
// ---------------------------------------------------------------------------
// Behavioral: "undocumented" passes validator; bogus values still fail
// ---------------------------------------------------------------------------
describe('ADR-1239 validator behavioral: undocumented sentinel passes, bogus fails', () => {
const SCALAR_AXES = ['embeddingMode', 'commandSurface', 'modelMode', 'hookBus', 'stateIO', 'transport', 'runtime'];
for (const axis of SCALAR_AXES) {
test(`hostIntegration.${axis}:"undocumented" → ZERO validator errors`, () => {
const cap = makeMinimalRuntimeCap({
hostIntegration: {
...makeMinimalRuntimeCap().runtime.hostIntegration,
[axis]: 'undocumented',
},
});
const errors = validateCapability(cap, 'test-runtime');
const hiErrors = errors.filter((e) => e.includes('hostIntegration.' + axis));
assert.strictEqual(hiErrors.length, 0,
`"undocumented" for axis "${axis}" must produce no validator errors; got: ${hiErrors.join(', ')}`);
});
test(`hostIntegration.${axis}:"zzz" → produces a validator error`, () => {
const cap = makeMinimalRuntimeCap({
hostIntegration: {
...makeMinimalRuntimeCap().runtime.hostIntegration,
[axis]: 'zzz',
},
});
const errors = validateCapability(cap, 'test-runtime');
const hiErrors = errors.filter((e) => e.includes('hostIntegration.' + axis));
assert.ok(hiErrors.length > 0,
`bogus value "zzz" for axis "${axis}" must produce a validator error`);
});
}
test('dispatch.namedDispatch:"undocumented" → ZERO validator errors for that field', () => {
const cap = makeMinimalRuntimeCap({
hostIntegration: {
...makeMinimalRuntimeCap().runtime.hostIntegration,
dispatch: {
namedDispatch: 'undocumented',
nested: 'undocumented',
maxDepth: 'undocumented',
background: 'undocumented',
subagentToolkit: 'undocumented',
},
},
});
const errors = validateCapability(cap, 'test-runtime');
const dispatchErrors = errors.filter((e) => e.includes('hostIntegration.dispatch'));
assert.strictEqual(dispatchErrors.length, 0,
`"undocumented" for all dispatch fields must produce no validator errors; got: ${dispatchErrors.join(', ')}`);
});
test('dispatch boolean fields: true/false still accepted', () => {
const cap = makeMinimalRuntimeCap();
const errors = validateCapability(cap, 'test-runtime');
const dispatchErrors = errors.filter((e) => e.includes('hostIntegration.dispatch'));
assert.strictEqual(dispatchErrors.length, 0,
`Valid boolean dispatch fields must produce no errors; got: ${dispatchErrors.join(', ')}`);
});
});
// ---------------------------------------------------------------------------
// Fix 3: validator must reject reserved keys on hostIntegration and dispatch
// ---------------------------------------------------------------------------
describe('Fix 3: reserved-key guard on hostIntegration and hostIntegration.dispatch', () => {
// Base valid runtime body (all documented values, claude layout)
// We build it via JSON.parse to produce an own "__proto__" key that would
// normally be swallowed by a spread (JSON.parse always produces own props).
const BASE_RUNTIME_JSON = JSON.stringify({
id: 'test-runtime',
role: 'runtime',
title: 'Test',
description: 'Test runtime',
tier: 'core',
requires: [],
version: '1.6.0',
engines: { gsd: '>=1.6.0' },
runtime: {
configHome: { kind: 'dot-home', name: '.test', env: [] },
configFormat: 'settings-json',
artifactLayout: { global: [], local: [] },
commandStyle: 'slash-hyphen',
hooksSurface: 'settings-json',
hookEvents: 'claude',
sandboxTier: 'none',
supportTier: 1,
installSurface: 'settings-json',
writesSharedSettings: true,
permissionWriter: null,
extendedHookEvents: ['SubagentStop', 'Stop', 'PreCompact', 'FileChanged'],
hostIntegration: {
embeddingMode: 'imperative',
commandSurface: 'slash-file',
dispatch: {
namedDispatch: true,
nested: true,
maxDepth: 5,
background: true,
subagentToolkit: 'full',
},
modelMode: 'passive',
hookBus: 'host',
stateIO: 'filesystem',
transport: 'mcp',
runtime: 'node',
},
},
});
test('baseline (no reserved keys) → ZERO errors', () => {
const cap = JSON.parse(BASE_RUNTIME_JSON);
const errors = validateCapability(cap, 'test-runtime');
assert.strictEqual(errors.length, 0,
'Baseline with no reserved keys must produce zero errors; got: ' + errors.join(', '));
});
test('hostIntegration with own __proto__ key → error mentioning "reserved key" and "__proto__"', () => {
// Inject a raw __proto__ key via JSON string manipulation — JSON.parse gives it
// as an OWN property (unlike { ..., __proto__: ... } which sets prototype chain).
const json = BASE_RUNTIME_JSON.replace(
'"hostIntegration":{',
'"hostIntegration":{"__proto__":{"polluted":true},',
);
const cap = JSON.parse(json);
// Verify the own-key is actually present (our assumption about JSON.parse)
assert.ok(
Object.prototype.hasOwnProperty.call(cap.runtime.hostIntegration, '__proto__'),
'JSON.parse must produce an own __proto__ key on hostIntegration',
);
const errors = validateCapability(cap, 'test-runtime');
const reservedErrors = errors.filter((e) => e.includes('reserved key') && e.includes('__proto__'));
assert.ok(reservedErrors.length > 0,
'Must produce an error mentioning "reserved key" and "__proto__" for hostIntegration; got: ' + errors.join(', '));
});
test('hostIntegration with own "constructor" key → error mentioning "reserved key" and "constructor"', () => {
const json = BASE_RUNTIME_JSON.replace(
'"hostIntegration":{',
'"hostIntegration":{"constructor":"polluted",',
);
const cap = JSON.parse(json);
assert.ok(
Object.prototype.hasOwnProperty.call(cap.runtime.hostIntegration, 'constructor'),
'JSON.parse must produce an own constructor key on hostIntegration',
);
const errors = validateCapability(cap, 'test-runtime');
const reservedErrors = errors.filter((e) => e.includes('reserved key') && e.includes('constructor'));
assert.ok(reservedErrors.length > 0,
'Must produce an error for "constructor" reserved key on hostIntegration; got: ' + errors.join(', '));
});
test('hostIntegration.dispatch with own __proto__ key → error mentioning "reserved key" and "__proto__"', () => {
const json = BASE_RUNTIME_JSON.replace(
'"dispatch":{',
'"dispatch":{"__proto__":{"polluted":true},',
);
const cap = JSON.parse(json);
assert.ok(
Object.prototype.hasOwnProperty.call(cap.runtime.hostIntegration.dispatch, '__proto__'),
'JSON.parse must produce an own __proto__ key on dispatch',
);
const errors = validateCapability(cap, 'test-runtime');
const reservedErrors = errors.filter((e) => e.includes('reserved key') && e.includes('__proto__'));
assert.ok(reservedErrors.length > 0,
'Must produce an error for "__proto__" reserved key on dispatch; got: ' + errors.join(', '));
});
test('hostIntegration.dispatch with own "prototype" key → error mentioning "reserved key" and "prototype"', () => {
const json = BASE_RUNTIME_JSON.replace(
'"dispatch":{',
'"dispatch":{"prototype":{"polluted":true},',
);
const cap = JSON.parse(json);
assert.ok(
Object.prototype.hasOwnProperty.call(cap.runtime.hostIntegration.dispatch, 'prototype'),
'JSON.parse must produce an own prototype key on dispatch',
);
const errors = validateCapability(cap, 'test-runtime');
const reservedErrors = errors.filter((e) => e.includes('reserved key') && e.includes('prototype'));
assert.ok(reservedErrors.length > 0,
'Must produce an error for "prototype" reserved key on dispatch; got: ' + errors.join(', '));
});
});