* 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>
This commit is contained in:
@@ -86,6 +86,16 @@ function runtimeCap(overrides) {
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -1775,6 +1775,16 @@ describe('C3: role:runtime body validation', () => {
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3218,6 +3228,16 @@ function makeRuntimeCap(overrides) {
|
||||
writesSharedSettings: true,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
...((overrides && overrides.runtime) ? overrides.runtime : {}),
|
||||
},
|
||||
...overrides,
|
||||
@@ -4283,6 +4303,16 @@ describe('ADR-857 phase 5f: cross-field consistency gate rejection tests (DEFECT
|
||||
writesSharedSettings: true,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
};
|
||||
if (overrides && typeof overrides === 'object') {
|
||||
@@ -5135,6 +5165,16 @@ describe('activationKey validation', () => {
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: false, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
};
|
||||
const errors = validateCapability(cap, 'cursor');
|
||||
|
||||
328
tests/host-integration-descriptors.test.cjs
Normal file
328
tests/host-integration-descriptors.test.cjs
Normal file
@@ -0,0 +1,328 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ADR-1239 Phase A: Descriptor tests — validate that all 16 role:runtime
|
||||
* capability descriptors have correct hostIntegration axes, pass the validator,
|
||||
* and negotiate correctly via the host-integration module.
|
||||
*
|
||||
* Expectations are derived from the generated capability registry and
|
||||
* .host-cli-final.json (source of truth). Values are verbatim; 'undocumented'
|
||||
* sentinels fail-closed in negotiation (safe documented default, never propagate).
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
const {
|
||||
negotiateHostCapabilities,
|
||||
profileOf,
|
||||
} = require(path.join(__dirname, '../gsd-core/bin/lib/host-integration.cjs'));
|
||||
|
||||
const registry = require(path.join(__dirname, '../gsd-core/bin/lib/capability-registry.cjs'));
|
||||
|
||||
const {
|
||||
validateCapability,
|
||||
} = require(path.join(__dirname, '../gsd-core/bin/lib/capability-validator.cjs'));
|
||||
|
||||
// All 8 scalar hostIntegration axis keys
|
||||
const SCALAR_AXES = ['embeddingMode', 'commandSurface', 'modelMode', 'hookBus', 'stateIO', 'transport', 'runtime'];
|
||||
// All 5 dispatch sub-keys
|
||||
const DISPATCH_KEYS = ['namedDispatch', 'nested', 'maxDepth', 'background', 'subagentToolkit'];
|
||||
|
||||
// All 16 runtime IDs (ordered alphabetically)
|
||||
const RUNTIME_IDS = [
|
||||
'antigravity', 'augment', 'claude', 'cline', 'codebuddy',
|
||||
'codex', 'copilot', 'cursor', 'gemini', 'hermes',
|
||||
'kilo', 'kimi', 'opencode', 'qwen', 'trae', 'windsurf',
|
||||
];
|
||||
|
||||
// Contract-pinned profile split (derived from .host-cli-final.json):
|
||||
// programmatic-cli: claude, cline, cursor, hermes, kilo, kimi, opencode, qwen, trae (9)
|
||||
// declarative-cli: antigravity, augment, codebuddy, codex, copilot, gemini, windsurf (7)
|
||||
// ide: 0
|
||||
const EXPECTED_PROFILES = {
|
||||
claude: 'programmatic-cli',
|
||||
cline: 'programmatic-cli',
|
||||
cursor: 'programmatic-cli',
|
||||
hermes: 'programmatic-cli',
|
||||
kilo: 'programmatic-cli',
|
||||
kimi: 'programmatic-cli',
|
||||
opencode: 'programmatic-cli',
|
||||
qwen: 'programmatic-cli',
|
||||
trae: 'programmatic-cli',
|
||||
antigravity: 'declarative-cli',
|
||||
augment: 'declarative-cli',
|
||||
codebuddy: 'declarative-cli',
|
||||
codex: 'declarative-cli',
|
||||
copilot: 'declarative-cli',
|
||||
gemini: 'declarative-cli',
|
||||
windsurf: 'declarative-cli',
|
||||
};
|
||||
|
||||
describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
// ─── Registry shape ──────────────────────────────────────────────────────────
|
||||
|
||||
test('registry.runtimes contains all 16 expected runtime ids', () => {
|
||||
for (const id of RUNTIME_IDS) {
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(registry.runtimes, id),
|
||||
'registry.runtimes must contain "' + id + '"',
|
||||
);
|
||||
}
|
||||
assert.strictEqual(
|
||||
Object.keys(registry.runtimes).length,
|
||||
16,
|
||||
'registry.runtimes must have exactly 16 entries',
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Per-runtime assertions ───────────────────────────────────────────────────
|
||||
|
||||
for (const id of RUNTIME_IDS) {
|
||||
describe('runtime: ' + id, () => {
|
||||
const cap = registry.runtimes[id];
|
||||
const hi = cap && cap.runtime && cap.runtime.hostIntegration;
|
||||
|
||||
// (i) Validator passes with zero errors
|
||||
test('(i) validateCapability returns zero errors', () => {
|
||||
const errors = validateCapability(cap, id);
|
||||
assert.deepEqual(
|
||||
errors,
|
||||
[],
|
||||
id + ': validateCapability must return no errors, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// (ii) hostIntegration object is present with all required keys
|
||||
test('(ii) cap.runtime.hostIntegration is present with all 8 axis keys and 5 dispatch sub-keys', () => {
|
||||
assert.ok(
|
||||
hi !== undefined && hi !== null && typeof hi === 'object',
|
||||
id + ': cap.runtime.hostIntegration must be a non-null object',
|
||||
);
|
||||
// All 8 scalar axes present
|
||||
for (const axis of SCALAR_AXES) {
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(hi, axis),
|
||||
id + ': hostIntegration must have axis "' + axis + '"',
|
||||
);
|
||||
}
|
||||
// dispatch is an object
|
||||
assert.ok(
|
||||
hi.dispatch !== null && typeof hi.dispatch === 'object',
|
||||
id + ': hostIntegration.dispatch must be a non-null object',
|
||||
);
|
||||
// All 5 dispatch sub-keys present
|
||||
for (const key of DISPATCH_KEYS) {
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(hi.dispatch, key),
|
||||
id + ': hostIntegration.dispatch must have key "' + key + '"',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// (iii) negotiateHostCapabilities does not throw and behaves correctly
|
||||
test('(iii) negotiateHostCapabilities: documented scalars pass through; undocumented scalars degrade with warning', () => {
|
||||
assert.ok(hi, id + ': hostIntegration must exist to negotiate');
|
||||
let result;
|
||||
assert.doesNotThrow(() => {
|
||||
result = negotiateHostCapabilities(hi);
|
||||
}, id + ': negotiateHostCapabilities must not throw');
|
||||
|
||||
const eff = result.effective;
|
||||
|
||||
// For each scalar axis: if declared !== 'undocumented', effective === declared
|
||||
// If declared === 'undocumented', effective !== 'undocumented' (safe default) and
|
||||
// warnings must mention that axis.
|
||||
for (const axis of SCALAR_AXES) {
|
||||
const declared = hi[axis];
|
||||
if (declared !== 'undocumented') {
|
||||
assert.strictEqual(
|
||||
eff[axis],
|
||||
declared,
|
||||
id + ': effective.' + axis + ' must equal declared (' + JSON.stringify(declared) + '), got: ' + JSON.stringify(eff[axis]),
|
||||
);
|
||||
} else {
|
||||
// fail-closed: effective must be a documented safe default, not 'undocumented'
|
||||
assert.notStrictEqual(
|
||||
eff[axis],
|
||||
'undocumented',
|
||||
id + ': effective.' + axis + ' must NOT be "undocumented" (fail-closed)',
|
||||
);
|
||||
// warnings must mention this axis
|
||||
const mentionsAxis = result.warnings.some((w) => w.includes(axis));
|
||||
assert.ok(
|
||||
mentionsAxis,
|
||||
id + ': result.warnings must mention axis "' + axis + '" when declared is undocumented, got: ' + JSON.stringify(result.warnings),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// (iii-b) dispatch negotiation for namedDispatch
|
||||
test('(iii-b) dispatch.namedDispatch negotiation', () => {
|
||||
assert.ok(hi, id + ': hostIntegration must exist to negotiate');
|
||||
const result = negotiateHostCapabilities(hi);
|
||||
|
||||
const declaredND = hi.dispatch && hi.dispatch.namedDispatch;
|
||||
|
||||
if (declaredND === true) {
|
||||
// documented as true → effective must be true
|
||||
assert.strictEqual(
|
||||
result.effective.dispatch.namedDispatch,
|
||||
true,
|
||||
id + ': effective.dispatch.namedDispatch must be true when declared is true',
|
||||
);
|
||||
} else if (declaredND === 'undocumented') {
|
||||
// undocumented → fail-closed: effective namedDispatch must be false
|
||||
assert.strictEqual(
|
||||
result.effective.dispatch.namedDispatch,
|
||||
false,
|
||||
id + ': effective.dispatch.namedDispatch must be false when declared is "undocumented" (fail-closed)',
|
||||
);
|
||||
// dispatch.effectiveLevel must be 'absent' (no named dispatch)
|
||||
assert.strictEqual(
|
||||
result.points.dispatch.effectiveLevel,
|
||||
'absent',
|
||||
id + ': points.dispatch.effectiveLevel must be "absent" when namedDispatch is undocumented',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// (iv) profileOf returns expected profile
|
||||
test('(iv) profileOf returns expected profile', () => {
|
||||
assert.ok(hi, id + ': hostIntegration must exist to profile');
|
||||
const profile = profileOf(hi);
|
||||
assert.ok(
|
||||
profile !== null,
|
||||
id + ': profileOf must return a non-null profile',
|
||||
);
|
||||
assert.strictEqual(
|
||||
profile,
|
||||
EXPECTED_PROFILES[id],
|
||||
id + ': profileOf must return "' + EXPECTED_PROFILES[id] + '" (got: "' + profile + '")',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Contract-pin profile split ───────────────────────────────────────────────
|
||||
|
||||
test('contract-pin: exactly 9 programmatic-cli, 7 declarative-cli, 0 ide', () => {
|
||||
const counts = { 'programmatic-cli': 0, 'declarative-cli': 0, 'ide': 0 };
|
||||
for (const id of RUNTIME_IDS) {
|
||||
const cap = registry.runtimes[id];
|
||||
const hi = cap && cap.runtime && cap.runtime.hostIntegration;
|
||||
assert.ok(hi, id + ': hostIntegration must exist for profile count');
|
||||
const profile = profileOf(hi);
|
||||
assert.ok(profile !== null, id + ': profileOf must be non-null');
|
||||
if (counts[profile] !== undefined) {
|
||||
counts[profile]++;
|
||||
}
|
||||
}
|
||||
assert.strictEqual(counts['programmatic-cli'], 9, 'Must have exactly 9 programmatic-cli runtimes');
|
||||
assert.strictEqual(counts['declarative-cli'], 7, 'Must have exactly 7 declarative-cli runtimes');
|
||||
assert.strictEqual(counts['ide'], 0, 'Must have exactly 0 ide runtimes');
|
||||
});
|
||||
|
||||
test('contract-pin: spot-check claude→programmatic-cli, codex→declarative-cli, opencode→programmatic-cli, gemini→declarative-cli', () => {
|
||||
const checks = [
|
||||
['claude', 'programmatic-cli'],
|
||||
['codex', 'declarative-cli'],
|
||||
['opencode', 'programmatic-cli'],
|
||||
['gemini', 'declarative-cli'],
|
||||
];
|
||||
for (const [id, expectedProfile] of checks) {
|
||||
const cap = registry.runtimes[id];
|
||||
const hi = cap && cap.runtime && cap.runtime.hostIntegration;
|
||||
assert.ok(hi, id + ': hostIntegration must exist');
|
||||
const profile = profileOf(hi);
|
||||
assert.strictEqual(
|
||||
profile,
|
||||
expectedProfile,
|
||||
id + ': profileOf must return "' + expectedProfile + '" (got: "' + profile + '")',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── NEGATIVE cases ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('NEGATIVE: invalid hostIntegration.embeddingMode triggers validator error', () => {
|
||||
test('embeddingMode "bogus" produces a validator error naming embeddingMode', () => {
|
||||
const cap = {
|
||||
id: 'test-neg',
|
||||
role: 'runtime',
|
||||
version: '1.0.0',
|
||||
title: 'Test Negative',
|
||||
description: 'Negative case for hostIntegration validation.',
|
||||
tier: 'core',
|
||||
requires: [],
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.test-neg', 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: [],
|
||||
hostIntegration: {
|
||||
embeddingMode: 'bogus',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' },
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
};
|
||||
const errors = validateCapability(cap, 'test-neg');
|
||||
assert.ok(errors.length > 0, 'Expected validation errors for bogus embeddingMode');
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('embeddingMode')),
|
||||
'At least one error must mention embeddingMode, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NEGATIVE: missing hostIntegration produces required-object error', () => {
|
||||
test('runtime body without hostIntegration produces the required-object error', () => {
|
||||
const cap = {
|
||||
id: 'test-missing-hi',
|
||||
role: 'runtime',
|
||||
version: '1.0.0',
|
||||
title: 'Test Missing HI',
|
||||
description: 'Negative case for missing hostIntegration.',
|
||||
tier: 'core',
|
||||
requires: [],
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.test-missing-hi', 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: [],
|
||||
// hostIntegration intentionally absent
|
||||
},
|
||||
};
|
||||
const errors = validateCapability(cap, 'test-missing-hi');
|
||||
assert.ok(errors.length > 0, 'Expected validation errors for missing hostIntegration');
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('hostIntegration') && e.includes('required')),
|
||||
'At least one error must mention hostIntegration and required, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
356
tests/host-integration-validator-parity.test.cjs
Normal file
356
tests/host-integration-validator-parity.test.cjs
Normal file
@@ -0,0 +1,356 @@
|
||||
'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(', '));
|
||||
});
|
||||
});
|
||||
888
tests/host-integration.test.cjs
Normal file
888
tests/host-integration.test.cjs
Normal file
@@ -0,0 +1,888 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Unit tests for host-integration.cjs (ADR-1239 Phase A).
|
||||
* Pure, additive, no-I/O module — no temp dirs needed.
|
||||
* Uses node:test + node:assert/strict.
|
||||
* Requires the COMPILED artifact: ../gsd-core/bin/lib/host-integration.cjs
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const hi = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const {
|
||||
PROTOCOL_VERSION,
|
||||
HOST_INTEGRATION_AXES,
|
||||
INTERFACE_POINTS,
|
||||
PROFILE_BASELINES,
|
||||
DEFAULT_ENGINE,
|
||||
UNDOCUMENTED,
|
||||
degradationFor,
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
} = hi;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CONTRACT-PIN: constants and vocabulary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('CONTRACT-PIN', () => {
|
||||
test('PROTOCOL_VERSION === 1', () => {
|
||||
assert.strictEqual(PROTOCOL_VERSION, 1);
|
||||
});
|
||||
|
||||
test('HOST_INTEGRATION_AXES is frozen', () => {
|
||||
assert.ok(Object.isFrozen(HOST_INTEGRATION_AXES), 'HOST_INTEGRATION_AXES must be frozen');
|
||||
});
|
||||
|
||||
test('each axis sub-array is frozen', () => {
|
||||
for (const [axis, arr] of Object.entries(HOST_INTEGRATION_AXES)) {
|
||||
assert.ok(Object.isFrozen(arr), `HOST_INTEGRATION_AXES.${axis} must be frozen`);
|
||||
}
|
||||
});
|
||||
|
||||
test('embeddingMode values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.embeddingMode].sort(),
|
||||
['declarative', 'imperative'],
|
||||
);
|
||||
});
|
||||
|
||||
test('commandSurface values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.commandSurface].sort(),
|
||||
['palette', 'prose-only', 'slash-file', 'slash-programmatic', 'slash-toml'],
|
||||
);
|
||||
});
|
||||
|
||||
test('modelMode values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.modelMode].sort(),
|
||||
['active', 'passive'],
|
||||
);
|
||||
});
|
||||
|
||||
test('hookBus values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.hookBus].sort(),
|
||||
['engine', 'host', 'none'],
|
||||
);
|
||||
});
|
||||
|
||||
test('stateIO values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.stateIO].sort(),
|
||||
['filesystem', 'sandboxed-storage', 'session-log-append'],
|
||||
);
|
||||
});
|
||||
|
||||
test('transport values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.transport].sort(),
|
||||
['mcp', 'native-extension'],
|
||||
);
|
||||
});
|
||||
|
||||
test('runtime values (sorted) — 8 documented values', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.runtime].sort(),
|
||||
['bun', 'electron', 'go', 'node', 'other', 'python', 'rust', 'sandboxed-web'],
|
||||
);
|
||||
});
|
||||
|
||||
test('UNDOCUMENTED === "undocumented"', () => {
|
||||
assert.equal(UNDOCUMENTED, 'undocumented');
|
||||
});
|
||||
|
||||
test('subagentToolkit values (sorted)', () => {
|
||||
assert.deepStrictEqual(
|
||||
[...HOST_INTEGRATION_AXES.subagentToolkit].sort(),
|
||||
['full', 'read-only'],
|
||||
);
|
||||
});
|
||||
|
||||
test('INTERFACE_POINTS frozen and contains expected values', () => {
|
||||
assert.ok(Object.isFrozen(INTERFACE_POINTS), 'INTERFACE_POINTS must be frozen');
|
||||
const expected = ['command', 'dispatch', 'model', 'hooks', 'state', 'artifact'].sort();
|
||||
assert.deepStrictEqual([...INTERFACE_POINTS].sort(), expected);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// degradationFor — happy path per enum value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('degradationFor — happy path', () => {
|
||||
test('command: slash-file → full', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'slash-file' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
assert.strictEqual(typeof r.fallback, 'string');
|
||||
});
|
||||
|
||||
test('command: slash-programmatic → full', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'slash-programmatic' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('command: slash-toml → degraded', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'slash-toml' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('command: palette → degraded', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'palette' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('command: prose-only → absent', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'prose-only' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.ok(r.fallback.length > 0, 'fallback must be non-empty for prose-only');
|
||||
});
|
||||
|
||||
test('model: active → full', () => {
|
||||
const r = degradationFor('model', { modelMode: 'active' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('model: passive → degraded', () => {
|
||||
const r = degradationFor('model', { modelMode: 'passive' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('hooks: host → full', () => {
|
||||
const r = degradationFor('hooks', { hookBus: 'host' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('hooks: engine → degraded', () => {
|
||||
const r = degradationFor('hooks', { hookBus: 'engine' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('hooks: none → absent', () => {
|
||||
const r = degradationFor('hooks', { hookBus: 'none' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
});
|
||||
|
||||
test('state: filesystem → full', () => {
|
||||
const r = degradationFor('state', { stateIO: 'filesystem' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('state: sandboxed-storage → degraded', () => {
|
||||
const r = degradationFor('state', { stateIO: 'sandboxed-storage' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('state: session-log-append → degraded', () => {
|
||||
const r = degradationFor('state', { stateIO: 'session-log-append' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('artifact: slash-file → full', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'slash-file' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('artifact: slash-programmatic → full', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'slash-programmatic' });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('artifact: slash-toml → degraded', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'slash-toml' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('artifact: prose-only → degraded', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'prose-only' });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('artifact: palette → absent', () => {
|
||||
const r = degradationFor('artifact', { commandSurface: 'palette' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
});
|
||||
|
||||
// dispatch variants
|
||||
test('dispatch: no namedDispatch → absent', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
});
|
||||
|
||||
test('dispatch: maxDepth===0 → absent', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: true, maxDepth: 0, background: true, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
});
|
||||
|
||||
test('dispatch: unbounded (-1) nested → full', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('dispatch: nested maxDepth>=2 → full', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: true, maxDepth: 2, background: true, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'full');
|
||||
});
|
||||
|
||||
test('dispatch: full but subagentToolkit read-only → degraded', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'read-only' } });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
|
||||
test('dispatch: flat (maxDepth===1) → degraded', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'degraded');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// degradationFor — EVERY enum value returns a defined result with valid level
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('degradationFor — all enum values return valid level', () => {
|
||||
const VALID_LEVELS = new Set(['full', 'degraded', 'absent']);
|
||||
|
||||
test('command — all commandSurface values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.commandSurface) {
|
||||
const r = degradationFor('command', { commandSurface: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `command/${v}: level '${r.level}' invalid`);
|
||||
assert.strictEqual(typeof r.fallback, 'string');
|
||||
}
|
||||
});
|
||||
|
||||
test('model — all modelMode values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.modelMode) {
|
||||
const r = degradationFor('model', { modelMode: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `model/${v}: level '${r.level}' invalid`);
|
||||
}
|
||||
});
|
||||
|
||||
test('hooks — all hookBus values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.hookBus) {
|
||||
const r = degradationFor('hooks', { hookBus: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `hooks/${v}: level '${r.level}' invalid`);
|
||||
}
|
||||
});
|
||||
|
||||
test('state — all stateIO values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.stateIO) {
|
||||
const r = degradationFor('state', { stateIO: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `state/${v}: level '${r.level}' invalid`);
|
||||
}
|
||||
});
|
||||
|
||||
test('artifact — all commandSurface values', () => {
|
||||
for (const v of HOST_INTEGRATION_AXES.commandSurface) {
|
||||
const r = degradationFor('artifact', { commandSurface: v });
|
||||
assert.ok(VALID_LEVELS.has(r.level), `artifact/${v}: level '${r.level}' invalid`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// degradationFor — unknown / missing axis → absent + unknown:true, never throws
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('degradationFor — unknown / missing axis', () => {
|
||||
test('unknown commandSurface value for command → absent + unknown:true', () => {
|
||||
const r = degradationFor('command', { commandSurface: 'zzz' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.strictEqual(r.unknown, true);
|
||||
});
|
||||
|
||||
test('missing commandSurface for command → absent + unknown:true', () => {
|
||||
const r = degradationFor('command', {});
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.strictEqual(r.unknown, true);
|
||||
});
|
||||
|
||||
test('unknown modelMode → absent + unknown:true', () => {
|
||||
const r = degradationFor('model', { modelMode: 'zzz' });
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.strictEqual(r.unknown, true);
|
||||
});
|
||||
|
||||
test('missing hookBus for hooks → absent + unknown:true', () => {
|
||||
const r = degradationFor('hooks', {});
|
||||
assert.strictEqual(r.level, 'absent');
|
||||
assert.strictEqual(r.unknown, true);
|
||||
});
|
||||
|
||||
test('no throw on unknown axis value', () => {
|
||||
assert.doesNotThrow(() => degradationFor('dispatch', { dispatch: 'not-an-object' }));
|
||||
});
|
||||
|
||||
test('no throw on completely empty axes', () => {
|
||||
for (const point of INTERFACE_POINTS) {
|
||||
assert.doesNotThrow(() => degradationFor(point, {}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// profileOf
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('profileOf', () => {
|
||||
test('profileOf(PROFILE_BASELINES["programmatic-cli"]) === "programmatic-cli"', () => {
|
||||
assert.strictEqual(profileOf(PROFILE_BASELINES['programmatic-cli']), 'programmatic-cli');
|
||||
});
|
||||
|
||||
test('profileOf(PROFILE_BASELINES["declarative-cli"]) === "declarative-cli"', () => {
|
||||
assert.strictEqual(profileOf(PROFILE_BASELINES['declarative-cli']), 'declarative-cli');
|
||||
});
|
||||
|
||||
test('profileOf(PROFILE_BASELINES["ide"]) === "ide"', () => {
|
||||
assert.strictEqual(profileOf(PROFILE_BASELINES['ide']), 'ide');
|
||||
});
|
||||
|
||||
test('imperative + sandboxed-web → ide', () => {
|
||||
assert.strictEqual(
|
||||
profileOf({ embeddingMode: 'imperative', runtime: 'sandboxed-web' }),
|
||||
'ide',
|
||||
);
|
||||
});
|
||||
|
||||
test('imperative + node → programmatic-cli', () => {
|
||||
assert.strictEqual(
|
||||
profileOf({ embeddingMode: 'imperative', runtime: 'node' }),
|
||||
'programmatic-cli',
|
||||
);
|
||||
});
|
||||
|
||||
test('declarative → declarative-cli', () => {
|
||||
assert.strictEqual(
|
||||
profileOf({ embeddingMode: 'declarative' }),
|
||||
'declarative-cli',
|
||||
);
|
||||
});
|
||||
|
||||
test('empty axes → null', () => {
|
||||
assert.strictEqual(profileOf({}), null);
|
||||
});
|
||||
|
||||
test('PROFILE_BASELINES are frozen', () => {
|
||||
assert.ok(Object.isFrozen(PROFILE_BASELINES), 'PROFILE_BASELINES must be frozen');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// negotiateHostCapabilities — HAPPY PATH
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('negotiateHostCapabilities — happy path', () => {
|
||||
test('declarative-cli baseline → effective matches, no warnings, points.command.effectiveLevel===full', () => {
|
||||
const baseline = PROFILE_BASELINES['declarative-cli'];
|
||||
const result = negotiateHostCapabilities(baseline);
|
||||
|
||||
// No warnings
|
||||
assert.deepStrictEqual(result.warnings, [], 'Expected no warnings for full declarative-cli baseline');
|
||||
|
||||
// Key points
|
||||
assert.strictEqual(result.points.command.effectiveLevel, 'full');
|
||||
assert.strictEqual(result.points.hooks.effectiveLevel, 'full');
|
||||
assert.strictEqual(result.points.state.effectiveLevel, 'full');
|
||||
|
||||
// protocolVersion
|
||||
assert.strictEqual(result.protocolVersion, PROTOCOL_VERSION);
|
||||
|
||||
// effective axes match baseline (scalar)
|
||||
assert.strictEqual(result.effective.embeddingMode, baseline.embeddingMode);
|
||||
assert.strictEqual(result.effective.commandSurface, baseline.commandSurface);
|
||||
assert.strictEqual(result.effective.modelMode, baseline.modelMode);
|
||||
assert.strictEqual(result.effective.hookBus, baseline.hookBus);
|
||||
assert.strictEqual(result.effective.stateIO, baseline.stateIO);
|
||||
|
||||
// effective dispatch has maxDepth resolved (declarative has maxDepth:1)
|
||||
assert.strictEqual(result.effective.dispatch.maxDepth, 1);
|
||||
assert.strictEqual(result.effective.dispatch.namedDispatch, true);
|
||||
});
|
||||
|
||||
test('all INTERFACE_POINTS are present in result.points', () => {
|
||||
const result = negotiateHostCapabilities(PROFILE_BASELINES['programmatic-cli']);
|
||||
for (const point of INTERFACE_POINTS) {
|
||||
assert.ok(point in result.points, `Missing point: ${point}`);
|
||||
assert.ok(['full', 'degraded', 'absent'].includes(result.points[point].effectiveLevel),
|
||||
`Invalid effectiveLevel for ${point}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// negotiateHostCapabilities — SECURITY / HOSTILE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('negotiateHostCapabilities — security / hostile', () => {
|
||||
test('(1) host declares future commandSurface at protocolVersion 99 → effective is KNOWN value, NOT the unknown one', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
commandSurface: 'future-surface',
|
||||
protocolVersion: 99,
|
||||
});
|
||||
// effective.commandSurface must be a KNOWN value
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES.commandSurface.includes(result.effective.commandSurface),
|
||||
`effective.commandSurface '${result.effective.commandSurface}' is not in known vocabulary`,
|
||||
);
|
||||
assert.notStrictEqual(result.effective.commandSurface, 'future-surface',
|
||||
'future-surface must NOT appear in effective');
|
||||
// A warning mentioning protocolVersion
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('protocolVersion') || warnText.includes('unknown'),
|
||||
`Expected a warning about protocolVersion or unknown value; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('(2) host modelMode active but engine passive → effective.modelMode === passive', () => {
|
||||
const restrictedEngine = {
|
||||
...DEFAULT_ENGINE,
|
||||
axes: { ...DEFAULT_ENGINE.axes, modelMode: 'passive' },
|
||||
};
|
||||
const result = negotiateHostCapabilities(
|
||||
{ ...PROFILE_BASELINES['programmatic-cli'], modelMode: 'active' },
|
||||
restrictedEngine,
|
||||
);
|
||||
assert.strictEqual(result.effective.modelMode, 'passive');
|
||||
});
|
||||
|
||||
test('(3) host dispatch maxDepth:5 nested:true but engine dispatch maxDepth:1 → effective.dispatch.maxDepth===1', () => {
|
||||
const restrictedEngine = {
|
||||
...DEFAULT_ENGINE,
|
||||
axes: {
|
||||
...DEFAULT_ENGINE.axes,
|
||||
dispatch: { ...DEFAULT_ENGINE.axes.dispatch, maxDepth: 1, nested: false },
|
||||
},
|
||||
};
|
||||
const result = negotiateHostCapabilities(
|
||||
{
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full' },
|
||||
},
|
||||
restrictedEngine,
|
||||
);
|
||||
assert.strictEqual(result.effective.dispatch.maxDepth, 1);
|
||||
});
|
||||
|
||||
test('(4) host omits hookBus → effective.hookBus is safe default + warning present', () => {
|
||||
const hostWithoutHookBus = { ...PROFILE_BASELINES['declarative-cli'] };
|
||||
delete hostWithoutHookBus.hookBus;
|
||||
|
||||
const result = negotiateHostCapabilities(hostWithoutHookBus);
|
||||
// effective hookBus must be a known value
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES.hookBus.includes(result.effective.hookBus),
|
||||
`effective.hookBus '${result.effective.hookBus}' is not known`,
|
||||
);
|
||||
// points.hooks must be present
|
||||
assert.ok('hooks' in result.points, 'points.hooks must be present');
|
||||
// a warning mentioning hookBus
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('hookBus'), `Expected warning about hookBus; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('(5) INVARIANT: every effective scalar ∈ engine.known[axis] for hostile hosts', () => {
|
||||
const hostileHosts = [
|
||||
// All unknown values
|
||||
{
|
||||
embeddingMode: 'future-mode',
|
||||
commandSurface: 'future-surface',
|
||||
modelMode: 'quantum',
|
||||
hookBus: 'blockchain',
|
||||
stateIO: 'cloud-magic',
|
||||
transport: 'telepathy',
|
||||
runtime: 'wasm',
|
||||
protocolVersion: 999,
|
||||
},
|
||||
// Mix of known and unknown
|
||||
{
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'palette',
|
||||
modelMode: 'active',
|
||||
hookBus: 'none',
|
||||
stateIO: 'unknown-future',
|
||||
transport: 'mcp',
|
||||
runtime: 'sandboxed-web',
|
||||
},
|
||||
// Empty host
|
||||
{},
|
||||
// Only dispatch with extreme values
|
||||
{
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: 9999, background: true, subagentToolkit: 'full' },
|
||||
},
|
||||
];
|
||||
|
||||
const scalarAxes = ['embeddingMode', 'commandSurface', 'modelMode', 'hookBus', 'stateIO', 'transport', 'runtime'];
|
||||
|
||||
for (const host of hostileHosts) {
|
||||
const result = negotiateHostCapabilities(host);
|
||||
for (const axis of scalarAxes) {
|
||||
const effectiveVal = result.effective[axis];
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES[axis].includes(effectiveVal),
|
||||
`INVARIANT VIOLATION: effective.${axis}='${effectiveVal}' is NOT in known vocabulary for host=${JSON.stringify(host)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('host protocolVersion > engine → warning mentioning protocolVersion', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
...PROFILE_BASELINES['declarative-cli'],
|
||||
protocolVersion: 99,
|
||||
});
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('protocolVersion'), `Expected protocolVersion warning; got: ${warnText}`);
|
||||
assert.strictEqual(result.protocolVersion, PROTOCOL_VERSION);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// INDEPENDENCE: mutation safety
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('independence / mutation safety', () => {
|
||||
test('mutating returned result does not affect second call', () => {
|
||||
const host = PROFILE_BASELINES['declarative-cli'];
|
||||
const r1 = negotiateHostCapabilities(host);
|
||||
// Mutate r1
|
||||
r1.warnings.push('injected');
|
||||
r1.effective.modelMode = 'active';
|
||||
r1.points.command.effectiveLevel = 'absent';
|
||||
|
||||
const r2 = negotiateHostCapabilities(host);
|
||||
// r2 must not be affected
|
||||
assert.deepStrictEqual(r2.warnings, [], 'r2.warnings must not include injected warning');
|
||||
assert.strictEqual(r2.effective.modelMode, host.modelMode, 'r2.effective.modelMode must be original value');
|
||||
assert.strictEqual(r2.points.command.effectiveLevel, 'full', 'r2.points.command.effectiveLevel must be full');
|
||||
});
|
||||
|
||||
test('all exports are present on the module', () => {
|
||||
const expectedExports = [
|
||||
'PROTOCOL_VERSION', 'HOST_INTEGRATION_AXES', 'INTERFACE_POINTS',
|
||||
'PROFILE_BASELINES', 'DEFAULT_ENGINE', 'UNDOCUMENTED',
|
||||
'degradationFor', 'profileOf', 'negotiateHostCapabilities',
|
||||
];
|
||||
for (const exp of expectedExports) {
|
||||
assert.ok(exp in hi, `Missing export: ${exp}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Decision 1: undocumented sentinel — fail-closed in negotiation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Decision 1: UNDOCUMENTED sentinel — fail-closed negotiation', () => {
|
||||
test('negotiate with embeddingMode:"undocumented" → effective is safe default (documented value), NOT "undocumented"', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['declarative-cli'],
|
||||
embeddingMode: 'undocumented',
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
// effective.embeddingMode must be a documented value, NOT 'undocumented'
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES.embeddingMode.includes(result.effective.embeddingMode),
|
||||
`effective.embeddingMode must be a documented value; got '${result.effective.embeddingMode}'`,
|
||||
);
|
||||
assert.notStrictEqual(result.effective.embeddingMode, 'undocumented',
|
||||
'effective.embeddingMode must not be "undocumented"');
|
||||
// A warning mentioning "undocumented"
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('undocumented'),
|
||||
`Expected a warning mentioning "undocumented"; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('negotiate with dispatch fields all "undocumented" → fail-closed dispatch', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: {
|
||||
namedDispatch: 'undocumented',
|
||||
nested: 'undocumented',
|
||||
maxDepth: 'undocumented',
|
||||
background: 'undocumented',
|
||||
subagentToolkit: 'undocumented',
|
||||
},
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
const d = result.effective.dispatch;
|
||||
assert.strictEqual(d.namedDispatch, false, 'namedDispatch must be false when "undocumented"');
|
||||
assert.strictEqual(d.nested, false, 'nested must be false when "undocumented"');
|
||||
assert.strictEqual(d.background, false, 'background must be false when "undocumented"');
|
||||
assert.strictEqual(d.subagentToolkit, 'read-only', 'subagentToolkit must be "read-only" when "undocumented"');
|
||||
assert.strictEqual(d.maxDepth, 0, 'maxDepth must be 0 when "undocumented"');
|
||||
// points.dispatch must be absent
|
||||
assert.strictEqual(result.points.dispatch.effectiveLevel, 'absent',
|
||||
'points.dispatch.effectiveLevel must be "absent" when dispatch is all undocumented');
|
||||
});
|
||||
|
||||
test('degradationFor dispatch with namedDispatch:"undocumented" → level "absent"', () => {
|
||||
const r = degradationFor('dispatch', {
|
||||
dispatch: {
|
||||
namedDispatch: 'undocumented',
|
||||
nested: false,
|
||||
maxDepth: 0,
|
||||
background: false,
|
||||
subagentToolkit: 'full',
|
||||
},
|
||||
});
|
||||
assert.strictEqual(r.level, 'absent',
|
||||
`degradationFor with namedDispatch:"undocumented" must return absent; got "${r.level}"`);
|
||||
});
|
||||
|
||||
test('subagentToolkit "undocumented" (truthy string) fails closed to read-only', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
dispatch: {
|
||||
namedDispatch: true,
|
||||
nested: true,
|
||||
maxDepth: -1,
|
||||
background: true,
|
||||
subagentToolkit: 'undocumented',
|
||||
},
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
assert.strictEqual(result.effective.dispatch.subagentToolkit, 'read-only',
|
||||
'subagentToolkit "undocumented" must degrade to "read-only"');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Decision 2: expanded runtime vocabulary (8 documented values)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Decision 2: expanded runtime vocabulary', () => {
|
||||
const newRuntimes = ['python', 'go', 'rust', 'electron', 'other'];
|
||||
|
||||
for (const rt of newRuntimes) {
|
||||
test(`negotiate with runtime:"${rt}" → effective.runtime === "${rt}" (no warn about unknown)`, () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
runtime: rt,
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
assert.strictEqual(result.effective.runtime, rt,
|
||||
`effective.runtime must be "${rt}"; got "${result.effective.runtime}"`);
|
||||
// Must NOT have an unknown-value warning for this runtime
|
||||
const runtimeWarnings = result.warnings.filter((w) => w.includes('runtime') && w.includes('not trusted'));
|
||||
assert.strictEqual(runtimeWarnings.length, 0,
|
||||
`Must not warn about unknown runtime "${rt}"; warnings: ${result.warnings.join(', ')}`);
|
||||
});
|
||||
}
|
||||
|
||||
test('runtime "undocumented" (sentinel) → fail-closed to safe default', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
runtime: 'undocumented',
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
// Must be a documented value, not "undocumented"
|
||||
assert.ok(
|
||||
HOST_INTEGRATION_AXES.runtime.includes(result.effective.runtime),
|
||||
`effective.runtime must be documented; got "${result.effective.runtime}"`,
|
||||
);
|
||||
assert.notStrictEqual(result.effective.runtime, 'undocumented');
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('undocumented'), `Expected undocumented warning; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('"wasm" (genuinely unknown, not sentinel) → still fails closed with "not trusted" warning', () => {
|
||||
const host = {
|
||||
...PROFILE_BASELINES['programmatic-cli'],
|
||||
runtime: 'wasm',
|
||||
};
|
||||
const result = negotiateHostCapabilities(host);
|
||||
assert.ok(HOST_INTEGRATION_AXES.runtime.includes(result.effective.runtime),
|
||||
`effective.runtime must be documented; got "${result.effective.runtime}"`);
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('not trusted') || warnText.includes('unknown'),
|
||||
`Expected not-trusted/unknown warning; got: ${warnText}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fix 1: degradationFor('dispatch') fail-closed on non-'full' subagentToolkit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Fix 1: degradationFor dispatch fails closed on non-full subagentToolkit', () => {
|
||||
const FULL_DEPTH_DISPATCH = { namedDispatch: true, nested: true, maxDepth: -1, background: true };
|
||||
|
||||
test('subagentToolkit:"full" + full depth → level "full"', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: 'full' } });
|
||||
assert.strictEqual(r.level, 'full',
|
||||
'subagentToolkit:"full" with full depth must return level "full"');
|
||||
});
|
||||
|
||||
test('subagentToolkit:"read-only" + full depth → level "degraded"', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: 'read-only' } });
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
'subagentToolkit:"read-only" must return level "degraded"');
|
||||
assert.ok(r.fallback.length > 0, 'fallback must be non-empty');
|
||||
});
|
||||
|
||||
test('subagentToolkit:"undocumented" + full depth → level "degraded" (fail-closed)', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: 'undocumented' } });
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
'subagentToolkit:"undocumented" must fail closed to level "degraded"; got "' + r.level + '"');
|
||||
assert.ok(r.fallback.length > 0, 'fallback must be non-empty');
|
||||
});
|
||||
|
||||
test('subagentToolkit:"future-xyz" + full depth → level "degraded" (fail-closed)', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: 'future-xyz' } });
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
'subagentToolkit:"future-xyz" (unknown) must fail closed to level "degraded"; got "' + r.level + '"');
|
||||
assert.ok(r.fallback.length > 0, 'fallback must be non-empty');
|
||||
});
|
||||
|
||||
test('subagentToolkit:"" (empty string) + full depth → level "degraded" (fail-closed)', () => {
|
||||
const r = degradationFor('dispatch', { dispatch: { ...FULL_DEPTH_DISPATCH, subagentToolkit: '' } });
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
'subagentToolkit:"" must fail closed to level "degraded"; got "' + r.level + '"');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// New fixes: M1 maxDepth NaN, M2 struct consistency, L1 SAFE_DEFAULTS,
|
||||
// L2 protocolVersion warn, N1 undocumented dispatch warnings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Fix M1: maxDepth NaN bypasses number guard', () => {
|
||||
test('negotiate with dispatch.maxDepth NaN → effective.dispatch.maxDepth === 0 AND warning about maxDepth AND Number.isFinite', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: NaN, background: false, subagentToolkit: 'full' },
|
||||
});
|
||||
const d = result.effective.dispatch;
|
||||
assert.strictEqual(d.maxDepth, 0, 'NaN maxDepth must be normalized to 0');
|
||||
assert.ok(Number.isFinite(d.maxDepth), 'effective.dispatch.maxDepth must be finite (Number.isFinite)');
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('maxDepth'), `Expected a warning about maxDepth; got: ${warnText}`);
|
||||
});
|
||||
|
||||
test('degradationFor dispatch with maxDepth NaN → level "degraded" (not NaN-dependent, not "full")', () => {
|
||||
const r = degradationFor('dispatch', {
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: NaN, subagentToolkit: 'full' },
|
||||
});
|
||||
// After fix: depth=(NaN not finite)→0; NaN===0 is false so initial check doesn't fire;
|
||||
// isUnbounded=false; isFullDepth = false || (nested:true && 0>=2) = false → 'degraded' (flat)
|
||||
assert.strictEqual(r.level, 'degraded',
|
||||
`NaN maxDepth with nested:true must yield 'degraded' (depth=0, not full-depth); got: ${r.level}`);
|
||||
assert.notStrictEqual(r.level, 'full', 'NaN maxDepth must NOT yield "full"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix M2: cap nested/background when namedDispatch is false', () => {
|
||||
test('negotiate with namedDispatch:"undocumented" → namedDispatch false, nested false, background false, maxDepth 0; warnings include namedDispatch undocumented note', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { namedDispatch: 'undocumented', nested: true, background: true, maxDepth: 5, subagentToolkit: 'full' },
|
||||
});
|
||||
const d = result.effective.dispatch;
|
||||
assert.strictEqual(d.namedDispatch, false, 'namedDispatch must be false');
|
||||
assert.strictEqual(d.nested, false, 'nested must be false when namedDispatch is false');
|
||||
assert.strictEqual(d.background, false, 'background must be false when namedDispatch is false');
|
||||
assert.strictEqual(d.maxDepth, 0, 'maxDepth must be 0 when namedDispatch is false');
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('namedDispatch') || warnText.includes('dispatch.namedDispatch'),
|
||||
`Expected a warning about namedDispatch being undocumented; got: ${warnText}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix L1: SAFE_DEFAULTS.dispatch.subagentToolkit is read-only', () => {
|
||||
// CONTRACT: negotiate({}) uses SAFE_DEFAULTS for each axis; dispatch uses its floor
|
||||
test('negotiate({}) → effective axes match documented SAFE_DEFAULTS (CONTRACT)', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
const eff = result.effective;
|
||||
assert.strictEqual(eff.embeddingMode, 'declarative');
|
||||
assert.strictEqual(eff.commandSurface, 'prose-only');
|
||||
assert.strictEqual(eff.modelMode, 'passive');
|
||||
assert.strictEqual(eff.hookBus, 'none');
|
||||
assert.strictEqual(eff.stateIO, 'session-log-append');
|
||||
assert.strictEqual(eff.transport, 'mcp');
|
||||
assert.strictEqual(eff.runtime, 'node');
|
||||
assert.strictEqual(eff.dispatch.subagentToolkit, 'read-only',
|
||||
'SAFE_DEFAULTS dispatch floor must be read-only');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix L2: warn on present-but-non-number protocolVersion', () => {
|
||||
test('negotiate with protocolVersion:"beta" → warnings include protocolVersion note; result.protocolVersion === engine default (1)', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'slash-file',
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
dispatch: { namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full' },
|
||||
protocolVersion: 'beta',
|
||||
});
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('protocolVersion'),
|
||||
`Expected a warning about protocolVersion being non-finite/non-number; got: ${warnText}`);
|
||||
assert.strictEqual(result.protocolVersion, 1,
|
||||
'result.protocolVersion must fall back to engine default (1)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix N1: symmetric observability warnings for undocumented dispatch fields', () => {
|
||||
test('dispatch.subagentToolkit:"undocumented" → warning includes "dispatch.subagentToolkit is undocumented"', () => {
|
||||
const result = negotiateHostCapabilities({
|
||||
dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'undocumented' },
|
||||
});
|
||||
const warnText = result.warnings.join(' ');
|
||||
assert.ok(warnText.includes('subagentToolkit') && warnText.includes('undocumented'),
|
||||
`Expected warning about dispatch.subagentToolkit undocumented; got: ${warnText}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix: degradationFor unknown point → {level:"absent", unknown:true}', () => {
|
||||
test('degradationFor("totally-unknown-point", {}) → {level:"absent", unknown:true}', () => {
|
||||
const r = degradationFor('totally-unknown-point', {});
|
||||
assert.strictEqual(r.level, 'absent', 'unknown point must return absent');
|
||||
assert.strictEqual(r.unknown, true, 'unknown point must have unknown:true');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fix 2: negotiateHostCapabilities — host omitting 'dispatch' → subagentToolkit 'read-only'
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Fix 2: negotiate — host omits dispatch → subagentToolkit read-only (fail-closed)', () => {
|
||||
test('negotiateHostCapabilities({}) → effective.dispatch.subagentToolkit === "read-only"', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.strictEqual(result.effective.dispatch.subagentToolkit, 'read-only',
|
||||
'When host omits dispatch, subagentToolkit must fail-closed to "read-only"; got "' + result.effective.dispatch.subagentToolkit + '"');
|
||||
});
|
||||
|
||||
test('negotiateHostCapabilities({}) → effective.dispatch.namedDispatch===false, maxDepth===0, nested===false, background===false', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
const d = result.effective.dispatch;
|
||||
assert.strictEqual(d.namedDispatch, false);
|
||||
assert.strictEqual(d.maxDepth, 0);
|
||||
assert.strictEqual(d.nested, false);
|
||||
assert.strictEqual(d.background, false);
|
||||
});
|
||||
|
||||
test('negotiateHostCapabilities({}) → points.dispatch.effectiveLevel === "absent"', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.strictEqual(result.points.dispatch.effectiveLevel, 'absent',
|
||||
'dispatch absent when host omits it');
|
||||
});
|
||||
|
||||
test('host with all axes but no dispatch → subagentToolkit "read-only"', () => {
|
||||
const hostWithoutDispatch = {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
modelMode: 'passive',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
// no dispatch key
|
||||
};
|
||||
const result = negotiateHostCapabilities(hostWithoutDispatch);
|
||||
assert.strictEqual(result.effective.dispatch.subagentToolkit, 'read-only',
|
||||
'Host missing dispatch must produce subagentToolkit "read-only"; got "' + result.effective.dispatch.subagentToolkit + '"');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user