Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
2309 lines
104 KiB
JavaScript
2309 lines
104 KiB
JavaScript
// docs-guard-exempt: record.docs is a metadata field string comparison, not a real docs/ file read.
|
||
'use strict';
|
||
process.env.MSD_TEST_MODE = '1';
|
||
|
||
/**
|
||
* reviewer-manifest-body.test.cjs — behavioral tests for the reviewer lane body
|
||
* (ADR-2782, chore #2795 Phase 2): `validateReviewerBody`, `collectReviewerWarnings` / `collectReviewerWarningRecords`,
|
||
* the `role:'reviewer'` dispatch branch of `validateCapability`, the reviewer-lane
|
||
* uniqueness rules inside `validateCrossCapability`, and the harvest widening in
|
||
* `buildRegistry` / `loadAndValidate`.
|
||
*
|
||
* Implements every row in `.msd/phase/chore-2795-reviewer-manifest-body/50-test-matrix.md`
|
||
* that carries a Test name (sections A–J). Test names are copied verbatim from the
|
||
* matrix. See `.msd/phase/chore-2795-reviewer-manifest-body/40-design.md` for the
|
||
* behavior table the matrix derives from.
|
||
*
|
||
* Level choice: rows describing the SHAPE of the `reviewer` body in isolation
|
||
* (A1–A3, A7–A11, and all of B–F) call `validateReviewerBody` directly — the
|
||
* cheapest unit that proves the behavior, per the matrix's own "Units and level"
|
||
* table. Rows describing ROLE-CONDITIONED admissibility of the body (A4–A6,
|
||
* A12–A15) call `validateCapability(cap, folderId)`, since that dispatch only
|
||
* exists there. Section G calls `validateCrossCapability(Map, Set)` — the real
|
||
* caller shape, never a plain object. Section H splits across `buildRegistry`
|
||
* (the harvest itself) and `validateCrossCapability` (the config-key ownership
|
||
* loop it also widened) per where each behavior actually lives in the source.
|
||
*/
|
||
|
||
const { test, describe } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const os = require('node:os');
|
||
const path = require('node:path');
|
||
const fc = require('fast-check');
|
||
|
||
const { cleanup } = require('./helpers.cjs');
|
||
const { HTTP_REACHABLE_PROBE_TIMEOUT_FIXTURE_MS } = require('./helpers/timeouts.cjs');
|
||
|
||
/** NOT a subprocess spawn timeout. Fixture DATA: the default valid `timeoutMs` for a command-capability-kind probe. */
|
||
const COMMAND_CAPABILITY_PROBE_TIMEOUT_FIXTURE_MS = 3000;
|
||
/** NOT a subprocess spawn timeout. Fixture DATA: manifest probe.timeoutMs boundary set (this validator's own -1/0/1 triple is unrelated to, and coincidentally overlaps with, tests/plan-phase-stall-detection.test.cjs's runBashScript seam-level boundary triple -- disclosed, not merged; different validator, different file). */
|
||
const MANIFEST_PROBE_TIMEOUT_BOUNDARY_ZERO_MS = 0;
|
||
const MANIFEST_PROBE_TIMEOUT_BOUNDARY_ONE_MS = 1;
|
||
const MANIFEST_PROBE_TIMEOUT_BOUNDARY_NEGATIVE_MS = -1;
|
||
const MANIFEST_PROBE_TIMEOUT_BOUNDARY_NON_INTEGER_MS = 1.5;
|
||
/** NOT a subprocess spawn timeout. Fixture DATA: an arbitrary value proving a command-exists-kind probe rejects ANY timeoutMs field -- the specific number is not itself under test. */
|
||
const COMMAND_EXISTS_FORBIDDEN_TIMEOUT_MS = 500;
|
||
|
||
const {
|
||
LANE_SLUG_RE,
|
||
validateReviewerBody,
|
||
collectReviewerWarnings,
|
||
collectReviewerWarningRecords,
|
||
REVIEWER_WARNING,
|
||
REMOVED_REVIEWER_CLI_FIELD,
|
||
validateCapability,
|
||
validateCrossCapability,
|
||
VALID_LANE_EFFORT_CHANNELS,
|
||
VALID_MODEL_DISCOVERY,
|
||
VALID_EMPTY_OUTPUT,
|
||
VALID_EVIDENCE_CLASSES,
|
||
VALID_LANE_HANDLERS,
|
||
KNOWN_REVIEWER_FIELDS,
|
||
KNOWN_HOST_BEHAVIORS,
|
||
MAX_REPORTED_UNKNOWN_KEYS,
|
||
MAX_REPORTED_KEY_CHARS,
|
||
} = require('../msd-core/bin/lib/capability-validator.cjs');
|
||
|
||
const { loadAndValidate, buildRegistry } = require('../scripts/gen-capability-registry.cjs');
|
||
|
||
// ─── Fixture builders ──────────────────────────────────────────────────────
|
||
// House convention (tests/gen-registry.test.cjs): builder functions return a
|
||
// VALID fixture, which each test then mutates. Every call returns a FRESH
|
||
// object — no module-level mutable shared state, no execution-order dependence.
|
||
|
||
/** A valid `spawn`-transport reviewer lane body. */
|
||
function validLane() {
|
||
return {
|
||
slug: 'my-lane',
|
||
flags: ['--my-lane'],
|
||
transport: 'spawn',
|
||
probe: { kind: 'command-exists', binary: 'my-lane' },
|
||
invoke: {
|
||
binary: 'my-lane',
|
||
args: [],
|
||
promptChannel: 'stdin',
|
||
outputChannel: 'stdout',
|
||
modelArg: null,
|
||
effortChannel: 'none',
|
||
},
|
||
timeoutFloorMs: 5000,
|
||
emptyOutput: 'stub-with-stderr',
|
||
reviewsSection: 'My Lane',
|
||
evidenceClass: 'source-grounded',
|
||
requiresBinaries: ['my-lane'],
|
||
promptBudgetKey: null,
|
||
handler: null,
|
||
};
|
||
}
|
||
|
||
/** A valid `openai-http`-transport reviewer lane body. */
|
||
function validHttpLane() {
|
||
return {
|
||
slug: 'lm-studio-http',
|
||
flags: ['--lm-studio'],
|
||
transport: 'openai-http',
|
||
probe: {
|
||
kind: 'http-reachable',
|
||
hostConfigKey: 'lmStudio.baseUrl',
|
||
path: '/v1/models',
|
||
timeoutMs: HTTP_REACHABLE_PROBE_TIMEOUT_FIXTURE_MS,
|
||
},
|
||
invoke: {
|
||
hostConfigKey: 'lmStudio.baseUrl',
|
||
path: '/v1/chat/completions',
|
||
modelDiscovery: 'none',
|
||
effortChannel: 'none',
|
||
},
|
||
timeoutFloorMs: 5000,
|
||
emptyOutput: 'stub-with-stderr',
|
||
reviewsSection: 'LM Studio',
|
||
evidenceClass: 'source-grounded',
|
||
requiresBinaries: [],
|
||
promptBudgetKey: null,
|
||
handler: null,
|
||
};
|
||
}
|
||
|
||
/** A fresh, well-formed spawn lane, mutated in place by `mutator` before return. */
|
||
function laneOverride(mutator) {
|
||
const lane = validLane();
|
||
mutator(lane);
|
||
return lane;
|
||
}
|
||
|
||
/** A fresh, well-formed http lane, mutated in place by `mutator` before return. */
|
||
function httpOverride(mutator) {
|
||
const lane = validHttpLane();
|
||
mutator(lane);
|
||
return lane;
|
||
}
|
||
|
||
/**
|
||
* A valid `role:'reviewer'` capability envelope (id/title/description/tier/
|
||
* requires/version all satisfy `validateCapability`'s common envelope) carrying
|
||
* a well-formed lane body. `overrides` is shallow-merged last, so passing
|
||
* `{ reviewer: undefined }` represents "no reviewer key" (property present,
|
||
* value undefined — behaviorally identical to an absent key for every check
|
||
* in this file, and it is what `JSON.stringify` drops when a fixture is
|
||
* written to disk in section I).
|
||
*/
|
||
function capWith(overrides) {
|
||
return Object.assign(
|
||
{
|
||
id: 'test-cap',
|
||
role: 'reviewer',
|
||
title: 'Test Capability',
|
||
description: 'A test capability for the reviewer manifest body test suite.',
|
||
tier: 'standard',
|
||
requires: [],
|
||
version: '1.0.0',
|
||
reviewer: validLane(),
|
||
},
|
||
overrides || {},
|
||
);
|
||
}
|
||
|
||
// ─── A. Body presence / shape ──────────────────────────────────────────────
|
||
|
||
describe('A. Body presence / shape', () => {
|
||
test('runtimeCapWithoutReviewerBodyIsValidAndNotALane', () => {
|
||
const errs = validateReviewerBody({ id: 'x', role: 'runtime' });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('featureCapWithoutReviewerBodyIsValid', () => {
|
||
const errs = validateReviewerBody({ id: 'x', role: 'feature' });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('runtimeCapMayCarryReviewerBody', () => {
|
||
const errs = validateReviewerBody({ id: 'x', role: 'runtime', reviewer: validLane() });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('reviewerRoleWithLaneBodyIsValid', () => {
|
||
const cap = capWith();
|
||
const errs = validateCapability(cap, cap.id);
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('reviewerRoleWithoutLaneBodyIsRejected', () => {
|
||
const cap = capWith({ reviewer: undefined });
|
||
const errs = validateCapability(cap, cap.id);
|
||
assert.ok(
|
||
errs.some((e) => e.includes('role:reviewer capability must have a "reviewer" body')),
|
||
`expected the lane-ness assertion error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('featureRoleRejectsReviewerBody', () => {
|
||
const cap = capWith({ role: 'feature', reviewer: validLane() });
|
||
const errs = validateCapability(cap, cap.id);
|
||
assert.ok(
|
||
errs.some((e) => e.includes('role:feature capability must not have a "reviewer" body')),
|
||
`expected the feature-forbids-reviewer-body error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
// A7/A1 are the highest-consequence pair: `typeof null === 'object'`, so an
|
||
// explicit `null` must NOT be read as "absent" (A1), yet must still error (A7).
|
||
test('reviewerNullIsRejectedNotTreatedAsAbsent', () => {
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: null });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer must be an object (got: null)')),
|
||
`expected a null-is-not-absent error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('reviewerBooleanIsRejected', () => {
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: true });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer must be an object (got: boolean)')),
|
||
`expected a must-be-an-object error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('reviewerZeroIsRejected', () => {
|
||
// Falsy-but-present: 0 must not be misread as absent (only `undefined` is).
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: 0 });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer must be an object (got: number)')),
|
||
`expected a must-be-an-object error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('reviewerStringIsRejected', () => {
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: 'spawn' });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer must be an object (got: string)')),
|
||
`expected a must-be-an-object error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('reviewerArrayIsRejected', () => {
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: [] });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer must be an object (got: array)')),
|
||
`expected an array-is-not-a-body error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('emptyReviewerBodyReportsAllMissingFields', () => {
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: {} });
|
||
const requiredFieldMarkers = [
|
||
'reviewer.slug',
|
||
'reviewer.flags',
|
||
'reviewer.transport',
|
||
'reviewer.probe',
|
||
'reviewer.invoke',
|
||
'reviewer.timeoutFloorMs',
|
||
'reviewer.emptyOutput',
|
||
'reviewer.reviewsSection',
|
||
'reviewer.evidenceClass',
|
||
'reviewer.requiresBinaries',
|
||
'reviewer.promptBudgetKey',
|
||
'reviewer.handler',
|
||
];
|
||
for (const marker of requiredFieldMarkers) {
|
||
assert.ok(
|
||
errs.some((e) => e.includes(marker)),
|
||
`expected an error mentioning ${marker}, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
assert.equal(
|
||
errs.length,
|
||
requiredFieldMarkers.length,
|
||
`expected exactly one error per required field (not just the first), got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('unknownFieldInsideReviewerBodyWarnsButValidates', () => {
|
||
const lane = validLane();
|
||
lane.futureField = 'from-a-newer-msd';
|
||
const cap = { id: 'cap-x', reviewer: lane };
|
||
|
||
const errs = validateReviewerBody(cap);
|
||
assert.deepEqual(errs, [], `unknown field must not be a validation error, got: ${JSON.stringify(errs)}`);
|
||
|
||
// Asserted on the typed IR, not the rendered prose (CONTRIBUTING.md,
|
||
// "Prohibited: Raw Text Matching on Test Outputs"). The `message` field
|
||
// exists for operator console output only.
|
||
const records = collectReviewerWarningRecords(cap);
|
||
assert.equal(records.length, 1, `expected exactly one record, got: ${JSON.stringify(records)}`);
|
||
assert.equal(records[0].code, REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD);
|
||
assert.equal(records[0].capId, 'cap-x');
|
||
assert.equal(records[0].field, 'reviewer.futureField');
|
||
assert.deepEqual(records[0].knownFields, [...KNOWN_REVIEWER_FIELDS]);
|
||
|
||
// The renderer still produces one string per record for the two production
|
||
// consumers (gen-capability-registry -> stderr, capability-loader -> OverlayMeta.warnings).
|
||
assert.equal(collectReviewerWarnings(cap).length, records.length);
|
||
});
|
||
|
||
test('unknownRoleIsRejectedWithEnumeratedMembers', () => {
|
||
const cap = capWith({ role: 'wat' });
|
||
const errs = validateCapability(cap, cap.id);
|
||
assert.ok(
|
||
errs.some((e) => e.includes('role must be one of: feature, runtime, reviewer')),
|
||
`expected the role error to enumerate all three members, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('reviewerRoleDoesNotRequireRuntimeCompat', () => {
|
||
const cap = capWith();
|
||
assert.equal(cap.runtimeCompat, undefined, 'fixture must not declare runtimeCompat');
|
||
const errs = validateCapability(cap, cap.id);
|
||
assert.deepEqual(errs, [], `expected no errors (runtimeCompat is a runtime-only concern), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('reviewerRoleRejectsRuntimeBody', () => {
|
||
const cap = capWith({ runtime: { configFormat: 'toml' } });
|
||
const errs = validateCapability(cap, cap.id);
|
||
assert.ok(
|
||
errs.some((e) => e.includes('role:reviewer capability must not have a "runtime" body')),
|
||
`expected a no-runtime-body error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('reviewerRoleRejectsFeatureOnlyFields', () => {
|
||
const cap = capWith({ skills: ['s'], agents: ['a'], steps: [{ point: 'plan:pre' }] });
|
||
const errs = validateCapability(cap, cap.id);
|
||
for (const field of ['skills', 'agents', 'steps']) {
|
||
assert.ok(
|
||
errs.some((e) => e.includes(`role:reviewer capability must not have "${field}"`)),
|
||
`expected a feature-only-field error for "${field}", got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ─── B. transport discriminator (D2) ───────────────────────────────────────
|
||
|
||
describe('B. transport discriminator (D2)', () => {
|
||
test('transportIsRequiredAndNeverInferred', () => {
|
||
const lane = laneOverride((l) => { delete l.transport; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.transport must be one of: spawn, openai-http')),
|
||
`expected a transport-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('spawnTransportAcceptsSpawnInvoke', () => {
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: validLane() });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('httpTransportAcceptsHttpInvoke', () => {
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: validHttpLane() });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('spawnTransportRejectsHttpOnlyInvokeFields', () => {
|
||
const lane = laneOverride((l) => { l.invoke.hostConfigKey = 'x.y'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.hostConfigKey is not permitted for transport "spawn"')),
|
||
`expected a forbidden-field error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('httpTransportRejectsSpawnOnlyInvokeFields', () => {
|
||
const lane = httpOverride((l) => { l.invoke.binary = 'lm-studio'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.binary is not permitted for transport "openai-http"')),
|
||
`expected a forbidden-field error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('invokeWithBothSubShapesIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.invoke.hostConfigKey = 'x.y'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke mixes spawn-only fields') && e.includes('openai-http-only fields')),
|
||
`expected D2's exact both-subshapes error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('invokeWithNeitherSubShapeIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.invoke = {}; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.binary must be a non-empty string for transport "spawn"')),
|
||
`expected the spawn sub-shape to be validated against an empty invoke, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
assert.ok(
|
||
!errs.some((e) => e.includes('mixes spawn-only fields')),
|
||
`neither-subshape must not ALSO report the mixed-subshape error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('unknownTransportEnumeratesValidMembers', () => {
|
||
const lane = laneOverride((l) => { l.transport = 'grpc'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.transport must be one of: spawn, openai-http') && e.includes('"grpc"')),
|
||
`expected an unknown-transport error enumerating valid members, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('invokeIsRequired', () => {
|
||
const lane = laneOverride((l) => { delete l.invoke; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke must be an object')),
|
||
`expected an invoke-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('invokeNullIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.invoke = null; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke must be an object')),
|
||
`expected an invoke-must-be-object error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('invokeArrayIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.invoke = []; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke must be an object')),
|
||
`expected an invoke-must-be-object error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('reservedNameAsTransportIsRejected', () => {
|
||
// A reserved JS name is rejected by closed-enum membership itself — a
|
||
// VALID_* set never contains __proto__/constructor/prototype, so no separate
|
||
// reserved-name branch is needed here and the enum error is the more useful
|
||
// one because it names the valid members. The inline literal guards stay
|
||
// where they do real work: the key-derived write sites.
|
||
for (const reserved of ['__proto__', 'constructor', 'prototype']) {
|
||
const lane = laneOverride((l) => { l.transport = reserved; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.transport must be one of: spawn, openai-http')),
|
||
`expected ${reserved} to be rejected by enum membership, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ─── C. spawn invoke fields ─────────────────────────────────────────────────
|
||
|
||
describe('C. spawn invoke fields', () => {
|
||
test('spawnBinaryIsRequired', () => {
|
||
const lane = laneOverride((l) => { delete l.invoke.binary; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.binary must be a non-empty string for transport "spawn"')),
|
||
`expected a binary-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('spawnEmptyBinaryIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.invoke.binary = ''; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.binary must be a non-empty string for transport "spawn"')),
|
||
`expected an empty-binary error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('spawnArgsArrayIsRequired', () => {
|
||
const lane = laneOverride((l) => { delete l.invoke.args; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.args must be an array')),
|
||
`expected an args-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('spawnEmptyArgsArrayIsValid', () => {
|
||
const lane = laneOverride((l) => { l.invoke.args = []; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors (a lane may take no args), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('spawnArgsRejectsNonStringElement', () => {
|
||
const lane = laneOverride((l) => { l.invoke.args = ['-p', 7]; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.args entry 7 must be a string')),
|
||
`expected a non-string-element error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('promptChannelStdinIsValid', () => {
|
||
const lane = laneOverride((l) => { l.invoke.promptChannel = 'stdin'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('promptChannelNoneIsValidPerAmendment', () => {
|
||
const lane = laneOverride((l) => { l.invoke.promptChannel = 'none'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors (A1 amendment), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('promptChannelArgvFileRefIsValid', () => {
|
||
const lane = laneOverride((l) => { l.invoke.promptChannel = 'argv-file-ref'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
// Zero shipped lanes use promptChannel:'argv' — this row is its only coverage
|
||
// until Phase 5b ships one (40-design.md C6 / Known Limits).
|
||
test('promptChannelArgvIsValidDespiteNoShippedLane', () => {
|
||
const lane = laneOverride((l) => { l.invoke.promptChannel = 'argv'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('outputChannelStdoutIsValid', () => {
|
||
const lane = laneOverride((l) => { l.invoke.outputChannel = 'stdout'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('fileArgOutputWithOutputArgIsValid', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.invoke.outputChannel = 'file-arg';
|
||
l.invoke.outputArg = 'out.txt';
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors (A2/A3 amendment), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('fileArgOutputWithoutOutputArgIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.invoke.outputChannel = 'file-arg'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.outputArg is required') && e.includes('"file-arg"')),
|
||
`expected a required-iff error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('stdoutOutputWithOutputArgIsRejected', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.invoke.outputChannel = 'stdout';
|
||
l.invoke.outputArg = 'out.txt';
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.outputArg is only permitted when outputChannel is "file-arg"')),
|
||
`expected a forbidden-field error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('modelArgNullMeansNoOverride', () => {
|
||
const lane = laneOverride((l) => { l.invoke.modelArg = null; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('modelArgEmptyStringIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.invoke.modelArg = ''; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.modelArg must be a non-empty string or null')),
|
||
`expected an empty-string-is-not-none error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('effortChannelAcceptsAllThreeMembers', () => {
|
||
for (const effortChannel of VALID_LANE_EFFORT_CHANNELS) {
|
||
const lane = laneOverride((l) => { l.invoke.effortChannel = effortChannel; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `effortChannel=${effortChannel} expected no errors, got: ${JSON.stringify(errs)}`);
|
||
}
|
||
});
|
||
|
||
// `invoke.env` (#2483). OPTIONAL, unlike every sibling above — absent is the common case, so the
|
||
// absent and present-and-valid rows are both real behavior rather than padding.
|
||
// NOTE: `env`'s optionality has no test of its own, deliberately. The env-less state is already
|
||
// validated by spawnTransportAcceptsSpawnInvoke above (validLane() declares no `env`) and the
|
||
// env-bearing state by envAcceptsStringPairs below, so a dedicated optionality test asserts no
|
||
// behavior neither of those reaches — it is organization, not coverage.
|
||
test('envAcceptsStringPairs', () => {
|
||
const lane = laneOverride((l) => { l.invoke.env = { A_VAR: '1', _B2: '' }; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('envRejectsNonObjectShapes', () => {
|
||
for (const bad of [['A=1'], 'A=1', 42, null, true]) {
|
||
const lane = laneOverride((l) => { l.invoke.env = bad; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.env must be an object of environment name/value pairs')),
|
||
`env=${JSON.stringify(bad)} expected a shape error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('envRejectsNonStringValues', () => {
|
||
for (const bad of [1, null, { nested: true }, ['x']]) {
|
||
const lane = laneOverride((l) => { l.invoke.env = { FOO: bad }; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.env.FOO must be a string')),
|
||
`value=${JSON.stringify(bad)} expected a value-type error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
// Named for what it actually proves: rejection by a portable-name POLICY, not by impossibility.
|
||
// Measured — of the names below only NUL is rejected by spawnSync; `=`, a leading digit, a dash and
|
||
// a space are all carried to the child (`{'A=B':'v'}` arrives as the entry `A=B=v`). An earlier
|
||
// name and comment asserted these could not be expressed at all; that was wrong twice over.
|
||
test('envRejectsKeysOutsideThePortableNameGrammar', () => {
|
||
for (const bad of ['', 'A=B', '2LEADING_DIGIT', 'has space', 'has-dash']) {
|
||
const lane = laneOverride((l) => { l.invoke.env = { [bad]: '1' }; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('is not a valid environment variable name')),
|
||
`key=${JSON.stringify(bad)} expected a key-grammar error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
// Built with JSON.parse deliberately: in an object LITERAL `__proto__` is special-cased and creates
|
||
// no own key at all, so a literal-built fixture would assert nothing. A manifest is JSON, where it
|
||
// IS an own key — it passes the grammar above, then vanishes when assigned onto the resolver's
|
||
// accumulator (the inherited setter consumes the assignment; for a string value it is a no-op and
|
||
// does not even change the prototype). Declared-but-never-delivered is what this rejection catches.
|
||
test('envRejectsProtoKeyThatWouldSilentlyVanish', () => {
|
||
const lane = laneOverride((l) => { l.invoke.env = JSON.parse('{"__proto__":"1"}'); });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.env key "__proto__" is not permitted')),
|
||
`expected a reserved-key error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
// Defence in depth, and the test says so: the BOUNDARY is install-time consent (capability-trust
|
||
// discloses every declared pair and binds it to the signature), so this list being incomplete is a
|
||
// known property rather than a gap. What it buys is that the highest-confidence, lowest-legitimacy
|
||
// routes cannot be taken quietly. `PATH` is included deliberately — it is the most complete
|
||
// primitive of the set and no shipped reviewer manifest declares it (asserted separately below).
|
||
test('envRejectsExecutionPrimitiveNames', () => {
|
||
for (const bad of ['PATH', 'NODE_OPTIONS', 'LD_PRELOAD', 'DYLD_INSERT_LIBRARIES', 'BASH_ENV',
|
||
'PYTHONPATH', 'PERL5OPT', 'RUBYOPT', 'GIT_SSH_COMMAND', 'JAVA_TOOL_OPTIONS']) {
|
||
const lane = laneOverride((l) => { l.invoke.env = { [bad]: '/tmp/evil' }; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes(`reviewer.invoke.env key "${bad}" is not permitted`)),
|
||
`key=${bad} expected an execution-primitive rejection, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
// Case folding is not pedantry: Windows environment lookup is case-insensitive, so `Path` reaches
|
||
// the child as `PATH`. An exact-case set is bypassed by changing one letter, which makes it worse
|
||
// than no list — it reads as a control while passing the exact input it names.
|
||
test('envDenylistIsCaseInsensitive', () => {
|
||
for (const bad of ['Path', 'path', 'node_options', 'Node_Options', 'Ld_Preload', 'bash_env']) {
|
||
const lane = laneOverride((l) => { l.invoke.env = { [bad]: '/tmp/evil' }; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('is not permitted (it makes the spawned reviewer')),
|
||
`key=${bad} must be denied regardless of case, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
// The rejection above must not break a lane that ships today. If this ever fails, the denylist has
|
||
// outgrown its evidence and the entry that broke it needs a decision, not a silent removal.
|
||
test('noShippedReviewerDeclaresADeniedEnvKey', () => {
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
const capsDir = path.join(__dirname, '..', 'capabilities');
|
||
const offenders = [];
|
||
for (const d of fs.readdirSync(capsDir)) {
|
||
const f = path.join(capsDir, d, 'capability.json');
|
||
if (!fs.existsSync(f)) continue;
|
||
const m = JSON.parse(fs.readFileSync(f, 'utf8'));
|
||
const env = m.reviewer && m.reviewer.invoke && m.reviewer.invoke.env;
|
||
if (!env) continue;
|
||
const errs = validateReviewerBody({ id: m.id || d, reviewer: m.reviewer });
|
||
const denied = errs.filter((e) => e.includes('is not permitted (it makes the spawned reviewer'));
|
||
if (denied.length) offenders.push(`${d}: ${denied.join('; ')}`);
|
||
}
|
||
assert.deepStrictEqual(offenders, [], 'a shipped reviewer capability declares a denied env key');
|
||
});
|
||
|
||
test('httpTransportRejectsEnv', () => {
|
||
const lane = httpOverride((l) => { l.invoke.env = { SNEAK: '1' }; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.env is not permitted for transport "openai-http"')),
|
||
`expected a forbidden-field error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── D. openai-http invoke fields ──────────────────────────────────────────
|
||
|
||
describe('D. openai-http invoke fields', () => {
|
||
test('httpHostConfigKeyIsRequired', () => {
|
||
const lane = httpOverride((l) => { delete l.invoke.hostConfigKey; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.hostConfigKey must be a non-empty dotted config key')),
|
||
`expected a hostConfigKey-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('httpEmptyHostConfigKeyIsRejected', () => {
|
||
const lane = httpOverride((l) => { l.invoke.hostConfigKey = ''; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.hostConfigKey must be a non-empty dotted config key')),
|
||
`expected an empty-hostConfigKey error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('httpPathIsRequiredAndNonEmpty', () => {
|
||
for (const badPath of [undefined, '']) {
|
||
const lane = httpOverride((l) => {
|
||
if (badPath === undefined) delete l.invoke.path;
|
||
else l.invoke.path = badPath;
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.path must be a non-empty string for transport "openai-http"')),
|
||
`path=${JSON.stringify(badPath)}: expected a path-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('modelDiscoveryAcceptsBothMembers', () => {
|
||
for (const modelDiscovery of VALID_MODEL_DISCOVERY) {
|
||
const lane = httpOverride((l) => { l.invoke.modelDiscovery = modelDiscovery; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `modelDiscovery=${modelDiscovery} expected no errors, got: ${JSON.stringify(errs)}`);
|
||
}
|
||
});
|
||
|
||
test('httpTransportRejectsNonNoneEffortChannel', () => {
|
||
const lane = httpOverride((l) => { l.invoke.effortChannel = 'argv'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.invoke.effortChannel must be "none" for transport "openai-http"')),
|
||
`expected an effortChannel-fixed-to-none error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── E. probe (D7) — bounded-probe control ─────────────────────────────────
|
||
|
||
describe('E. probe (D7) — bounded-probe control', () => {
|
||
test('probeIsRequiredObject', () => {
|
||
const laneAbsent = laneOverride((l) => { delete l.probe; });
|
||
const errsAbsent = validateReviewerBody({ id: 'x', reviewer: laneAbsent });
|
||
assert.ok(
|
||
errsAbsent.some((e) => e.includes('reviewer.probe must be an object with a "kind" from:')),
|
||
`absent probe: expected a probe-required error, got: ${JSON.stringify(errsAbsent)}`,
|
||
);
|
||
|
||
const laneNonObject = laneOverride((l) => { l.probe = 'not-an-object'; });
|
||
const errsNonObject = validateReviewerBody({ id: 'x', reviewer: laneNonObject });
|
||
assert.ok(
|
||
errsNonObject.some((e) => e.includes('reviewer.probe must be an object with a "kind" from:')),
|
||
`non-object probe: expected a probe-required error, got: ${JSON.stringify(errsNonObject)}`,
|
||
);
|
||
});
|
||
|
||
test('commandExistsProbeIsValid', () => {
|
||
const lane = laneOverride((l) => { l.probe = { kind: 'command-exists', binary: 'my-lane' }; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
// Zero shipped lanes use probe.kind:'command-capability' — this row is its
|
||
// only coverage until Phase 5b ships one.
|
||
test('commandCapabilityProbeIsValidDespiteNoShippedLane', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'command-capability', binary: 'my-lane', needle: 'v1.2', timeoutMs: COMMAND_CAPABILITY_PROBE_TIMEOUT_FIXTURE_MS };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('httpReachableProbeIsValid', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'http-reachable', hostConfigKey: 'lmStudio.baseUrl', path: '/v1/models', timeoutMs: HTTP_REACHABLE_PROBE_TIMEOUT_FIXTURE_MS };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('commandCapabilityProbeRequiresTimeout', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'command-capability', binary: 'my-lane', needle: 'v1.2' };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.timeoutMs must be a positive integer of milliseconds for kind "command-capability"')),
|
||
`expected a timeout-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('httpReachableProbeRequiresTimeout', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'http-reachable', hostConfigKey: 'lmStudio.baseUrl', path: '/v1/models' };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.timeoutMs must be a positive integer of milliseconds for kind "http-reachable"')),
|
||
`expected a timeout-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('probeTimeoutZeroIsRejected', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'http-reachable', hostConfigKey: 'x.y', path: '/z', timeoutMs: MANIFEST_PROBE_TIMEOUT_BOUNDARY_ZERO_MS };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.timeoutMs must be a positive integer of milliseconds') && e.includes('(got: 0)')),
|
||
`expected a boundary (limit-1) rejection, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('probeTimeoutOneIsAccepted', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'http-reachable', hostConfigKey: 'x.y', path: '/z', timeoutMs: MANIFEST_PROBE_TIMEOUT_BOUNDARY_ONE_MS };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors (boundary: limit), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('probeNegativeTimeoutIsRejected', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'http-reachable', hostConfigKey: 'x.y', path: '/z', timeoutMs: MANIFEST_PROBE_TIMEOUT_BOUNDARY_NEGATIVE_MS };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.timeoutMs must be a positive integer of milliseconds') && e.includes('(got: -1)')),
|
||
`expected a negative-timeout rejection, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('probeFractionalTimeoutIsRejected', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'http-reachable', hostConfigKey: 'x.y', path: '/z', timeoutMs: MANIFEST_PROBE_TIMEOUT_BOUNDARY_NON_INTEGER_MS };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.timeoutMs must be a positive integer of milliseconds') && e.includes('(got: 1.5)')),
|
||
`expected a fractional-timeout rejection (integer only), got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('probeNumericStringTimeoutIsRejected', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'http-reachable', hostConfigKey: 'x.y', path: '/z', timeoutMs: '900' };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.timeoutMs must be a positive integer of milliseconds') && e.includes('(got: "900")')),
|
||
`expected a numeric-string rejection, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('probeNaNTimeoutIsRejected', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'http-reachable', hostConfigKey: 'x.y', path: '/z', timeoutMs: NaN };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.timeoutMs must be a positive integer of milliseconds')),
|
||
`expected a NaN rejection, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('probeInfiniteTimeoutIsRejected', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'http-reachable', hostConfigKey: 'x.y', path: '/z', timeoutMs: Infinity };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
// Note: JSON.stringify(Infinity) renders as "null" in the message's "(got: …)"
|
||
// suffix — that's a property of JSON.stringify, not of this assertion; the
|
||
// load-bearing check is that Infinity (literally unbounded, the exact defect
|
||
// D7 exists to prevent) is rejected by the same branch as every other
|
||
// non-positive-integer value.
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.timeoutMs must be a positive integer of milliseconds for kind "http-reachable"')),
|
||
`expected an unbounded-timeout rejection, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('commandExistsProbeRejectsTimeout', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'command-exists', binary: 'my-lane', timeoutMs: COMMAND_EXISTS_FORBIDDEN_TIMEOUT_MS };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.timeoutMs is not permitted for kind "command-exists"')),
|
||
`expected a forbidden-timeout error (no process is started), got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('unknownProbeKindEnumeratesValidMembers', () => {
|
||
const lane = laneOverride((l) => { l.probe = { kind: 'nope' }; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.kind must be one of: command-exists, command-capability, http-reachable')),
|
||
`expected an unknown-kind error enumerating all three kinds, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('commandCapabilityProbeRequiresNeedle', () => {
|
||
const lane = laneOverride((l) => {
|
||
l.probe = { kind: 'command-capability', binary: 'my-lane', timeoutMs: COMMAND_CAPABILITY_PROBE_TIMEOUT_FIXTURE_MS };
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.probe.needle must be a non-empty string for kind "command-capability"')),
|
||
`expected a needle-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── F. Lane scalars ────────────────────────────────────────────────────────
|
||
|
||
describe('F. Lane scalars', () => {
|
||
test('slugIsRequiredNonEmptyString', () => {
|
||
for (const badSlug of [undefined, '', 42]) {
|
||
const lane = laneOverride((l) => {
|
||
if (badSlug === undefined) delete l.slug;
|
||
else l.slug = badSlug;
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.slug must be a non-empty string')),
|
||
`slug=${JSON.stringify(badSlug)}: expected a slug-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('snakeCaseSlugIsAcceptedUnlikeCapabilityId', () => {
|
||
const lane = laneOverride((l) => { l.slug = 'lm_studio'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors (slug is not KEBAB_RE-checked), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
// DEFECT.GENERATIVE-FIX parity assertion. The slug grammar exists in TWO places
|
||
// and cannot be reduced to one: Phase 1 owns it in src/review-lane-descriptor.cts,
|
||
// but that module compiles to gitignored build output, and the manifest validator
|
||
// is a committed plain .cjs that must load on a fresh worktree before build:lib.
|
||
// A divergence here silently reintroduces the translation layer ADR-2782 exists
|
||
// to delete — a slug the core descriptor accepts would be rejected by the
|
||
// manifest validator, and only for lanes nobody has shipped yet, so no other
|
||
// test would notice. This caught a real divergence in review: the validator
|
||
// required a leading LETTER while the descriptor allows a leading digit, which
|
||
// would have rejected a model-named lane such as `4o-mini`.
|
||
test('laneSlugGrammarMatchesPhase1Descriptor', () => {
|
||
// Built artifact — present after `npm run build:lib`, which CI runs before tests.
|
||
const descriptor = require('../msd-core/bin/lib/review-lane-descriptor.cjs');
|
||
assert.ok(
|
||
descriptor.LANE_SLUG_RE instanceof RegExp,
|
||
'Phase 1 must export LANE_SLUG_RE; if it moved, this parity assertion needs updating, not deleting',
|
||
);
|
||
assert.equal(
|
||
String(LANE_SLUG_RE), String(descriptor.LANE_SLUG_RE),
|
||
'reviewer.slug grammar has diverged between the manifest validator and the Phase 1 core descriptor',
|
||
);
|
||
|
||
// Behavioural parity, not just source equality: the same inputs must get the
|
||
// same verdict from both surfaces.
|
||
for (const slug of ['gemini', 'lm_studio', 'llama_cpp', '4o-mini', '2b-local', 'kimi-code']) {
|
||
const lane = laneOverride((l) => { l.slug = slug; });
|
||
const accepted = validateReviewerBody({ id: 'x', reviewer: lane }).length === 0;
|
||
assert.equal(
|
||
accepted, descriptor.LANE_SLUG_RE.test(slug),
|
||
`slug "${slug}": manifest validator and core descriptor disagree`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('slugRejectsUppercase', () => {
|
||
const lane = laneOverride((l) => { l.slug = 'Lm-Studio'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.slug "Lm-Studio" must match')),
|
||
`expected an uppercase-rejection error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('slugRejectsWhitespace', () => {
|
||
const lane = laneOverride((l) => { l.slug = 'lm studio'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.slug "lm studio" must match')),
|
||
`expected a whitespace-rejection error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('slugRejectsFlagSyntax', () => {
|
||
const lane = laneOverride((l) => { l.slug = '--x'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.slug "--x" must match')),
|
||
`expected a flag-syntax-rejection error (a slug is not a flag), got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('flagsArrayIsRequired', () => {
|
||
for (const badFlags of [undefined, 'not-an-array']) {
|
||
const lane = laneOverride((l) => {
|
||
if (badFlags === undefined) delete l.flags;
|
||
else l.flags = badFlags;
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.flags must be an array of CLI flags')),
|
||
`flags=${JSON.stringify(badFlags)}: expected a flags-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('emptyFlagsArrayIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.flags = []; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.flags must declare at least one flag')),
|
||
`expected an unnameable-lane rejection, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('duplicateFlagWithinOneLaneIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.flags = ['--x', '--x']; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.flags lists "--x" more than once')),
|
||
`expected a self-duplicate-flag error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('flagWithoutDoubleDashIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.flags = ['x']; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.flags entry "x" must match')),
|
||
`expected a missing-double-dash error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('multipleFlagsPerLaneAreValidPerAmendment', () => {
|
||
const lane = laneOverride((l) => { l.flags = ['--antigravity', '--agy']; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors (A4 amendment), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('timeoutFloorIsRequired', () => {
|
||
const lane = laneOverride((l) => { delete l.timeoutFloorMs; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.timeoutFloorMs must be a positive integer of milliseconds')),
|
||
`expected a timeoutFloorMs-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('timeoutFloorZeroIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.timeoutFloorMs = 0; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.timeoutFloorMs must be a positive integer of milliseconds') && e.includes('(got: 0)')),
|
||
`expected a boundary (limit-1) rejection, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('timeoutFloorOneIsAccepted', () => {
|
||
const lane = laneOverride((l) => { l.timeoutFloorMs = 1; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors (boundary: limit), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('emptyOutputAcceptsBothMembers', () => {
|
||
for (const emptyOutput of VALID_EMPTY_OUTPUT) {
|
||
const lane = laneOverride((l) => { l.emptyOutput = emptyOutput; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `emptyOutput=${emptyOutput} expected no errors, got: ${JSON.stringify(errs)}`);
|
||
}
|
||
});
|
||
|
||
test('reviewsSectionIsRequiredNonEmpty', () => {
|
||
for (const bad of [undefined, '']) {
|
||
const lane = laneOverride((l) => {
|
||
if (bad === undefined) delete l.reviewsSection;
|
||
else l.reviewsSection = bad;
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.reviewsSection must be a non-empty string')),
|
||
`reviewsSection=${JSON.stringify(bad)}: expected a reviewsSection-required error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('evidenceClassAcceptsBothMembers', () => {
|
||
for (const evidenceClass of VALID_EVIDENCE_CLASSES) {
|
||
const lane = laneOverride((l) => { l.evidenceClass = evidenceClass; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `evidenceClass=${evidenceClass} expected no errors, got: ${JSON.stringify(errs)}`);
|
||
}
|
||
});
|
||
|
||
test('requiresBinariesArrayIsRequired', () => {
|
||
for (const bad of [undefined, 'not-an-array']) {
|
||
const lane = laneOverride((l) => {
|
||
if (bad === undefined) delete l.requiresBinaries;
|
||
else l.requiresBinaries = bad;
|
||
});
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.requiresBinaries must be an array')),
|
||
`requiresBinaries=${JSON.stringify(bad)}: expected a required-array error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('emptyRequiresBinariesIsValid', () => {
|
||
const lane = laneOverride((l) => { l.requiresBinaries = []; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors (no external tool required), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('promptBudgetKeyNullIsValid', () => {
|
||
const lane = laneOverride((l) => { l.promptBudgetKey = null; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('promptBudgetKeyEmptyStringIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.promptBudgetKey = ''; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.promptBudgetKey must be a dotted config key or null') && e.includes('(got: "")')),
|
||
`expected an empty-string-is-not-none error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('handlerNullIsValidDefault', () => {
|
||
const lane = laneOverride((l) => { l.handler = null; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors (default), got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('handlerAcceptsEachFirstPartyMember', () => {
|
||
for (const handler of VALID_LANE_HANDLERS) {
|
||
const lane = laneOverride((l) => { l.handler = handler; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `handler=${handler} expected no errors, got: ${JSON.stringify(errs)}`);
|
||
}
|
||
});
|
||
|
||
test('unknownHandlerIsRejectedAtBuildTime', () => {
|
||
const lane = laneOverride((l) => { l.handler = 'acme'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.handler must be null or one of: antigravity, openai-compatible') && e.includes('"acme"')),
|
||
`expected an unknown-handler error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('pathTraversalHandlerIsRejected', () => {
|
||
const lane = laneOverride((l) => { l.handler = '../evil'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.handler must be null or one of: antigravity, openai-compatible') && e.includes('"../evil"')),
|
||
`expected a path-shaped-handler rejection, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── F2. timeoutConfigKey (#3274) — mirrors modelConfigKey's D4 forward/backward-compat shape ──
|
||
|
||
describe('F2. timeoutConfigKey (#3274)', () => {
|
||
test('reviewer.timeoutConfigKey is optional — absent is valid (row 14)', () => {
|
||
const lane = laneOverride(() => {});
|
||
assert.equal(lane.timeoutConfigKey, undefined, 'fixture must not declare timeoutConfigKey');
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('reviewer.timeoutConfigKey: null is valid (row 15)', () => {
|
||
const lane = laneOverride((l) => { l.timeoutConfigKey = null; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('reviewer.timeoutConfigKey: a dotted string is valid (row 16)', () => {
|
||
const lane = laneOverride((l) => { l.timeoutConfigKey = 'review.timeouts.acme'; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('reviewer.timeoutConfigKey: empty string is rejected (row 17)', () => {
|
||
const lane = laneOverride((l) => { l.timeoutConfigKey = ''; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.timeoutConfigKey must be a dotted config key or null') && e.includes('(got: "")')),
|
||
`expected an empty-string-is-not-none error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('reviewer.timeoutConfigKey: non-string, non-null is rejected (row 18)', () => {
|
||
for (const bad of [42, {}, []]) {
|
||
const lane = laneOverride((l) => { l.timeoutConfigKey = bad; });
|
||
const errs = validateReviewerBody({ id: 'x', reviewer: lane });
|
||
assert.ok(
|
||
errs.some((e) => e.includes('reviewer.timeoutConfigKey must be a dotted config key or null')),
|
||
`timeoutConfigKey=${JSON.stringify(bad)}: expected a type-rejection error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ─── G. Uniqueness (D8) — validateCrossCapability(Map, Set) ────────────────
|
||
|
||
describe('G. Uniqueness (D8) — validateCrossCapability(Map, Set)', () => {
|
||
// Each lane defaults to a slug/flags/reviewsSection derived from its own id,
|
||
// so distinct caps never accidentally collide — a test forces exactly ONE
|
||
// axis to collide via `reviewerOverrides`, isolating what it claims to test.
|
||
function laneCap(id, reviewerOverrides) {
|
||
return {
|
||
id,
|
||
role: 'reviewer',
|
||
reviewer: Object.assign(
|
||
validLane(),
|
||
{ slug: `${id}-slug`, flags: [`--${id}`], reviewsSection: `Section for ${id}` },
|
||
reviewerOverrides || {},
|
||
),
|
||
};
|
||
}
|
||
|
||
test('duplicateSlugAcrossCapabilitiesIsRejected', () => {
|
||
const capA = laneCap('cap-a');
|
||
const capB = laneCap('cap-b', { slug: capA.reviewer.slug });
|
||
const errs = validateCrossCapability(new Map([['cap-a', capA], ['cap-b', capB]]), new Set());
|
||
assert.ok(
|
||
errs.some((e) => e.includes(`reviewer slug "${capA.reviewer.slug}" is declared by "cap-a" and "cap-b"`)),
|
||
`expected a slug-collision error naming both ids, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
// Regression guard for a real order-dependence defect in the first cut of the
|
||
// lane-uniqueness check. That version reported a collision when the SECOND
|
||
// claimant arrived, so with three lanes on one key it named whichever pair
|
||
// happened to arrive first: forward order blamed {cap-0,cap-1}+{cap-0,cap-2},
|
||
// reversed blamed {cap-1,cap-2}+{cap-0,cap-2}. Map order is readdir order at
|
||
// build time and candidate order at load time, so a cross-platform CI lane
|
||
// could disagree with a local run about the text of the same failure. The
|
||
// pairwise case (G7) is order-independent either way, which is exactly why it
|
||
// did not catch this. Claims are now accumulated and reported after the sweep:
|
||
// ONE error per colliding key naming EVERY claimant, ids sorted.
|
||
test('threeWayCollisionIsReportedOnceAndOrderIndependently', () => {
|
||
const ids = ['cap-0', 'cap-1', 'cap-2'];
|
||
const caps = ids.map((id) => laneCap(id, { slug: 'shared-slug' }));
|
||
const laneErrs = (entries) => validateCrossCapability(new Map(entries), new Set())
|
||
.filter((e) => e.startsWith('reviewer slug '));
|
||
|
||
const forward = laneErrs(ids.map((id, i) => [id, caps[i]]));
|
||
const reversed = laneErrs(ids.map((id, i) => [id, caps[i]]).reverse());
|
||
|
||
assert.equal(
|
||
forward.length, 1,
|
||
`a 3-way collision is ONE error naming all three, got: ${JSON.stringify(forward)}`,
|
||
);
|
||
assert.deepEqual(
|
||
forward, reversed,
|
||
`error text must not depend on Map insertion order, got forward=${JSON.stringify(forward)} reversed=${JSON.stringify(reversed)}`,
|
||
);
|
||
for (const id of ids) {
|
||
assert.ok(forward[0].includes(`"${id}"`), `every claimant must be named; ${id} missing from: ${forward[0]}`);
|
||
}
|
||
});
|
||
|
||
test('overlappingFlagsAcrossCapabilitiesAreRejected', () => {
|
||
const capA = laneCap('cap-a');
|
||
const capB = laneCap('cap-b', { flags: [...capA.reviewer.flags] });
|
||
const errs = validateCrossCapability(new Map([['cap-a', capA], ['cap-b', capB]]), new Set());
|
||
assert.ok(
|
||
errs.some((e) => e.includes(`reviewer flag "${capA.reviewer.flags[0]}" is declared by "cap-a" and "cap-b"`)),
|
||
`expected a flag-collision error (flattened across arrays), got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('duplicateReviewsSectionIsRejected', () => {
|
||
const capA = laneCap('cap-a');
|
||
const capB = laneCap('cap-b', { reviewsSection: capA.reviewer.reviewsSection });
|
||
const errs = validateCrossCapability(new Map([['cap-a', capA], ['cap-b', capB]]), new Set());
|
||
assert.ok(
|
||
errs.some((e) => e.includes(`reviewer reviewsSection "${capA.reviewer.reviewsSection}" is declared by "cap-a" and "cap-b"`)),
|
||
`expected a reviewsSection-collision error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('reviewsSectionUniquenessIsCaseSensitiveByDesign', () => {
|
||
const capA = laneCap('cap-a');
|
||
const capB = laneCap('cap-b', { reviewsSection: capA.reviewer.reviewsSection.toUpperCase() });
|
||
const errs = validateCrossCapability(new Map([['cap-a', capA], ['cap-b', capB]]), new Set());
|
||
assert.deepEqual(
|
||
errs, [],
|
||
`case-differing reviewsSection must NOT collide (Rejected #4, Known Limit), got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('singleLaneHasNoUniquenessError', () => {
|
||
const capA = laneCap('cap-a');
|
||
const errs = validateCrossCapability(new Map([['cap-a', capA]]), new Set());
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('capabilitiesWithoutLanesContributeNoUniquenessErrors', () => {
|
||
const capA = { id: 'cap-a', role: 'runtime' };
|
||
const capB = { id: 'cap-b', role: 'runtime' };
|
||
const errs = validateCrossCapability(new Map([['cap-a', capA], ['cap-b', capB]]), new Set());
|
||
assert.deepEqual(errs, [], `expected no errors, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('nonLaneCapabilityIdMayMatchALaneSlug', () => {
|
||
const capA = laneCap('cap-a', { slug: 'shared-name' });
|
||
const capB = { id: 'shared-name', role: 'runtime' };
|
||
const errs = validateCrossCapability(new Map([['cap-a', capA], ['shared-name', capB]]), new Set());
|
||
assert.deepEqual(
|
||
errs, [],
|
||
`a capability id matching a lane slug string is not a collision (instances/ids are not lanes), got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('uniquenessErrorsAreOrderIndependent', () => {
|
||
const capA = laneCap('cap-a');
|
||
const capB = laneCap('cap-b', { slug: capA.reviewer.slug });
|
||
const forward = new Map([['cap-a', capA], ['cap-b', capB]]);
|
||
const reversed = new Map([['cap-b', capB], ['cap-a', capA]]);
|
||
const errsForward = validateCrossCapability(forward, new Set());
|
||
const errsReversed = validateCrossCapability(reversed, new Set());
|
||
assert.ok(errsForward.length > 0, `expected a real collision to exercise, got: ${JSON.stringify(errsForward)}`);
|
||
assert.deepEqual(
|
||
errsForward, errsReversed,
|
||
`error set must not depend on Map insertion order: forward=${JSON.stringify(errsForward)} reversed=${JSON.stringify(errsReversed)}`,
|
||
);
|
||
});
|
||
|
||
test('threeLanesWithOneCollisionReportOnce', () => {
|
||
const capA = laneCap('cap-a');
|
||
const capB = laneCap('cap-b', { slug: capA.reviewer.slug });
|
||
const capC = laneCap('cap-c');
|
||
const errs = validateCrossCapability(
|
||
new Map([['cap-a', capA], ['cap-b', capB], ['cap-c', capC]]),
|
||
new Set(),
|
||
);
|
||
const slugErrors = errs.filter((e) => e.startsWith('reviewer slug'));
|
||
assert.equal(slugErrors.length, 1, `expected exactly one collision error, not two, got: ${JSON.stringify(errs)}`);
|
||
assert.equal(errs.length, 1, `expected no unrelated errors from the non-colliding third lane, got: ${JSON.stringify(errs)}`);
|
||
});
|
||
});
|
||
|
||
// ─── H. Harvest widening — buildRegistry(capMap) ───────────────────────────
|
||
|
||
describe('H. Harvest widening — buildRegistry(capMap)', () => {
|
||
test('runtimeCapConfigIsHarvestedNotDropped', () => {
|
||
const capMap = new Map([['my-runtime', {
|
||
id: 'my-runtime',
|
||
role: 'runtime',
|
||
config: { 'myRuntime.enabled': { type: 'boolean', default: false, description: 'Enable my runtime feature.' } },
|
||
}]]);
|
||
const registry = buildRegistry(capMap);
|
||
assert.equal(registry.configKeys['myRuntime.enabled'], 'my-runtime');
|
||
assert.deepEqual(registry.configSchema['myRuntime.enabled'], {
|
||
owner: 'my-runtime',
|
||
type: 'boolean',
|
||
default: false,
|
||
description: 'Enable my runtime feature.',
|
||
});
|
||
});
|
||
|
||
test('reviewerCapConfigIsHarvested', () => {
|
||
const capMap = new Map([['my-reviewer', {
|
||
id: 'my-reviewer',
|
||
role: 'reviewer',
|
||
config: { 'myReviewer.enabled': { type: 'boolean', default: true, description: 'Enable my reviewer lane.' } },
|
||
}]]);
|
||
const registry = buildRegistry(capMap);
|
||
assert.equal(registry.configKeys['myReviewer.enabled'], 'my-reviewer');
|
||
assert.deepEqual(registry.configSchema['myReviewer.enabled'], {
|
||
owner: 'my-reviewer',
|
||
type: 'boolean',
|
||
default: true,
|
||
description: 'Enable my reviewer lane.',
|
||
});
|
||
});
|
||
|
||
test('runtimeCapWithoutConfigIsUnchanged', () => {
|
||
const cap = { id: 'my-runtime', role: 'runtime' };
|
||
const capMap = new Map([['my-runtime', cap]]);
|
||
const registry = buildRegistry(capMap);
|
||
assert.equal(registry.runtimes['my-runtime'], cap);
|
||
// registry.configKeys is Object.create(null) (S2b prototype-pollution guard),
|
||
// so comparing against a plain `{}` literal would fail on prototype alone —
|
||
// compare via Object.keys() instead of asserting shape equality.
|
||
assert.deepEqual(Object.keys(registry.configKeys), []);
|
||
});
|
||
|
||
test('reviewerCapIsNotStoredAsRuntime', () => {
|
||
const cap = { id: 'my-reviewer', role: 'reviewer' };
|
||
const capMap = new Map([['my-reviewer', cap]]);
|
||
const registry = buildRegistry(capMap);
|
||
assert.equal(registry.capabilities['my-reviewer'], cap);
|
||
assert.equal(registry.runtimes['my-reviewer'], undefined);
|
||
});
|
||
|
||
// Lives in validateCrossCapability's config-key ownership loop, not
|
||
// buildRegistry — that loop is the OTHER half of the harvest widening
|
||
// (40-design.md: "the role filter was `role !== 'feature'`"), so this row
|
||
// and H6 exercise validateCrossCapability directly rather than the harvest.
|
||
test('nonFeatureConfigKeyCollidingWithCentralSchemaIsFlagged', () => {
|
||
const capMap = new Map([['my-runtime', {
|
||
id: 'my-runtime',
|
||
role: 'runtime',
|
||
config: { 'shared.key': { type: 'string', default: 'x', description: 'y' } },
|
||
}]]);
|
||
const errs = validateCrossCapability(capMap, new Set(['shared.key']));
|
||
assert.ok(
|
||
errs.some((e) => e.includes('shared.key') && e.includes('central config-schema')),
|
||
`expected a central-schema collision error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('configKeyOwnedByTwoRolesIsRejected', () => {
|
||
const capMap = new Map([
|
||
['cap-a', { id: 'cap-a', role: 'runtime', config: { 'dup.key': { type: 'string', default: 'x', description: 'y' } } }],
|
||
['cap-b', { id: 'cap-b', role: 'reviewer', config: { 'dup.key': { type: 'string', default: 'z', description: 'w' } } }],
|
||
]);
|
||
const errs = validateCrossCapability(capMap, new Set());
|
||
assert.ok(
|
||
errs.some((e) => e.includes('dup.key') && e.includes('owned by both "cap-a" and "cap-b"')),
|
||
`expected a two-role ownership-collision error, got: ${JSON.stringify(errs)}`,
|
||
);
|
||
});
|
||
|
||
test('shippedRegistryConfigKeysAreOwnedByDeclaringCapabilities', () => {
|
||
// Phase 2 introduced the harvest widening (ownership follows DECLARING a
|
||
// config slice, not being a feature) and asserted it was INERT — no shipped
|
||
// non-feature capability owned a config key, so the widening changed nothing.
|
||
//
|
||
// #2797 (Phase 4) is what consumes it: the reviewer lanes now own their own
|
||
// config keys, and those lanes are `role: "reviewer"` (lane-only CLIs) or
|
||
// `role: "runtime"` (dual-purpose install targets). Asserting `role ===
|
||
// 'feature'` here would now be asserting the migration did not happen.
|
||
//
|
||
// The protection is kept, just restated: every key's owner must exist, and a
|
||
// `review.*` key must be owned by a capability that actually declares a
|
||
// reviewer lane — not by an arbitrary capability that happened to claim it.
|
||
const { capMap, errors } = loadAndValidate(new Set());
|
||
assert.deepEqual(errors, [], `expected the real shipped capability set to validate cleanly, got: ${JSON.stringify(errors)}`);
|
||
|
||
const registry = buildRegistry(capMap);
|
||
let reviewKeys = 0;
|
||
for (const [key, ownerId] of Object.entries(registry.configKeys)) {
|
||
const owner = capMap.get(ownerId);
|
||
assert.ok(owner, `configKeys owner "${ownerId}" for key "${key}" must exist in capMap`);
|
||
assert.ok(
|
||
['feature', 'reviewer', 'runtime'].includes(owner.role),
|
||
`config key "${key}" is owned by capability "${ownerId}" with unexpected role "${owner.role}"`,
|
||
);
|
||
if (key.startsWith('review.')) {
|
||
reviewKeys += 1;
|
||
assert.ok(
|
||
owner.reviewer && typeof owner.reviewer.slug === 'string',
|
||
`review config key "${key}" must be owned by a capability declaring a reviewer lane, ` +
|
||
`but "${ownerId}" declares none`,
|
||
);
|
||
}
|
||
}
|
||
assert.ok(reviewKeys > 0, 'expected the federated reviewer config keys to be present (#2797)');
|
||
});
|
||
});
|
||
|
||
// ─── I. Integration — loadAndValidate(centralKeys, tmpCapDir) ──────────────
|
||
|
||
describe('I. Integration — loadAndValidate(centralKeys, tmpCapDir)', () => {
|
||
function withCapDir(foldersToContent, fn) {
|
||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-reviewer-body-'));
|
||
try {
|
||
for (const [folder, contentOrCap] of Object.entries(foldersToContent)) {
|
||
const dir = path.join(tmp, folder);
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
const content = typeof contentOrCap === 'string' ? contentOrCap : `${JSON.stringify(contentOrCap, null, 2)}\n`;
|
||
fs.writeFileSync(path.join(dir, 'capability.json'), content, 'utf8');
|
||
}
|
||
return fn(tmp);
|
||
} finally {
|
||
cleanup(tmp);
|
||
}
|
||
}
|
||
|
||
test('reviewerCapabilityLoadsFromDisk', () => {
|
||
const cap = capWith({
|
||
id: 'lm-studio',
|
||
reviewer: Object.assign(validLane(), { slug: 'lm-studio', flags: ['--lm-studio'] }),
|
||
});
|
||
withCapDir({ 'lm-studio': cap }, (tmp) => {
|
||
const { capMap, errors } = loadAndValidate(new Set(), tmp);
|
||
assert.deepEqual(errors, [], `expected no errors, got: ${JSON.stringify(errors)}`);
|
||
assert.ok(capMap.has('lm-studio'));
|
||
});
|
||
});
|
||
|
||
test('malformedReviewerBodyReportsFolderId', () => {
|
||
const cap = capWith({ id: 'lm-studio', reviewer: {} });
|
||
withCapDir({ 'lm-studio': cap }, (tmp) => {
|
||
const { errors } = loadAndValidate(new Set(), tmp);
|
||
assert.ok(
|
||
errors.some((e) => e.startsWith('lm-studio/capability.json:') && e.includes('reviewer.slug')),
|
||
`expected the folder id to prefix the reported error, got: ${JSON.stringify(errors)}`,
|
||
);
|
||
});
|
||
});
|
||
|
||
test('emptyCapabilityFileIsReportedNotCrashed', () => {
|
||
withCapDir({ 'lm-studio': '' }, (tmp) => {
|
||
let errors;
|
||
assert.doesNotThrow(() => { ({ errors } = loadAndValidate(new Set(), tmp)); });
|
||
assert.ok(
|
||
errors.some((e) => e.includes('lm-studio/capability.json') && e.includes('JSON parse error')),
|
||
`expected a JSON-parse error naming the folder, got: ${JSON.stringify(errors)}`,
|
||
);
|
||
});
|
||
});
|
||
|
||
test('crlfCapabilityFileParsesIdentically', () => {
|
||
const cap = capWith({
|
||
id: 'lm-studio',
|
||
reviewer: Object.assign(validLane(), { slug: 'lm-studio', flags: ['--lm-studio'] }),
|
||
});
|
||
const lfContent = `${JSON.stringify(cap, null, 2)}\n`;
|
||
const crlfContent = lfContent.replace(/\n/g, '\r\n');
|
||
withCapDir({ 'lm-studio': lfContent }, (tmpLf) => {
|
||
withCapDir({ 'lm-studio': crlfContent }, (tmpCrlf) => {
|
||
const lfResult = loadAndValidate(new Set(), tmpLf);
|
||
const crlfResult = loadAndValidate(new Set(), tmpCrlf);
|
||
assert.deepEqual(lfResult.errors, [], `LF variant should validate cleanly, got: ${JSON.stringify(lfResult.errors)}`);
|
||
assert.deepEqual(crlfResult.errors, [], `CRLF variant should validate cleanly, got: ${JSON.stringify(crlfResult.errors)}`);
|
||
assert.deepEqual(
|
||
crlfResult.capMap.get('lm-studio'),
|
||
lfResult.capMap.get('lm-studio'),
|
||
'a CRLF-authored manifest must parse identically to its LF counterpart',
|
||
);
|
||
});
|
||
});
|
||
});
|
||
|
||
test('unreadableCapabilityFileIsReportedNotCrashed', () => {
|
||
const cap = capWith({
|
||
id: 'lm-studio',
|
||
reviewer: Object.assign(validLane(), { slug: 'lm-studio', flags: ['--lm-studio'] }),
|
||
});
|
||
withCapDir({ 'lm-studio': cap }, (tmp) => {
|
||
const capPath = path.join(tmp, 'lm-studio', 'capability.json');
|
||
// House rule: inject a deterministic IO failure by monkeypatching the fs
|
||
// method (save the original, override it to throw, restore in `finally`).
|
||
// NEVER `chmod 0o000` — root bypasses mode bits, so that trick silently
|
||
// passes with zero coverage in root Docker/CI. The override is scoped to
|
||
// the exact capability.json path under test and delegates every other
|
||
// read (e.g. loadAndValidate's own wired-loop-point scan) to the real
|
||
// implementation, so it fails only the read this test cares about.
|
||
const originalReadFileSync = fs.readFileSync;
|
||
let result;
|
||
try {
|
||
fs.readFileSync = (p, ...rest) => {
|
||
if (p === capPath) {
|
||
throw new Error('injected unreadable-file failure (simulated EACCES)');
|
||
}
|
||
return originalReadFileSync.call(fs, p, ...rest);
|
||
};
|
||
assert.doesNotThrow(() => { result = loadAndValidate(new Set(), tmp); });
|
||
} finally {
|
||
fs.readFileSync = originalReadFileSync;
|
||
}
|
||
assert.ok(
|
||
result.errors.some((e) => e.includes('lm-studio') && e.includes('injected unreadable-file failure')),
|
||
`expected the unreadable file to be reported, not crashed, got: ${JSON.stringify(result.errors)}`,
|
||
);
|
||
});
|
||
});
|
||
|
||
test('snakeCaseIdIsRejectedEvenWhenSlugIsSnakeCase', () => {
|
||
// The Phase 5a trap: folder is kebab ("lm-studio") and reviewer.slug is
|
||
// snake ("lm_studio", accepted per F2) — but `id` itself must ALSO be kebab.
|
||
const badCap = capWith({
|
||
id: 'lm_studio',
|
||
reviewer: Object.assign(validLane(), { slug: 'lm_studio', flags: ['--lm-studio'] }),
|
||
});
|
||
withCapDir({ 'lm-studio': badCap }, (tmp) => {
|
||
const { errors } = loadAndValidate(new Set(), tmp);
|
||
assert.ok(
|
||
errors.some((e) => e.includes('lm-studio/capability.json') && e.includes('kebab-case')),
|
||
`expected a snake_case id to be rejected, got: ${JSON.stringify(errors)}`,
|
||
);
|
||
});
|
||
|
||
const goodCap = capWith({
|
||
id: 'lm-studio',
|
||
reviewer: Object.assign(validLane(), { slug: 'lm_studio', flags: ['--lm-studio'] }),
|
||
});
|
||
withCapDir({ 'lm-studio': goodCap }, (tmp) => {
|
||
const { capMap, errors } = loadAndValidate(new Set(), tmp);
|
||
assert.deepEqual(
|
||
errors, [],
|
||
`a kebab id with a snake_case reviewer.slug must be accepted, got: ${JSON.stringify(errors)}`,
|
||
);
|
||
assert.ok(capMap.has('lm-studio'));
|
||
});
|
||
});
|
||
|
||
// ADR-2782 D4.3, LOAD-TIME half. The build-time generator only ever sees
|
||
// first-party in-repo manifests, so an unknown field on an INSTALLED
|
||
// third-party lane — the case D4.3 exists for — surfaced nowhere at runtime
|
||
// until the loader was wired to collect diagnostics. A global-scope overlay is
|
||
// used because project scope additionally requires a consent record; the
|
||
// subject here is the diagnostic channel, not the consent gate.
|
||
test('overlayLaneWithUnknownFieldIsAcceptedAndDiagnosed', () => {
|
||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-reviewer-overlay-'));
|
||
try {
|
||
const capDir = path.join(home, '.msd', 'capabilities', 'acme-reviewer');
|
||
fs.mkdirSync(capDir, { recursive: true });
|
||
fs.writeFileSync(path.join(capDir, 'capability.json'), JSON.stringify({
|
||
id: 'acme-reviewer',
|
||
role: 'reviewer',
|
||
version: '1.0.0',
|
||
title: 'Acme',
|
||
description: 'Third-party reviewer lane.',
|
||
tier: 'full',
|
||
requires: [],
|
||
engines: { msd: '>=1.0.0' },
|
||
reviewer: Object.assign(validLane(), {
|
||
slug: 'acme',
|
||
flags: ['--acme'],
|
||
reviewsSection: 'Acme',
|
||
// The field a NEWER MSD would understand and this one does not.
|
||
futureFieldFromNewerMsd: true,
|
||
}),
|
||
}));
|
||
|
||
const { loadRegistry } = require('../msd-core/bin/lib/capability-loader.cjs');
|
||
const registry = loadRegistry({
|
||
includeInstalled: true, cwd: process.cwd(), msdHome: home, hostVersion: '1.8.0',
|
||
});
|
||
|
||
// Accepted, NOT skipped — an unknown field must never cost the user the lane.
|
||
assert.ok(
|
||
registry.capabilities && registry.capabilities['acme-reviewer'],
|
||
'a third-party lane with an unknown reviewer field must still be accepted',
|
||
);
|
||
const overlay = registry._overlay;
|
||
assert.ok(overlay, 'overlay meta must be attached when an overlay is composed');
|
||
assert.deepEqual(
|
||
overlay.warnings, [],
|
||
`an unknown field is a diagnostic, not a skip, got: ${JSON.stringify(overlay.warnings)}`,
|
||
);
|
||
assert.ok(
|
||
overlay.diagnostics.some((d) => d.includes('futureFieldFromNewerMsd')),
|
||
`expected a diagnostic naming the unknown field, got: ${JSON.stringify(overlay.diagnostics)}`,
|
||
);
|
||
} finally {
|
||
cleanup(home);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ─── J. Property-based (fast-check) ────────────────────────────────────────
|
||
|
||
describe('J. Property-based (fast-check)', () => {
|
||
// The first version of this property used a bare `fc.anything()` as the whole
|
||
// reviewer value, and it was FALSE CONFIDENCE: at default constraints
|
||
// fc.anything() emits no BigInt, no circular reference, no getter and no
|
||
// Proxy — 20,000 sampled draws produced zero of each — which is precisely the
|
||
// value space that broke the contract. Worse, even enabling withBigInt is not
|
||
// enough under whole-value fuzzing, because the bug needs an exotic value in a
|
||
// SPECIFICALLY NAMED field and random key names essentially never land on one.
|
||
// So the generator is field-targeted, and the value space is widened by hand
|
||
// to include the shapes JSON cannot express but a JS caller can still pass.
|
||
const assertTotal = (cap, label) => {
|
||
let result;
|
||
try {
|
||
result = validateReviewerBody(cap);
|
||
} catch (err) {
|
||
assert.fail(`validateReviewerBody threw for ${label}: ${err && err.message}`);
|
||
}
|
||
assert.ok(Array.isArray(result), `must return an array for ${label}`);
|
||
assert.ok(result.every((e) => typeof e === 'string'), `every entry must be a string for ${label}`);
|
||
};
|
||
|
||
test('validateReviewerBodyNeverThrowsOnArbitraryInput', () => {
|
||
// Whole-value fuzzing — the original property, kept as the broad sweep.
|
||
fc.assert(
|
||
fc.property(fc.anything({ withBigInt: true }), (reviewerValue) => {
|
||
assertTotal({ id: 'prop-test', reviewer: reviewerValue }, 'whole-body fuzz');
|
||
return true;
|
||
}),
|
||
{ numRuns: 500 },
|
||
);
|
||
|
||
// Field-targeted fuzzing — this is the variant that actually falsifies the
|
||
// pre-fix implementation, on roughly the first generated case.
|
||
const TARGET_FIELDS = [
|
||
'slug', 'flags', 'transport', 'probe', 'invoke', 'timeoutFloorMs',
|
||
'emptyOutput', 'reviewsSection', 'evidenceClass', 'requiresBinaries',
|
||
'promptBudgetKey', 'handler',
|
||
];
|
||
fc.assert(
|
||
fc.property(
|
||
fc.constantFrom(...TARGET_FIELDS),
|
||
fc.anything({ withBigInt: true }),
|
||
(field, value) => {
|
||
const lane = laneOverride((l) => { l[field] = value; });
|
||
assertTotal({ id: 'prop-test', reviewer: lane }, `${field} = ${String(field)}`);
|
||
return true;
|
||
},
|
||
),
|
||
{ numRuns: 1000 },
|
||
);
|
||
});
|
||
|
||
// The shapes fast-check cannot generate but a JS caller can still hand us.
|
||
// These are enumerated rather than fuzzed because a generator that produced
|
||
// them would be reimplementing the enumeration anyway.
|
||
test('validateReviewerBodyNeverThrowsOnValuesJsonCannotExpress', () => {
|
||
const circular = () => { const o = {}; o.self = o; return o; };
|
||
const throwingGetter = () => {
|
||
const o = {};
|
||
Object.defineProperty(o, 'valueOf', { get() { throw new Error('boom'); } });
|
||
Object.defineProperty(o, 'toJSON', { get() { throw new Error('boom'); } });
|
||
return o;
|
||
};
|
||
const hostile = [
|
||
['bigint', 10n],
|
||
['circular', circular()],
|
||
['throwing-getter', throwingGetter()],
|
||
['symbol', Symbol('s')],
|
||
['function', () => {}],
|
||
['null-prototype', Object.create(null)],
|
||
];
|
||
|
||
for (const field of ['slug', 'transport', 'timeoutFloorMs', 'handler', 'promptBudgetKey', 'probe', 'invoke']) {
|
||
for (const [name, value] of hostile) {
|
||
const lane = laneOverride((l) => { l[field] = value; });
|
||
assertTotal({ id: 'prop-test', reviewer: lane }, `${field} = ${name}`);
|
||
}
|
||
}
|
||
|
||
// Array-element positions, which take a different code path from scalars.
|
||
for (const field of ['flags', 'requiresBinaries']) {
|
||
for (const [name, value] of hostile) {
|
||
const lane = laneOverride((l) => { l[field] = [value]; });
|
||
assertTotal({ id: 'prop-test', reviewer: lane }, `${field}[0] = ${name}`);
|
||
}
|
||
}
|
||
const argsLane = laneOverride((l) => { l.invoke.args = [circular()]; });
|
||
assertTotal({ id: 'prop-test', reviewer: argsLane }, 'invoke.args[0] = circular');
|
||
|
||
// Read-time throws: a getter or Proxy trap fires BEFORE any message is built,
|
||
// so describeValue() alone cannot save these — only the structural wrapper can.
|
||
assertTotal(
|
||
Object.defineProperty({ reviewer: {} }, 'id', { get() { throw new Error('boom'); } }),
|
||
'throwing getter on cap.id',
|
||
);
|
||
const slugThrows = laneOverride(() => {});
|
||
Object.defineProperty(slugThrows, 'slug', { get() { throw new Error('boom'); } });
|
||
assertTotal({ id: 'x', reviewer: slugThrows }, 'throwing getter on reviewer.slug');
|
||
assertTotal({ id: 'x', reviewer: new Proxy({}, { get() { throw new Error('boom'); } }) }, 'Proxy get trap');
|
||
assertTotal({ id: 'x', reviewer: new Proxy({}, { ownKeys() { throw new Error('boom'); } }) }, 'Proxy ownKeys trap');
|
||
|
||
// collectReviewerWarnings carries the same contract — it runs on
|
||
// loadRegistry's ACCEPT path, so a throwing diagnostic would cost a user a
|
||
// lane that is otherwise perfectly valid.
|
||
for (const cap of [
|
||
Object.defineProperty({ reviewer: {} }, 'id', { get() { throw new Error('boom'); } }),
|
||
{ id: 'x', reviewer: new Proxy({}, { ownKeys() { throw new Error('boom'); } }) },
|
||
]) {
|
||
let warnings;
|
||
try {
|
||
warnings = collectReviewerWarnings(cap);
|
||
} catch (err) {
|
||
assert.fail(`collectReviewerWarnings threw: ${err && err.message}`);
|
||
}
|
||
assert.ok(Array.isArray(warnings), 'collectReviewerWarnings must always return an array');
|
||
}
|
||
});
|
||
|
||
test('uniquenessIsInvariantUnderMapOrder', () => {
|
||
// Groups of ARBITRARY size (1..4 capabilities sharing one slug), so N-way
|
||
// collisions are exercised, not just pairwise ones. An earlier cut of the
|
||
// uniqueness check reported a collision the moment a second claimant arrived,
|
||
// which named a different pair depending on insertion order once three lanes
|
||
// shared a key; claims are now accumulated and reported after the sweep, so
|
||
// the guarantee holds for any N. Errors are compared UNSORTED and filtered to
|
||
// the lane errors, so this asserts deterministic emission ORDER too — sorting
|
||
// both sides first would hide exactly the defect this guards.
|
||
fc.assert(
|
||
fc.property(
|
||
fc.array(fc.integer({ min: 1, max: 4 }), { minLength: 1, maxLength: 4 }),
|
||
fc.array(fc.integer({ min: 0, max: 999 }), { minLength: 0, maxLength: 8 }),
|
||
(groupSizes, shuffleSeed) => {
|
||
const entries = [];
|
||
groupSizes.forEach((size, groupIdx) => {
|
||
for (let member = 0; member < size; member += 1) {
|
||
const id = `cap-${groupIdx}-${member}`;
|
||
entries.push([id, {
|
||
id,
|
||
role: 'reviewer',
|
||
// Every member of a group shares one slug; flags and sections stay
|
||
// distinct so the slug is the only colliding dimension.
|
||
reviewer: Object.assign(validLane(), {
|
||
slug: `group-slug-${groupIdx}`,
|
||
flags: [`--${id}`],
|
||
reviewsSection: `Section ${id}`,
|
||
}),
|
||
}]);
|
||
}
|
||
});
|
||
|
||
const laneErrors = (ordered) => validateCrossCapability(new Map(ordered), new Set())
|
||
.filter((e) => e.startsWith('reviewer '));
|
||
|
||
// A deterministic permutation derived from the generated seed, so the
|
||
// property covers arbitrary orderings rather than only forward/reverse.
|
||
const shuffled = [...entries];
|
||
shuffleSeed.forEach((n, i) => {
|
||
const j = n % shuffled.length;
|
||
const k = (i + n) % shuffled.length;
|
||
[shuffled[j], shuffled[k]] = [shuffled[k], shuffled[j]];
|
||
});
|
||
|
||
const forward = laneErrors(entries);
|
||
const reversed = laneErrors([...entries].reverse());
|
||
const permuted = laneErrors(shuffled);
|
||
|
||
assert.deepEqual(reversed, forward, 'lane errors must not depend on Map insertion order (reversed)');
|
||
assert.deepEqual(permuted, forward, 'lane errors must not depend on Map insertion order (permuted)');
|
||
|
||
// One error per colliding group, naming every claimant.
|
||
const expectedCollisions = groupSizes.filter((s) => s > 1).length;
|
||
assert.equal(
|
||
forward.length, expectedCollisions,
|
||
`expected one error per colliding group, got: ${JSON.stringify(forward)}`,
|
||
);
|
||
return true;
|
||
},
|
||
),
|
||
{ numRuns: 200 },
|
||
);
|
||
});
|
||
|
||
test('absentReviewerBodyNeverContributesErrors', () => {
|
||
fc.assert(
|
||
fc.property(
|
||
fc.dictionary(fc.string(), fc.anything()),
|
||
(dict) => {
|
||
delete dict.reviewer; // guarantee absence regardless of low-probability random key collision
|
||
const capUndefinedKey = { ...dict, reviewer: undefined };
|
||
const capDeletedKey = { ...dict };
|
||
const errsUndefinedKey = validateReviewerBody(capUndefinedKey);
|
||
const errsDeletedKey = validateReviewerBody(capDeletedKey);
|
||
assert.deepEqual(errsUndefinedKey, errsDeletedKey, 'an explicit undefined must behave identically to an absent key');
|
||
assert.deepEqual(errsUndefinedKey, [], 'D4.1 — absence must never be an error');
|
||
return true;
|
||
},
|
||
),
|
||
{ numRuns: 200 },
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── K. Removed `hostBehaviors.reviewerCli` alias (ADR-2782 D9, chore #2801) ──
|
||
//
|
||
// Phase 7 deletes the derived legacy alias. `collectReviewerWarnings` is the
|
||
// channel the removal announces itself on, because it is already wired to BOTH
|
||
// surfaces a manifest can arrive through: the build-time generator
|
||
// (`gen-capability-registry.cjs` -> stderr) and the overlay loader
|
||
// (`capability-loader.cts` -> OverlayMeta.warnings, on the ACCEPT path for every
|
||
// accepted capability). An out-of-tree manifest still setting the alias reaches
|
||
// the second one.
|
||
//
|
||
// Rows K1-K9 implement W1-W9 of
|
||
// `.msd/phase/chore-2801-remove-reviewercli-alias/50-test-matrix.md`.
|
||
//
|
||
// The load-bearing structural fact these rows pin down: the removal check must
|
||
// run BEFORE `collectReviewerWarningRecordFields`' `reviewer`-body early-return. An
|
||
// alias-only manifest — precisely the case the deprecation window existed for —
|
||
// has no `reviewer` body, so a check placed after that guard would fire only for
|
||
// capabilities that do not need it. K1 is the row that fails if it is misplaced.
|
||
|
||
/** A whole runtime manifest — the shape production passes to this function. */
|
||
function runtimeCapWithHostBehaviors(hostBehaviors, extra = {}) {
|
||
return {
|
||
id: 'legacy-cli',
|
||
role: 'runtime',
|
||
runtime: { hostBehaviors },
|
||
...extra,
|
||
};
|
||
}
|
||
|
||
describe('K. Removed hostBehaviors.reviewerCli alias (#2801)', () => {
|
||
/** Records for the removal notice only, keyed on the typed code. */
|
||
function removalRecords(cap) {
|
||
return collectReviewerWarningRecords(cap)
|
||
.filter((rec) => rec.code === REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR);
|
||
}
|
||
|
||
test('reviewerWarningCodeSurfaceIsLocked', () => {
|
||
// The third of the three coordinated changes a new code requires. Without
|
||
// this, a code can be added or renamed with no test noticing.
|
||
assert.deepEqual(
|
||
Object.keys(REVIEWER_WARNING).sort(),
|
||
['REMOVED_HOST_BEHAVIOR', 'UNKNOWN_HOST_BEHAVIOR', 'UNKNOWN_REVIEWER_FIELD'],
|
||
);
|
||
assert.equal(Object.isFrozen(REVIEWER_WARNING), true, 'the code enum must be frozen');
|
||
assert.equal(REMOVED_REVIEWER_CLI_FIELD, 'runtime.hostBehaviors.reviewerCli');
|
||
});
|
||
|
||
test('removedReviewerCliAliasWarnsWhenPresentWithoutABody', () => {
|
||
// No `reviewer` body at all — the alias-only manifest. This is the row that
|
||
// proves the check runs before the body early-return.
|
||
const cap = runtimeCapWithHostBehaviors({ reviewerCli: true });
|
||
const records = removalRecords(cap);
|
||
assert.equal(
|
||
records.length, 1,
|
||
`expected exactly one removal record for an alias-only manifest, got: ${JSON.stringify(collectReviewerWarningRecords(cap))}`,
|
||
);
|
||
assert.equal(records[0].capId, 'legacy-cli');
|
||
assert.equal(records[0].field, REMOVED_REVIEWER_CLI_FIELD);
|
||
});
|
||
|
||
test('removedReviewerCliAliasWarnsAlongsideADeclaredBody', () => {
|
||
const cap = runtimeCapWithHostBehaviors({ reviewerCli: true }, { reviewer: validLane() });
|
||
assert.equal(removalRecords(cap).length, 1, 'a declared body must not suppress the removal notice');
|
||
assert.deepEqual(
|
||
validateReviewerBody(cap), [],
|
||
'the vestigial key must stay a WARNING — never a validation error (Postel: liberal in what we accept)',
|
||
);
|
||
});
|
||
|
||
test('removedReviewerCliAliasWarnsRegardlessOfItsValue', () => {
|
||
// Presence-based, deliberately: after removal the key is unknown at ANY
|
||
// value, exactly as an unknown `reviewer.*` field is. A value-sensitive
|
||
// warning would tell an author carrying `reviewerCli: false` that their
|
||
// stale key is fine, when it is simply dead.
|
||
// (40-design.md -> Rejected 3.)
|
||
for (const value of [true, false, 'true', 0, 1, null, {}, []]) {
|
||
const cap = runtimeCapWithHostBehaviors({ reviewerCli: value });
|
||
assert.equal(
|
||
removalRecords(cap).length, 1,
|
||
`expected a removal record for reviewerCli = ${JSON.stringify(value)}, got: ${JSON.stringify(collectReviewerWarningRecords(cap))}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('similarlyNamedHostBehaviorKeysAreNotTheRemovedField', () => {
|
||
// Exact own-key match only: a near-miss name must never be reported as the
|
||
// removed `reviewerCli`. Since #2801 closed the vocabulary these names DO
|
||
// now draw an unknown-host-behavior notice, which is correct — they are not
|
||
// declared behaviors — but they must not draw the removal notice.
|
||
const cap = runtimeCapWithHostBehaviors({
|
||
reviewerCliPath: '/usr/bin/thing',
|
||
reviewer_cli: true,
|
||
reviewerCLI: true,
|
||
reapplyCommand: 'x',
|
||
});
|
||
const records = collectReviewerWarningRecords(cap);
|
||
assert.deepEqual(
|
||
records.filter((rec) => rec.code === REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR), [],
|
||
'only the exact own key `reviewerCli` is the removed field',
|
||
);
|
||
assert.deepEqual(
|
||
records.map((rec) => rec.field).sort(),
|
||
[
|
||
'runtime.hostBehaviors.reviewerCLI',
|
||
'runtime.hostBehaviors.reviewer_cli',
|
||
'runtime.hostBehaviors.reviewerCliPath',
|
||
].sort(),
|
||
'the three undeclared names draw an unknown-host-behavior notice; the declared reapplyCommand does not',
|
||
);
|
||
});
|
||
|
||
test('malformedHostBehaviorsNeitherWarnsNorThrows', () => {
|
||
const shapes = [
|
||
['null', { id: 'c', role: 'runtime', runtime: { hostBehaviors: null } }],
|
||
['array', { id: 'c', role: 'runtime', runtime: { hostBehaviors: [] } }],
|
||
['string', { id: 'c', role: 'runtime', runtime: { hostBehaviors: 'reviewerCli' } }],
|
||
['number', { id: 'c', role: 'runtime', runtime: { hostBehaviors: 42 } }],
|
||
['empty object', { id: 'c', role: 'runtime', runtime: { hostBehaviors: {} } }],
|
||
['no hostBehaviors', { id: 'c', role: 'runtime', runtime: {} }],
|
||
['no runtime', { id: 'c', role: 'reviewer', reviewer: validLane() }],
|
||
['runtime null', { id: 'c', role: 'runtime', runtime: null }],
|
||
];
|
||
for (const [name, cap] of shapes) {
|
||
let records;
|
||
try {
|
||
records = collectReviewerWarningRecords(cap);
|
||
} catch (err) {
|
||
assert.fail(`collectReviewerWarningRecords threw for ${name}: ${err && err.message}`);
|
||
}
|
||
assert.deepEqual(
|
||
records.filter((rec) => rec.code === REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR), [],
|
||
`${name} must not produce a removal record`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('removalWarningAndUnknownFieldWarningCoexist', () => {
|
||
// Two independent diagnostics on one manifest. Neither may swallow the other
|
||
// — an early `return` after the first would hide the second.
|
||
const lane = validLane();
|
||
lane.futureField = 'from-a-newer-msd';
|
||
const cap = runtimeCapWithHostBehaviors({ reviewerCli: true }, { id: 'both-cap', reviewer: lane });
|
||
|
||
const records = collectReviewerWarningRecords(cap);
|
||
assert.deepEqual(
|
||
records.map((rec) => rec.code).sort(),
|
||
[REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR, REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD].sort(),
|
||
`expected exactly one of each code, got: ${JSON.stringify(records)}`,
|
||
);
|
||
});
|
||
|
||
test('inheritedReviewerCliFromPrototypeDoesNotWarn', () => {
|
||
// Own-key read: a polluted prototype must not manufacture a removal record
|
||
// on every otherwise-innocent manifest.
|
||
const polluted = Object.create({ reviewerCli: true });
|
||
polluted.reapplyCommand = 'x';
|
||
const cap = runtimeCapWithHostBehaviors(polluted);
|
||
assert.deepEqual(
|
||
collectReviewerWarningRecords(cap), [],
|
||
'an inherited reviewerCli is not a declared field',
|
||
);
|
||
});
|
||
|
||
test('removalWarningNamesTheReviewerBodyReplacement', () => {
|
||
// A removal notice that does not say what to do instead is not a migration
|
||
// path. The field was undocumented for its whole life and only documented at
|
||
// 1.9.0 as ALREADY deprecated, so we cannot enumerate who depends on it
|
||
// (Hyrum) — the exit has to carry its own instructions. Asserted on the
|
||
// typed fields, never on the rendered sentence.
|
||
const [record] = removalRecords(runtimeCapWithHostBehaviors({ reviewerCli: true }));
|
||
assert.ok(record, 'expected a removal record');
|
||
assert.equal(record.replacement, 'reviewer');
|
||
assert.equal(record.docs, 'docs/how-to/ship-a-reviewer-lane.md');
|
||
});
|
||
|
||
test('renderedStringsStayOneToOneWithRecords', () => {
|
||
// The two production consumers still receive strings; the renderer must not
|
||
// drop or duplicate a diagnostic.
|
||
const lane = validLane();
|
||
lane.futureField = 'x';
|
||
for (const cap of [
|
||
runtimeCapWithHostBehaviors({ reviewerCli: true }),
|
||
runtimeCapWithHostBehaviors({ reviewerCli: true }, { reviewer: lane }),
|
||
runtimeCapWithHostBehaviors({ reapplyCommand: 'x' }),
|
||
]) {
|
||
const records = collectReviewerWarningRecords(cap);
|
||
const strings = collectReviewerWarnings(cap);
|
||
assert.equal(strings.length, records.length);
|
||
assert.deepEqual(strings, records.map((rec) => rec.message));
|
||
}
|
||
});
|
||
|
||
test('collectReviewerWarningsStaysTotalOverTheNewHostBehaviorsReadPath', () => {
|
||
// W9 — the totality contract (#1461 OVL-1) now covers a second read path.
|
||
// A throwing getter or Proxy trap fires on the READ, before any message is
|
||
// built, so only the structural wrapper can save these. Both the IR and the
|
||
// renderer must survive, since the renderer maps over the IR.
|
||
const throwing = () => { throw new Error('boom'); };
|
||
|
||
const hostBehaviorsGetterThrows = { id: 'x', role: 'runtime', runtime: {} };
|
||
Object.defineProperty(hostBehaviorsGetterThrows.runtime, 'hostBehaviors', { get: throwing });
|
||
|
||
const reviewerCliGetterThrows = { id: 'x', role: 'runtime', runtime: { hostBehaviors: {} } };
|
||
Object.defineProperty(reviewerCliGetterThrows.runtime.hostBehaviors, 'reviewerCli', { get: throwing });
|
||
|
||
const cases = [
|
||
['runtime getter throws', Object.defineProperty({ id: 'x' }, 'runtime', { get: throwing })],
|
||
['hostBehaviors getter throws', hostBehaviorsGetterThrows],
|
||
['reviewerCli getter throws', reviewerCliGetterThrows],
|
||
['hostBehaviors Proxy traps throw', {
|
||
id: 'x',
|
||
role: 'runtime',
|
||
runtime: { hostBehaviors: new Proxy({}, { has: throwing, get: throwing, getOwnPropertyDescriptor: throwing, ownKeys: throwing }) },
|
||
}],
|
||
];
|
||
|
||
for (const [name, cap] of cases) {
|
||
let records;
|
||
let strings;
|
||
try {
|
||
records = collectReviewerWarningRecords(cap);
|
||
strings = collectReviewerWarnings(cap);
|
||
} catch (err) {
|
||
assert.fail(`${name}: threw ${err && err.message}`);
|
||
}
|
||
assert.ok(Array.isArray(records), `${name}: records must always be an array`);
|
||
assert.ok(Array.isArray(strings), `${name}: strings must always be an array`);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ─── L. Closed `hostBehaviors` vocabulary (ADR-1016, closed by #2801) ────────
|
||
|
||
describe('L. Closed hostBehaviors vocabulary (#2801)', () => {
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
|
||
/** Every hostBehaviors key the shipped manifests actually declare. */
|
||
function shippedHostBehaviorKeys() {
|
||
const keys = new Set();
|
||
const capsDir = path.join(ROOT, 'capabilities');
|
||
for (const dir of fs.readdirSync(capsDir, { withFileTypes: true })) {
|
||
if (!dir.isDirectory()) continue;
|
||
const file = path.join(capsDir, dir.name, 'capability.json');
|
||
if (!fs.existsSync(file)) continue;
|
||
const cap = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||
const hb = cap && cap.runtime && cap.runtime.hostBehaviors;
|
||
if (hb && typeof hb === 'object' && !Array.isArray(hb)) {
|
||
for (const key of Object.keys(hb)) keys.add(key);
|
||
}
|
||
}
|
||
return keys;
|
||
}
|
||
|
||
test('vocabularyExactlyMatchesWhatTheShippedManifestsDeclare', () => {
|
||
// DEFECT.GENERATIVE-FIX: two surfaces, one truth. A key added to a manifest
|
||
// without being declared here would warn on every build; a key left here
|
||
// after its last manifest drops it is dead vocabulary. Both directions fail.
|
||
const shipped = shippedHostBehaviorKeys();
|
||
assert.deepEqual(
|
||
[...shipped].sort(), [...KNOWN_HOST_BEHAVIORS].sort(),
|
||
'the closed vocabulary and the shipped manifests must name the same keys',
|
||
);
|
||
});
|
||
|
||
test('noShippedCapabilityDrawsAHostBehaviorWarning', () => {
|
||
// The closure must be inert for everything that ships today. If this fails,
|
||
// closing the vocabulary broke a real capability rather than a hypothetical one.
|
||
const capsDir = path.join(ROOT, 'capabilities');
|
||
const offenders = [];
|
||
for (const dir of fs.readdirSync(capsDir, { withFileTypes: true })) {
|
||
if (!dir.isDirectory()) continue;
|
||
const file = path.join(capsDir, dir.name, 'capability.json');
|
||
if (!fs.existsSync(file)) continue;
|
||
const cap = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||
for (const rec of collectReviewerWarningRecords(cap)) {
|
||
if (rec.code === REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR
|
||
|| rec.code === REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR) {
|
||
offenders.push(`${dir.name}: ${rec.field}`);
|
||
}
|
||
}
|
||
}
|
||
assert.deepEqual(offenders, [], `no shipped capability may draw a hostBehaviors notice, got: ${JSON.stringify(offenders)}`);
|
||
});
|
||
|
||
test('anUndeclaredHostBehaviorWarnsAndIsNotAnError', () => {
|
||
const cap = runtimeCapWithHostBehaviors({ someFutureSwitch: true });
|
||
const records = collectReviewerWarningRecords(cap);
|
||
assert.equal(records.length, 1, `expected one record, got: ${JSON.stringify(records)}`);
|
||
assert.equal(records[0].code, REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR);
|
||
assert.equal(records[0].field, 'runtime.hostBehaviors.someFutureSwitch');
|
||
// Forward-compat invariant: a warning, never a validation error.
|
||
assert.deepEqual(validateCapability({ ...cap, version: '1.0.0' }, cap.id).filter((e) => e.includes('someFutureSwitch')), []);
|
||
});
|
||
|
||
test('aDeclaredHostBehaviorIsSilentAtAnyValue', () => {
|
||
for (const value of [true, false, 'x', 0, null, {}, []]) {
|
||
const cap = runtimeCapWithHostBehaviors({ reapplyCommand: value });
|
||
assert.deepEqual(
|
||
collectReviewerWarningRecords(cap), [],
|
||
`a declared key must be silent regardless of value, got value ${JSON.stringify(value)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('theRemovedAliasDrawsItsOwnNoticeNotTheGenericOne', () => {
|
||
// reviewerCli is excluded from the unknown-key sweep on purpose: it has a
|
||
// migration pointer the generic notice does not carry, and two records for
|
||
// one key would be noise.
|
||
const records = collectReviewerWarningRecords(runtimeCapWithHostBehaviors({ reviewerCli: true }));
|
||
assert.equal(records.length, 1, `expected exactly one record, got: ${JSON.stringify(records)}`);
|
||
assert.equal(records[0].code, REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR);
|
||
assert.equal(records[0].replacement, 'reviewer');
|
||
});
|
||
|
||
test('reservedKeysInTheBagAreIgnoredNotWarned', () => {
|
||
const hostile = JSON.parse('{"__proto__": {"polluted": true}, "constructor": 1, "prototype": 2, "reapplyCommand": "x"}');
|
||
const cap = runtimeCapWithHostBehaviors(hostile);
|
||
let records;
|
||
try {
|
||
records = collectReviewerWarningRecords(cap);
|
||
} catch (err) {
|
||
assert.fail(`collectReviewerWarningRecords threw: ${err && err.message}`);
|
||
}
|
||
assert.deepEqual(records, [], 'reserved names are skipped, not reported as unknown behaviors');
|
||
assert.equal({}.polluted, undefined, 'Object.prototype must not be polluted');
|
||
});
|
||
});
|
||
|
||
// ─── M. Diagnostics are bounded and control-safe (#2801 review findings) ─────
|
||
//
|
||
// Both loops iterate MANIFEST-SUPPLIED keys. An installed third-party manifest
|
||
// is attacker-controlled and bounded only by MANIFEST_MAX_BYTES (8MB), so the
|
||
// record count and each key's rendered length must both have a ceiling, and a
|
||
// key must not be able to carry terminal escapes or a forged newline into
|
||
// stderr / OverlayMeta.warnings.
|
||
|
||
describe('M. Diagnostics are bounded and control-safe (#2801)', () => {
|
||
function manyUnknownHostBehaviors(n) {
|
||
const hb = {};
|
||
for (let i = 0; i < n; i += 1) hb['undeclaredKey' + i] = true;
|
||
return runtimeCapWithHostBehaviors(hb);
|
||
}
|
||
|
||
test('unknownHostBehaviorRecordsAreCappedWithASummary', () => {
|
||
const n = MAX_REPORTED_UNKNOWN_KEYS + 25;
|
||
const records = collectReviewerWarningRecords(manyUnknownHostBehaviors(n));
|
||
assert.equal(
|
||
records.length, MAX_REPORTED_UNKNOWN_KEYS + 1,
|
||
`expected ${MAX_REPORTED_UNKNOWN_KEYS} records plus one summary, got ${records.length}`,
|
||
);
|
||
const summary = records[records.length - 1];
|
||
assert.equal(summary.truncated, true);
|
||
assert.equal(summary.omittedCount, 25);
|
||
assert.equal(summary.field, 'runtime.hostBehaviors');
|
||
});
|
||
|
||
test('exactlyAtTheCapThereIsNoSummaryRecord', () => {
|
||
// limit-1 / limit / limit+1 around the ceiling.
|
||
const below = collectReviewerWarningRecords(manyUnknownHostBehaviors(MAX_REPORTED_UNKNOWN_KEYS - 1));
|
||
assert.equal(below.length, MAX_REPORTED_UNKNOWN_KEYS - 1);
|
||
assert.equal(below.some((rec) => rec.truncated), false);
|
||
|
||
const at = collectReviewerWarningRecords(manyUnknownHostBehaviors(MAX_REPORTED_UNKNOWN_KEYS));
|
||
assert.equal(at.length, MAX_REPORTED_UNKNOWN_KEYS);
|
||
assert.equal(at.some((rec) => rec.truncated), false, 'no summary when nothing was omitted');
|
||
|
||
const above = collectReviewerWarningRecords(manyUnknownHostBehaviors(MAX_REPORTED_UNKNOWN_KEYS + 1));
|
||
assert.equal(above.length, MAX_REPORTED_UNKNOWN_KEYS + 1);
|
||
assert.equal(above[above.length - 1].omittedCount, 1);
|
||
});
|
||
|
||
test('unknownReviewerFieldRecordsAreCappedTheSameWay', () => {
|
||
const lane = validLane();
|
||
for (let i = 0; i < MAX_REPORTED_UNKNOWN_KEYS + 5; i += 1) lane['futureField' + i] = 1;
|
||
const records = collectReviewerWarningRecords({ id: 'cap-x', reviewer: lane });
|
||
assert.equal(records.length, MAX_REPORTED_UNKNOWN_KEYS + 1);
|
||
assert.equal(records[records.length - 1].truncated, true);
|
||
assert.equal(records[records.length - 1].omittedCount, 5);
|
||
});
|
||
|
||
test('controlCharactersInAKeyNeverReachTheDiagnostic', () => {
|
||
// ESC-based colour sequence, a CR overwrite, and an embedded newline that
|
||
// would forge a second log line.
|
||
const hostile = '\x1b[31mred\x1b[0m\r\nforged: everything is fine';
|
||
for (const cap of [
|
||
runtimeCapWithHostBehaviors({ [hostile]: true }),
|
||
{ id: 'cap-x', reviewer: { slug: 'x', [hostile]: true } },
|
||
]) {
|
||
for (const rec of collectReviewerWarningRecords(cap)) {
|
||
// eslint-disable-next-line no-control-regex
|
||
assert.equal(/[\x00-\x1f\x7f-\x9f]/.test(rec.field), false, `control char survived into field: ${JSON.stringify(rec.field)}`);
|
||
// eslint-disable-next-line no-control-regex
|
||
assert.equal(/[\x00-\x1f\x7f-\x9f]/.test(rec.message), false, `control char survived into message: ${JSON.stringify(rec.message)}`);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('anEnormousKeyNameIsClipped', () => {
|
||
const huge = 'k'.repeat(5000);
|
||
const [record] = collectReviewerWarningRecords(runtimeCapWithHostBehaviors({ [huge]: true }));
|
||
assert.ok(record, 'expected a record');
|
||
assert.ok(
|
||
record.field.length < MAX_REPORTED_KEY_CHARS + 40,
|
||
`field must be bounded, got length ${record.field.length}`,
|
||
);
|
||
assert.ok(record.field.endsWith('…'), 'a clipped key is marked as clipped');
|
||
});
|
||
|
||
test('aDeclaredKeyIsNeverClippedOrAltered', () => {
|
||
// The sanitizer must not perturb the ordinary case: declared keys are silent,
|
||
// and an undeclared but well-formed key is reported verbatim.
|
||
const records = collectReviewerWarningRecords(runtimeCapWithHostBehaviors({ someFutureSwitch: true }));
|
||
assert.equal(records.length, 1);
|
||
assert.equal(records[0].field, 'runtime.hostBehaviors.someFutureSwitch');
|
||
});
|
||
});
|