* fix(#4709): retire the Gemini CLI reviewer lane
Google stopped serving Gemini CLI for the free/Pro/Ultra tiers on 2026-06-18 —
the same sunset that removed the gemini RUNTIME in #1928 (shipped 1.8.0). GSD
targets solo developers, so those tiers ARE the user path: the lane spawned
`gemini {{model}} -p -`, a binary that no longer answers for the majority of
users, and five locales documented it as a supported choice.
The lane was re-created after #1928 by the reviewer-lane-as-manifest-data work
(6a9babda69, #2798/#2837, ADR-2782). Per the maintainer that re-creation was an
error in that buildout rather than a considered decision, so this corrects a
mistake and needs no ADR-2782 amendment.
Reviewer roster: 12 lanes / 13 flags -> 11 lanes / 12 flags.
TWO sources of truth had to be removed, not one. Deleting
capabilities/gemini/capability.json left the capability registry at 11 lanes
while src/review-lane-descriptor.cts's hand-maintained REVIEWER_LANES array
still carried its own complete gemini entry at 12 — precisely the disagreement
checkReviewerLaneParity exists to catch. Both are gone; both parity checkers
now run clean against the real tree (lane parity ok/0 violations, docs parity
0 violations).
Surfaces stripped of the dead flag:
- capabilities/gemini/ deleted; registry and capability-matrix regenerated
- src/review-lane-descriptor.cts: REVIEWER_LANES entry, docblock count, and the
three doc comments that used --gemini as a live example
- commands/gsd/{review,plan-review-convergence,autonomous,progress}.md and the
four matching skills/*/SKILL.md: argument-hint frontmatter and flag bullets
- gsd-core/workflows/help/modes/{full,full.compact}.md: /gsd-help signatures,
the detected-CLI list, and the reviewer-title list
- gsd-core/workflows/settings-integrations.md: the integrations wizard no longer
offers "Gemini" as a model option, and the settable-keys list drops it
- gsd-core/workflows/review.md: the `command -v gemini` probe, the --gemini
flag, the roster frontmatter, the install pointer to the sunset repo, and the
jq-less / precedence / self-skip lane lists
- gsd-core/workflows/sync-skills.md: "two runtimes (grok, gemini) resolve to
ANOTHER runtime's skills root" is now one runtime; gemini never aliased
anything, it fell through canonicalizeRuntimeName to a fail-closed default
- docs/{CONFIGURATION,COMMANDS,CLI-TOOLS}.md, docs/reference/capability-matrix.md,
docs/how-to/set-up-cross-ai-review.md — including its `npm install -g
@google/gemini-cli` instruction and the two rows recommending --gemini
- docs/features/{cross-ai-peer-review,opt-in-parallel-reviewer-lanes}.md as the
generator inputs behind docs/FEATURES.md, plus the three locale FEATURES.md
signature lines the docs-parity gate covers (the #2781 class: a flag change
that never reaches the mirrors)
Counts reconciled against measurement rather than arithmetic: 8 timeout keys of
11 lanes, 11 budget keys, 9 model keys, and four hardcoded literals in
tests/reviewer-lane-declarations.test.cjs (NEW_LANE_ONLY_IDS 5->4, LITERAL_ROSTER
12->11, two roster counts 12->11).
BEHAVIOR CHANGE, accepted deliberately: `gsd config-set review.models.gemini`
now errors with "Unknown config key". An existing key already in
.planning/config.json still parses and is simply never read, so no project fails
to load. This is the repo's own documented policy for exactly this case
(docs/CONFIGURATION.md:327 — "a key left over from a removed reviewer validated
silently and was never read. Such a key is now rejected by config-set"), so no
installer migration ships. Note my first measurement of this was WRONG: I tested
config-get, which reads undeclared keys fine, and generalised. Read and write are
different surfaces and gave different answers.
Antigravity is untouched throughout — its --antigravity/--agy flags,
review.models.agy, ~/.gemini/antigravity configHome, ~/.gemini/config global
skills root (#3738), hookEvents "gemini", GEMINI.md instruction file, and every
gemini-* model id it actually runs on.
Refs #4709
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(#4709): changeset for the reviewer-lane retirement
Type Removed: the --gemini flag and its three config keys are user-visible
surface that no longer exists.
Refs #4709
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#4709): close the 24 test failures and the locale-doc gap the gates found
An adversarial review and a full matrix run between them found substantially
more fallout than inspection had. All of it is this PR's own, and all of it is
fixed rather than waved off.
THE MATRIX RUN FOUND 24 FAILURES ACROSS 6 FILES. Inspection had predicted two.
The dominant class was a test helper that looks up a lane by slug and throws
`no declared lane 'gemini'`:
- tests/feat-2483-review-claude-mds-guard.test.cjs (6) — used gemini as the
"other declared first-party lane" to contrast against claude's env
suppression. Now qwen, verified from source as a lane that declares no `env`
(only claude does), so the contrast still holds.
- tests/review-lane-descriptor.test.cjs (6) — the duplicate-flag and
duplicate-section fixtures deliberately COLLIDED with a real declared lane to
prove the parity checker reports a duplicate. `--gemini`/`Gemini` no longer
collide with anything, so the checker reported
`descriptor_lane_not_in_registry:acme` instead and the tests proved nothing.
Now collide with `--codex`/`Codex`, reproduced against the real checker.
- tests/review-reviewer-selection.test.cjs (3) — these distinguish KNOWN-but-
undetected from UNKNOWN. gemini flipped categories, inverting what they
proved. The known case now uses qwen; `__nope__` stays the unknown fixture.
- tests/review-default-reviewers-resolution.test.cjs (2), and
tests/settings-integrations.test.cjs (3) — the wizard now offers three
reviewer CLIs, not four, so the test and its name say three.
- Two count assertions the earlier sweep missed outright:
reviewer-lane-declarations.test.cjs:359 (`length, 12`) and
reviewer-docs-parity.test.cjs:681 (`>= 12`).
THE LOCALE-DOC GAP, and why the parity gate stayed green over it. All four
locale mirrors still documented `--gemini` as a live reviewer flag. The
docs-parity checker asserts the PRESENCE of every current flag and never the
ABSENCE of a retired one, so "0 violations" was never evidence those files were
clean — my earlier reading of it as such was wrong. This is the #2781
locale-drift class in the opposite direction. Fixed across 12 locale files:
COMMANDS.md flag lists and table rows, CONFIGURATION.md `review.models.gemini`
rows and reviewer prose, CLI-TOOLS.md config examples, and
set-up-cross-ai-review.md including its install block and its
which-reviewer-to-choose row, which now recommends Antigravity.
ALSO FOUND, and instructive about my own method: docs/CONFIGURATION.md:297 still
carried a `review.models.gemini` row. My sweep had missed it because my grep
excluded lines matching `gemini-[0-9]` to spare Google's model ids — and that
row's example value is `"gemini-2.5-pro"` on the same line. The exclusion built
to avoid false positives created a false negative.
Remaining comment/example sites: src/review-reviewer-selection.cts:309 and
src/config.cts:598 named the dead flag and key as examples;
gsd-core/references/planning-config.md:269 likewise; and
review-reviewer-selection.cts:22 claimed in the PRESENT tense that gemini is a
lane-only reviewer capability. Line 38 of that same docblock says "Before this
phase the five non-runtime reviewers (gemini, ...)" and is left exactly as is —
that is past-tense history, and rewriting it would falsify the record.
Deliberately still deferred to Phase 4, because it is the RUNTIME axis rather
than the reviewer lane: the locale install-on-your-runtime.md `--gemini --global`
instructions, the USER-GUIDE colon-form notes, and the ARCHITECTURE
runtime-detection flag lists.
Both parity checkers green against the real tree; lint:ci exit 0.
Refs #4709
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(#4709): backfill the changeset PR number
pr: 0 -> 4716, now that the PR exists. Never guessed ahead of the number.
Refs #4709
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
764 lines
37 KiB
JavaScript
764 lines
37 KiB
JavaScript
'use strict';
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
/**
|
|
* reviewer-lane-declarations.test.cjs — behavioral tests for ADR-2782 Phase 5a
|
|
* (chore #2798): declaring the eleven existing reviewer lanes as capability-
|
|
* manifest data.
|
|
*
|
|
* Implements every row carrying a Test name in
|
|
* `.gsd/phase/chore-2798-declare-reviewer-lanes/50-test-matrix.md` (sections
|
|
* A-E). See that phase's `40-design.md` for the behavior table the matrix
|
|
* derives from. Test names are copied verbatim from the matrix.
|
|
*
|
|
* AMENDED by ADR-2782 Phase 7 (chore #2801), which removes the
|
|
* `runtime.hostBehaviors.reviewerCli` derived legacy alias. Rows that asserted
|
|
* the alias still contributed a slug are inverted here rather than deleted, so
|
|
* the file keeps a guard against reintroduction. See
|
|
* `.gsd/phase/chore-2801-remove-reviewercli-alias/50-test-matrix.md` rows
|
|
* B1 and C1-C10, P1.
|
|
*
|
|
* THE SINGLE MOST IMPORTANT PROPERTY (matrix "Red-before-green"): the roster is
|
|
* eleven slugs before this phase and eleven after, with IDENTICAL membership —
|
|
* C1 is the keystone, asserted against a LITERAL list, never against a value
|
|
* computed by the same machinery under test. E1 is the highest-value row: it
|
|
* asserts the manifest and `src/review-lane-descriptor.cts`'s `REVIEWER_LANES`
|
|
* describe the same eleven lanes field-for-field — the whole premise of
|
|
* ADR-2782 is that there is no translation layer between the two surfaces.
|
|
*
|
|
* Level choice, per the matrix's own "Units" note: the eleven
|
|
* `capabilities/*\/capability.json` files are validated through the existing
|
|
* registry generator (`loadAndValidate` / `validateCrossCapability` / the
|
|
* generated `capability-registry.cjs`), never by reading JSON text — a
|
|
* source-grep assertion would be rejected by `local/no-source-grep` and would
|
|
* prove nothing about validity. `src/review-reviewer-selection.cts`'s roster
|
|
* derivation (`deriveReviewerSlugs`) is exercised directly against synthetic
|
|
* registries for the C-section rows that need to isolate one input class at a
|
|
* time (Independence: each test builds its own fixture; no shared mutable
|
|
* state).
|
|
*
|
|
* `SHIPPED` below is the one deliberate exception to "each test builds its own
|
|
* fixture": it is the REAL, already-validated capability set (computed once,
|
|
* read-only — no test mutates `SHIPPED.capMap` or any capability object
|
|
* inside it), reused across the A/B/D/E rows that assert against the actual
|
|
* shipped repo rather than a synthetic input class. Recomputing it per test
|
|
* would re-scan and re-validate all of `capabilities/*` on every one of those
|
|
* rows for no behavioral benefit.
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const fc = require('fast-check');
|
|
|
|
const {
|
|
loadAndValidate,
|
|
validateCapability,
|
|
validateCrossCapability,
|
|
deriveProfileMembership,
|
|
deriveCapabilityClusters,
|
|
} = require('../scripts/gen-capability-registry.cjs');
|
|
|
|
const {
|
|
KEBAB_RE,
|
|
KNOWN_REVIEWER_FIELDS,
|
|
} = require('../gsd-core/bin/lib/capability-validator.cjs');
|
|
|
|
const {
|
|
REVIEWER_LANES,
|
|
checkReviewerLaneParity,
|
|
} = require('../gsd-core/bin/lib/review-lane-descriptor.cjs');
|
|
|
|
// Kept as a whole-module reference (rather than only destructuring) so C5 can
|
|
// assert on the module's OWN export surface, not just the names we happen to use.
|
|
const reviewerSelectionModule = require('../gsd-core/bin/lib/review-reviewer-selection.cjs');
|
|
const {
|
|
KNOWN_REVIEWER_SLUGS,
|
|
deriveReviewerSlugs,
|
|
resolveReviewerSelection,
|
|
} = reviewerSelectionModule;
|
|
|
|
// Generated registry (ADR-894) — `.capabilities` is keyed by capability id;
|
|
// each value is the whole manifest object, exactly as loaded from disk.
|
|
const capabilityRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
|
|
/**
|
|
* The four net-new lane-only `role:"reviewer"` capabilities (ADR-2782 D3).
|
|
*
|
|
* Was five: `gemini` was retired by #4709 (Google sunset Gemini CLI 2026-06-18, the same sunset
|
|
* that removed the gemini RUNTIME in #1928/1.8.0), so `capabilities/gemini/` no longer exists.
|
|
*/
|
|
const NEW_LANE_ONLY_IDS = ['coderabbit', 'ollama', 'lm-studio', 'llama-cpp'];
|
|
|
|
/** The six pre-existing dual-purpose `role:"runtime"` capabilities. */
|
|
const RUNTIME_REVIEWER_IDS = ['antigravity', 'claude', 'codex', 'cursor', 'opencode', 'qwen'];
|
|
|
|
/**
|
|
* The shipped roster BEFORE this phase, as a literal (not computed) sorted
|
|
* list. C1 compares `KNOWN_REVIEWER_SLUGS` against THIS, never against a
|
|
* value produced by `deriveReviewerSlugs` itself — a self-consistent-but-wrong
|
|
* refactor would otherwise sail through.
|
|
*/
|
|
const LITERAL_ROSTER = [
|
|
'antigravity', 'claude', 'coderabbit', 'codex', 'cursor',
|
|
// `kimi-code` joined in Phase 5b (#2799, closes #2718) — see
|
|
// kimiCodeIsDeclaredAndInvocableInThisPhase for why it landed here and not in 5a.
|
|
'kimi-code',
|
|
'llama_cpp', 'lm_studio', 'ollama', 'opencode', 'qwen',
|
|
];
|
|
|
|
/**
|
|
* The real, already-validated capability set. Read-only; see the file header
|
|
* for why this is shared instead of rebuilt per test. `new Set()` for central
|
|
* keys skips config-schema collision detection — irrelevant to this phase's
|
|
* rows and not part of what any of them assert (same choice the existing
|
|
* `shippedRegistryOutputIsUnchangedByHarvestWidening` test makes).
|
|
*/
|
|
const SHIPPED = loadAndValidate(new Set());
|
|
|
|
// ─── A. The five new lane-only capabilities ─────────────────────────────────
|
|
|
|
describe('A. The five new lane-only capabilities', () => {
|
|
test('newLaneOnlyCapabilitiesValidate', () => {
|
|
assert.deepEqual(
|
|
SHIPPED.errors, [],
|
|
`expected the shipped capability set to validate cleanly, got: ${JSON.stringify(SHIPPED.errors)}`,
|
|
);
|
|
for (const id of NEW_LANE_ONLY_IDS) {
|
|
assert.ok(SHIPPED.capMap.has(id), `expected capability "${id}" to be loaded from capabilities/${id}/`);
|
|
}
|
|
});
|
|
|
|
test('newLaneCapabilitiesUseTheReviewerRole', () => {
|
|
for (const id of NEW_LANE_ONLY_IDS) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
assert.equal(cap.role, 'reviewer', `capability "${id}" must declare role:"reviewer"`);
|
|
}
|
|
});
|
|
|
|
test('newLaneCapabilitiesDeclareAReviewerBody', () => {
|
|
for (const id of NEW_LANE_ONLY_IDS) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
assert.ok(
|
|
cap.reviewer && typeof cap.reviewer === 'object' && !Array.isArray(cap.reviewer),
|
|
`capability "${id}" must carry a "reviewer" body`,
|
|
);
|
|
assert.ok(
|
|
typeof cap.reviewer.slug === 'string' && cap.reviewer.slug.length > 0,
|
|
`capability "${id}" reviewer body must declare a non-empty slug`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('laneOnlyCapabilitiesHaveNoRuntimeBody', () => {
|
|
// A runtime body would be a validation ERROR for role:"reviewer" — these
|
|
// are not install targets (design 40-design.md "A3").
|
|
for (const id of NEW_LANE_ONLY_IDS) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
assert.equal('runtime' in cap, false, `lane-only capability "${id}" must not carry a "runtime" body`);
|
|
}
|
|
});
|
|
|
|
test('laneOnlyCapabilitiesNeedNoRuntimeCompat', () => {
|
|
for (const id of NEW_LANE_ONLY_IDS) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
assert.equal('runtimeCompat' in cap, false, `lane-only capability "${id}" must not declare runtimeCompat`);
|
|
// The absence must not cost it validity (D3: not required for role:"reviewer").
|
|
const errs = validateCapability(cap, id);
|
|
assert.deepEqual(
|
|
errs, [],
|
|
`capability "${id}" without runtimeCompat must still validate cleanly, got: ${JSON.stringify(errs)}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('laneOnlyCapabilitiesContributeNoArtifacts', () => {
|
|
// No install surface, nothing to install: none of the fields buildRegistry
|
|
// harvests for a feature capability's install/loop-point surface.
|
|
for (const id of NEW_LANE_ONLY_IDS) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
for (const field of ['skills', 'agents', 'steps', 'gates', 'contributions']) {
|
|
const value = cap[field];
|
|
assert.ok(
|
|
value === undefined || (Array.isArray(value) && value.length === 0),
|
|
`lane-only capability "${id}" must contribute no ${field}, got: ${JSON.stringify(value)}`,
|
|
);
|
|
}
|
|
}
|
|
// Nothing routes through them at the registry level either.
|
|
const bySkillOwners = new Set(Object.values(capabilityRegistry.bySkill));
|
|
const byAgentOwners = new Set(Object.values(capabilityRegistry.byAgent));
|
|
for (const id of NEW_LANE_ONLY_IDS) {
|
|
assert.equal(bySkillOwners.has(id), false, `"${id}" must own no skill in the generated registry`);
|
|
assert.equal(byAgentOwners.has(id), false, `"${id}" must own no agent in the generated registry`);
|
|
}
|
|
});
|
|
|
|
test('laneCapabilityIdsAreKebabWhileSlugsMaySnake', () => {
|
|
// The build-breaking trap (ADR-2782's three-namespace trap): the epic body
|
|
// and #2798 both literally specified `capabilities/lm_studio/` and
|
|
// `capabilities/llama_cpp/`, which fail KEBAB_RE outright — the folder/id
|
|
// MUST be kebab while `reviewer.slug` keeps the shipped roster's snake form.
|
|
const kebabIdToSnakeSlug = { 'lm-studio': 'lm_studio', 'llama-cpp': 'llama_cpp' };
|
|
for (const [id, expectedSlug] of Object.entries(kebabIdToSnakeSlug)) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
assert.ok(KEBAB_RE.test(id), `capability id "${id}" must satisfy KEBAB_RE (the folder-name grammar)`);
|
|
assert.equal(cap.reviewer.slug, expectedSlug, `capability "${id}" reviewer.slug must be "${expectedSlug}"`);
|
|
assert.equal(
|
|
KEBAB_RE.test(cap.reviewer.slug), false,
|
|
`reviewer.slug "${cap.reviewer.slug}" must NOT satisfy KEBAB_RE — it deliberately differs from the kebab id`,
|
|
);
|
|
// The rejected alternative the epic literally specified as a folder name.
|
|
assert.equal(
|
|
KEBAB_RE.test(expectedSlug), false,
|
|
`"${expectedSlug}" would fail KEBAB_RE as a folder/id — that is exactly the trap this row guards`,
|
|
);
|
|
}
|
|
// The other two new capabilities are single-word and unaffected: id === slug.
|
|
for (const id of ['coderabbit', 'ollama']) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
assert.ok(KEBAB_RE.test(id), `capability id "${id}" must satisfy KEBAB_RE`);
|
|
assert.equal(cap.reviewer.slug, id, `single-word capability "${id}" must have a matching slug`);
|
|
}
|
|
});
|
|
|
|
test('laneOnlyCapabilitiesReceiveNoProfileMembership', () => {
|
|
// tier is required (source of truth for the requires-closure), but with no
|
|
// skills, deriveProfileMembership/deriveCapabilityClusters skip them —
|
|
// membership is computed and inert, not simply "not applicable".
|
|
const profileMembership = deriveProfileMembership(SHIPPED.capMap);
|
|
const capabilityClusters = deriveCapabilityClusters(SHIPPED.capMap);
|
|
for (const id of NEW_LANE_ONLY_IDS) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
assert.ok(typeof cap.tier === 'string' && cap.tier.length > 0, `lane-only capability "${id}" must still declare a tier`);
|
|
assert.equal(id in profileMembership, false, `lane-only capability "${id}" must receive no profile membership`);
|
|
assert.equal(id in capabilityClusters, false, `lane-only capability "${id}" must own no cluster`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── B. The six existing runtime capabilities ───────────────────────────────
|
|
|
|
describe('B. The six existing runtime capabilities', () => {
|
|
test('dualPurposeRuntimesCarryAReviewerBody', () => {
|
|
for (const id of RUNTIME_REVIEWER_IDS) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
assert.equal(cap.role, 'runtime', `"${id}" must remain role:"runtime"`);
|
|
assert.ok(cap.runtime && typeof cap.runtime === 'object', `"${id}" must retain its runtime body`);
|
|
assert.ok(cap.reviewer && typeof cap.reviewer === 'object', `"${id}" must gain a reviewer body alongside it (D1)`);
|
|
const errs = validateCapability(cap, id);
|
|
assert.deepEqual(errs, [], `"${id}" carrying both bodies must validate cleanly, got: ${JSON.stringify(errs)}`);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Real per-capability snapshots of `cap.runtime`'s own KEY SET, captured at
|
|
* this phase's boundary (`git status` confirms these six files' only
|
|
* uncommitted change is the added `reviewer` key). Adding that sibling key
|
|
* must not add, remove, or rename anything inside `runtime` — a top-level
|
|
* key drift here means the edit that added `reviewer` also touched
|
|
* `runtime`, by accident or by a future careless merge of the two bodies.
|
|
*
|
|
* Deliberately a KEY-SET snapshot, not a full-content one: embedding all six
|
|
* ~15-70-field runtime bodies verbatim would duplicate six actively-edited
|
|
* install descriptors into the test as a second source of truth that drifts
|
|
* on every legitimate future runtime change (these are the most frequently
|
|
* touched capabilities in the repo). Value-level integrity is covered by
|
|
* `validateCapability` (schema-complete) below and by the no-cross-
|
|
* contamination check (no reviewer-only field name inside `runtime`, and
|
|
* vice versa) — together these catch "the sibling edit corrupted this
|
|
* body" without requiring line-for-line duplication.
|
|
*/
|
|
// #2871 Phase 2 added `triggerPrecedence` (required-with-default) to every
|
|
// runtime body — a real, deliberate key-set change, not drift. Reflected
|
|
// here in sorted position, same as every other key in these snapshots.
|
|
const EXPECTED_RUNTIME_KEYS = {
|
|
antigravity: ['artifactLayout', 'commandStyle', 'configFormat', 'configHome', 'extendedHookEvents', 'hookEvents', 'hooksSurface', 'hostBehaviors', 'hostIntegration', 'installSurface', 'localConfigDir', 'permissionWriter', 'sandboxTier', 'supportTier', 'triggerPrecedence', 'writesSharedSettings'],
|
|
claude: ['artifactLayout', 'commandStyle', 'configFormat', 'configHome', 'extendedHookEvents', 'harnessIsolationFlag', 'hookEvents', 'hooksSurface', 'hostBehaviors', 'hostIntegration', 'installSurface', 'localConfigDir', 'permissionWriter', 'sandboxTier', 'supportTier', 'triggerPrecedence', 'writesSharedSettings'],
|
|
codex: ['artifactLayout', 'commandStyle', 'configFormat', 'configHome', 'extendedHookEvents', 'hookEvents', 'hooksSurface', 'hostBehaviors', 'hostIntegration', 'installSurface', 'localConfigDir', 'orchestratorExec', 'permissionWriter', 'sandboxTier', 'supportTier', 'triggerPrecedence', 'writesSharedSettings'],
|
|
cursor: ['artifactLayout', 'commandStyle', 'configFormat', 'configHome', 'extendedHookEvents', 'harnessIsolationFlag', 'hookEvents', 'hooksSurface', 'hostBehaviors', 'hostIntegration', 'installSurface', 'localConfigDir', 'permissionWriter', 'sandboxTier', 'supportTier', 'triggerPrecedence', 'writesSharedSettings'],
|
|
opencode: ['artifactLayout', 'commandStyle', 'configFormat', 'configHome', 'extendedHookEvents', 'extensionEvents', 'hooksSurface', 'hostBehaviors', 'hostIntegration', 'installSurface', 'localConfigDir', 'orchestratorExec', 'permissionWriter', 'sandboxTier', 'supportTier', 'triggerPrecedence', 'writesSharedSettings'],
|
|
qwen: ['artifactLayout', 'commandStyle', 'configFormat', 'configHome', 'extendedHookEvents', 'hookEvents', 'hooksSurface', 'hostBehaviors', 'hostIntegration', 'installSurface', 'localConfigDir', 'permissionWriter', 'sandboxTier', 'supportTier', 'triggerPrecedence', 'writesSharedSettings'],
|
|
};
|
|
|
|
test('runtimeBodiesAreUnchangedByLaneDeclaration', () => {
|
|
for (const id of RUNTIME_REVIEWER_IDS) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
|
|
assert.deepEqual(
|
|
Object.keys(cap.runtime).sort(), EXPECTED_RUNTIME_KEYS[id],
|
|
`"${id}" runtime body's key set must be unperturbed by adding the sibling reviewer body`,
|
|
);
|
|
|
|
// No cross-contamination in either direction — a field from one body
|
|
// leaking into the other would be an install-behavior-changing defect
|
|
// that schema validation alone (which tolerates unknown fields via a
|
|
// warning, not an error) would not catch.
|
|
const runtimeKeys = new Set(Object.keys(cap.runtime));
|
|
for (const reviewerField of KNOWN_REVIEWER_FIELDS) {
|
|
assert.equal(
|
|
runtimeKeys.has(reviewerField), false,
|
|
`"${id}" runtime body must not contain reviewer-only field "${reviewerField}"`,
|
|
);
|
|
}
|
|
const reviewerKeys = Object.keys(cap.reviewer);
|
|
assert.ok(
|
|
reviewerKeys.every((k) => KNOWN_REVIEWER_FIELDS.has(k)),
|
|
`"${id}" reviewer body must contain only known reviewer fields, got: ${JSON.stringify(reviewerKeys)}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('reviewerCliAliasIsRemovedFromEveryShippedManifest', () => {
|
|
// Phase 7 (#2801): the deprecation window opened in 1.9.0 (Phase 5a) and
|
|
// 1.9.1 + 1.10.0 have since shipped, so the alias goes. Each of these six
|
|
// already declares a `reviewer` body whose slug equals its capability id, so
|
|
// removing the key costs none of them a lane — C1 is the proof.
|
|
for (const id of RUNTIME_REVIEWER_IDS) {
|
|
const cap = SHIPPED.capMap.get(id);
|
|
const hb = cap.runtime.hostBehaviors || {};
|
|
assert.equal(
|
|
Object.prototype.hasOwnProperty.call(hb, 'reviewerCli'), false,
|
|
`"${id}" must no longer declare hostBehaviors.reviewerCli (removed in Phase 7 / #2801); declare a reviewer body instead`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('vestigialAliasKeyDoesNotAddASecondSlug', () => {
|
|
// Post-#2801 this is no longer a PRECEDENCE rule (body beats alias) — the
|
|
// key is simply never read. Kept, with slug !== capId so a stray
|
|
// contribution would be observable as a distinct roster entry rather than
|
|
// hidden by an accidental string match, because an out-of-tree manifest can
|
|
// still carry the vestigial key for years.
|
|
const registry = {
|
|
capabilities: {
|
|
'dual-purpose-cap': {
|
|
role: 'runtime',
|
|
runtime: { hostBehaviors: { reviewerCli: true } },
|
|
reviewer: { slug: 'dual-slug' },
|
|
},
|
|
},
|
|
};
|
|
const roster = deriveReviewerSlugs(registry);
|
|
assert.equal(roster.length, 1, `expected exactly one contribution, not two, got: ${JSON.stringify(roster)}`);
|
|
assert.deepEqual(roster, ['dual-slug']);
|
|
assert.equal(roster.includes('dual-purpose-cap'), false, 'the removed alias key must not contribute the capability id');
|
|
});
|
|
});
|
|
|
|
// ─── C. Roster derivation — src/review-reviewer-selection.cts ─────────────
|
|
|
|
describe('C. Roster derivation — src/review-reviewer-selection.cts', () => {
|
|
test('rosterMembershipIsUnchangedByAliasRemoval', () => {
|
|
// KEYSTONE. This row is GREEN before and after #2801 — it is the invariant
|
|
// the phase must not break, not a red row. The literal list is never
|
|
// computed by the machinery under test.
|
|
assert.equal(KNOWN_REVIEWER_SLUGS.length, 11, 'roster must be exactly 11 — not 10, not 12');
|
|
assert.deepEqual(
|
|
[...KNOWN_REVIEWER_SLUGS].sort(), LITERAL_ROSTER,
|
|
`roster must be exactly the declared lane set, got: ${JSON.stringify(KNOWN_REVIEWER_SLUGS)}`,
|
|
);
|
|
});
|
|
|
|
test('declaredReviewerBodyContributesItsSlug', () => {
|
|
const registry = { capabilities: { 'my-cap': { role: 'reviewer', reviewer: { slug: 'my-lane' } } } };
|
|
assert.deepEqual(deriveReviewerSlugs(registry), ['my-lane']);
|
|
});
|
|
|
|
test('aliasOnlyCapabilityContributesNoSlug', () => {
|
|
// #2801 core inversion: no reviewer body, only the removed hostBehaviors
|
|
// flag. Before Phase 7 this contributed `legacy-cli`; now it contributes
|
|
// nothing, and `collectReviewerWarnings` says so (see section K of
|
|
// tests/reviewer-manifest-body.test.cjs).
|
|
const registry = {
|
|
capabilities: {
|
|
'legacy-cli': { role: 'runtime', runtime: { hostBehaviors: { reviewerCli: true } } },
|
|
},
|
|
};
|
|
assert.deepEqual(deriveReviewerSlugs(registry), []);
|
|
});
|
|
|
|
test('aliasWithAnyValueContributesNoSlug', () => {
|
|
// Value sweep: the old branch was a strict `=== true`, so `false`/`"true"`/`1`
|
|
// never contributed even before. Locking all of them at once means a partial
|
|
// revert that restores only the truthy branch is still caught.
|
|
for (const value of [true, false, 'true', 0, 1, null, {}, []]) {
|
|
const registry = {
|
|
capabilities: {
|
|
'legacy-cli': { role: 'runtime', runtime: { hostBehaviors: { reviewerCli: value } } },
|
|
},
|
|
};
|
|
assert.deepEqual(
|
|
deriveReviewerSlugs(registry), [],
|
|
`reviewerCli = ${JSON.stringify(value)} must contribute no slug`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('nonReviewerCapabilityContributesNoSlug', () => {
|
|
const registry = { capabilities: { 'plain-feature': { role: 'feature' } } };
|
|
assert.deepEqual(deriveReviewerSlugs(registry), []);
|
|
});
|
|
|
|
test('hardcodedNonRuntimeTailIsDeleted', () => {
|
|
assert.equal(
|
|
'NON_RUNTIME_REVIEWER_SLUGS' in reviewerSelectionModule, false,
|
|
'NON_RUNTIME_REVIEWER_SLUGS must no longer be exported from review-reviewer-selection',
|
|
);
|
|
assert.equal(reviewerSelectionModule.NON_RUNTIME_REVIEWER_SLUGS, undefined);
|
|
});
|
|
|
|
test('declaredBodyIsUnaffectedByAVestigialAliasKey', () => {
|
|
// capId (what the removed alias used to contribute) differs from
|
|
// reviewer.slug, so the result alone shows which surface was read.
|
|
const registry = {
|
|
capabilities: {
|
|
'conflicting-cap-id': {
|
|
role: 'runtime',
|
|
runtime: { hostBehaviors: { reviewerCli: true } },
|
|
reviewer: { slug: 'the-declared-slug' },
|
|
},
|
|
},
|
|
};
|
|
const roster = deriveReviewerSlugs(registry);
|
|
assert.deepEqual(
|
|
roster, ['the-declared-slug'],
|
|
`only the declared body's slug may contribute, got: ${JSON.stringify(roster)}`,
|
|
);
|
|
});
|
|
|
|
test('emptyRegistryYieldsEmptyRoster', () => {
|
|
assert.deepEqual(deriveReviewerSlugs({ capabilities: {} }), []);
|
|
assert.deepEqual(deriveReviewerSlugs({}), [], 'a registry object with no capabilities key at all must not throw');
|
|
});
|
|
|
|
test('rosterIsStableRegardlessOfRegistryOrder', () => {
|
|
// `gamma` was an alias-only capability before #2801. It is now body-declared
|
|
// so this row still proves a THREE-way sort; shrinking it to two entries
|
|
// would quietly weaken the ordering guarantee it exists to protect.
|
|
const capsForward = {
|
|
alpha: { role: 'reviewer', reviewer: { slug: 'zzz-lane' } },
|
|
beta: { role: 'reviewer', reviewer: { slug: 'aaa-lane' } },
|
|
gamma: { role: 'runtime', runtime: { hostBehaviors: {} }, reviewer: { slug: 'gamma' } },
|
|
};
|
|
const reversedCaps = {};
|
|
for (const key of Object.keys(capsForward).reverse()) reversedCaps[key] = capsForward[key];
|
|
|
|
const forwardRoster = deriveReviewerSlugs({ capabilities: capsForward });
|
|
const reversedRoster = deriveReviewerSlugs({ capabilities: reversedCaps });
|
|
assert.deepEqual(forwardRoster, reversedRoster, 'roster must not depend on registry key insertion order');
|
|
assert.deepEqual(
|
|
forwardRoster, ['aaa-lane', 'gamma', 'zzz-lane'],
|
|
'roster must be sorted, independent of declaration order',
|
|
);
|
|
});
|
|
|
|
test('derivedRosterNeverAdmitsAnAliasOnlyCapability', () => {
|
|
// P1 — property over arbitrary registries. Three invariants at once:
|
|
// (1) no capability that declares ONLY the removed alias ever reaches the
|
|
// roster, (2) the roster is sorted and duplicate-free, and (3) it does not
|
|
// depend on key insertion order. A partial revert of the alias branch is
|
|
// caught by (1) at any registry shape, not just the handful enumerated above.
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(
|
|
fc.record({
|
|
capId: fc.string({ minLength: 1, maxLength: 8 }).filter((s) => s.trim().length > 0),
|
|
declaresBody: fc.boolean(),
|
|
slug: fc.string({ minLength: 1, maxLength: 8 }).filter((s) => s.trim().length > 0),
|
|
aliasValue: fc.constantFrom(true, false, 'true', 1, 0, null, undefined),
|
|
}),
|
|
{ minLength: 0, maxLength: 12 },
|
|
),
|
|
(specs) => {
|
|
const capabilities = {};
|
|
const aliasOnlyIds = new Set();
|
|
const declaredSlugs = new Set();
|
|
for (const s of specs) {
|
|
if (s.capId === '__proto__' || s.capId === 'constructor' || s.capId === 'prototype') continue;
|
|
const cap = { role: 'runtime', runtime: { hostBehaviors: {} } };
|
|
if (s.aliasValue !== undefined) cap.runtime.hostBehaviors.reviewerCli = s.aliasValue;
|
|
if (s.declaresBody) {
|
|
cap.reviewer = { slug: s.slug };
|
|
declaredSlugs.add(s.slug.trim());
|
|
} else if (s.aliasValue !== undefined) {
|
|
aliasOnlyIds.add(s.capId);
|
|
}
|
|
capabilities[s.capId] = cap;
|
|
}
|
|
|
|
const roster = deriveReviewerSlugs({ capabilities });
|
|
|
|
// (1) An alias-only capability's id may appear ONLY if some other
|
|
// capability legitimately declared it as a body slug.
|
|
for (const id of aliasOnlyIds) {
|
|
if (declaredSlugs.has(id)) continue;
|
|
assert.equal(
|
|
roster.includes(id), false,
|
|
`alias-only capability "${id}" must not reach the roster; roster=${JSON.stringify(roster)}`,
|
|
);
|
|
}
|
|
|
|
// (2) sorted + unique
|
|
assert.deepEqual(roster, [...roster].sort(), `roster must be sorted, got: ${JSON.stringify(roster)}`);
|
|
assert.equal(new Set(roster).size, roster.length, `roster must be duplicate-free, got: ${JSON.stringify(roster)}`);
|
|
|
|
// (3) order-independent
|
|
const reversed = {};
|
|
for (const key of Object.keys(capabilities).reverse()) reversed[key] = capabilities[key];
|
|
assert.deepEqual(
|
|
deriveReviewerSlugs({ capabilities: reversed }), roster,
|
|
'roster must not depend on registry key insertion order',
|
|
);
|
|
return true;
|
|
},
|
|
),
|
|
{ numRuns: 200, seed: 2801 },
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── D. Cross-phase invariants that must not regress ────────────────────────
|
|
|
|
describe('D. Cross-phase invariants that must not regress', () => {
|
|
test('phase1ParityAssertionStillHolds', () => {
|
|
const workflowText = fs
|
|
.readFileSync(path.join(ROOT, 'gsd-core', 'workflows', 'review.md'), 'utf-8')
|
|
.replace(/\r\n/g, '\n');
|
|
const registry = [...SHIPPED.capMap.values()]
|
|
.map((c) => c && c.reviewer && c.reviewer.slug)
|
|
.filter((x) => typeof x === 'string' && x)
|
|
.sort();
|
|
const result = checkReviewerLaneParity({
|
|
descriptor: REVIEWER_LANES,
|
|
roster: KNOWN_REVIEWER_SLUGS,
|
|
registry,
|
|
workflowText,
|
|
});
|
|
assert.deepEqual(
|
|
result.violations, [],
|
|
`descriptor <-> roster <-> registry parity must stay green across this migration, got: ${JSON.stringify(result.violations)}`,
|
|
);
|
|
assert.equal(result.ok, true);
|
|
});
|
|
|
|
test('kimiCodeIsDeclaredAndInvocableInThisPhase', () => {
|
|
// Phase 5a deliberately withheld this lane: declaring it there would have made it selectable
|
|
// but NOT invocable — present in `--all`, selected, and producing an empty section for the
|
|
// whole 5a -> 5b window. ADR-2782's phase table lands it here, with the iteration that runs it.
|
|
assert.equal(KNOWN_REVIEWER_SLUGS.includes('kimi-code'), true);
|
|
const cap = SHIPPED.capMap.get('kimi-code');
|
|
assert.ok(cap, 'expected the kimi-code capability to exist');
|
|
assert.equal('reviewer' in cap, true, 'kimi-code must declare a reviewer body in 5b');
|
|
assert.equal(cap.reviewer.slug, 'kimi-code');
|
|
// The probe is the whole reason D7 ships wider than existence: `kimi` is claimed by BOTH the
|
|
// Kimi Code CLI and the legacy Python kimi-cli, so an existence-only probe registers the wrong
|
|
// tool. And it MUST be bounded — the original was an unbounded `kimi --help | grep` that ran
|
|
// on every review regardless of flags.
|
|
assert.equal(cap.reviewer.probe.kind, 'command-capability');
|
|
assert.equal(cap.reviewer.probe.binary, 'kimi');
|
|
assert.ok(cap.reviewer.probe.timeoutMs > 0, 'every process-starting probe must be bounded');
|
|
assert.equal(
|
|
Boolean(cap.runtime && cap.runtime.hostBehaviors && cap.runtime.hostBehaviors.reviewerCli),
|
|
false,
|
|
'the body is the declaration — the legacy reviewerCli alias must not also be set',
|
|
);
|
|
});
|
|
|
|
test('legacyKimiCapabilityIsNotAReviewerLane', () => {
|
|
assert.equal(KNOWN_REVIEWER_SLUGS.includes('kimi'), false, '"kimi" (the legacy Python CLI) must not be a reviewer lane');
|
|
const cap = SHIPPED.capMap.get('kimi');
|
|
assert.ok(cap, 'expected the kimi capability to exist');
|
|
assert.equal('reviewer' in cap, false, 'kimi must not acquire a reviewer body by proximity to kimi-code');
|
|
assert.equal(
|
|
Boolean(cap.runtime && cap.runtime.hostBehaviors && cap.runtime.hostBehaviors.reviewerCli),
|
|
false,
|
|
'kimi must not carry the legacy reviewerCli alias',
|
|
);
|
|
});
|
|
|
|
test('allElevenDeclaredLanesSatisfyUniqueness', () => {
|
|
const errs = validateCrossCapability(SHIPPED.capMap, new Set());
|
|
const laneErrs = errs.filter((e) => e.startsWith('reviewer '));
|
|
assert.deepEqual(
|
|
laneErrs, [],
|
|
`expected no reviewer-lane uniqueness violations (slug/flag/section) across the real eleven, got: ${JSON.stringify(laneErrs)}`,
|
|
);
|
|
});
|
|
|
|
test('selectionPrecedenceIsUnchanged', () => {
|
|
// ADR-0011: explicit flags > --all > review.default_reviewers > all
|
|
// detected. Exercised with real roster members so a broken roster
|
|
// derivation would also surface here — normalizeReviewerInstances /
|
|
// resolveReviewerSelection gate config_default membership on
|
|
// KNOWN_REVIEWER_SLUGS.includes(...).
|
|
const detected = ['codex', 'claude', 'qwen'];
|
|
|
|
const explicit = resolveReviewerSelection({
|
|
detected, explicitFlags: ['codex'], allFlag: true, configuredDefaultReviewers: ['claude'],
|
|
});
|
|
assert.equal(explicit.source, 'explicit_flags');
|
|
assert.deepEqual(explicit.selected, ['codex']);
|
|
|
|
const allFlagResult = resolveReviewerSelection({
|
|
detected, explicitFlags: [], allFlag: true, configuredDefaultReviewers: ['claude'],
|
|
});
|
|
assert.equal(allFlagResult.source, 'all_flag');
|
|
assert.deepEqual(allFlagResult.selected, [...detected].sort());
|
|
|
|
const configDefault = resolveReviewerSelection({
|
|
detected, explicitFlags: [], allFlag: false, configuredDefaultReviewers: ['claude', 'qwen'],
|
|
});
|
|
assert.equal(configDefault.source, 'config_default');
|
|
assert.deepEqual(configDefault.selected, ['claude', 'qwen']);
|
|
|
|
const noConfig = resolveReviewerSelection({ detected, explicitFlags: [], allFlag: false });
|
|
assert.equal(noConfig.source, 'no_config_all_detected');
|
|
assert.deepEqual(noConfig.selected, [...detected].sort());
|
|
});
|
|
});
|
|
|
|
// ─── E. Lane fidelity — no translation layer ────────────────────────────────
|
|
|
|
describe('E. Lane fidelity — no translation layer', () => {
|
|
test('declaredManifestLanesMatchThePhase1Descriptor', () => {
|
|
// The epic's entire premise: the manifest and the core descriptor describe
|
|
// the SAME lane with no translation layer. Phase 2's review already caught
|
|
// one divergence (the slug grammar) invisible to every other test — assert
|
|
// the WHOLE table, per field, so any future divergence names itself: the
|
|
// lane AND the exact field (and, for nested fields, the sub-field) that
|
|
// diverged.
|
|
const bySlug = new Map();
|
|
for (const [capId, cap] of Object.entries(capabilityRegistry.capabilities)) {
|
|
if (cap && cap.reviewer && typeof cap.reviewer.slug === 'string') {
|
|
bySlug.set(cap.reviewer.slug, { capId, reviewer: cap.reviewer });
|
|
}
|
|
}
|
|
|
|
assert.equal(REVIEWER_LANES.length, 11, 'expected exactly 11 declared descriptor lanes');
|
|
assert.equal(bySlug.size, 11, `expected exactly 11 capabilities declaring a reviewer body, got: ${bySlug.size}`);
|
|
|
|
// Top-level scalar/array fields compared whole; the two fields that are
|
|
// themselves nested objects (probe, invoke) are compared sub-field-by-
|
|
// sub-field over the UNION of keys on both sides, so a field present on
|
|
// only one side is caught exactly as loudly as one with a differing value.
|
|
const TOP_FIELDS = [
|
|
'flags', 'transport', 'probe', 'invoke', 'timeoutFloorMs', 'emptyOutput',
|
|
'reviewsSection', 'evidenceClass', 'requiresBinaries', 'promptBudgetKey', 'handler',
|
|
];
|
|
const NESTED_OBJECT_FIELDS = new Set(['probe', 'invoke']);
|
|
|
|
for (const lane of REVIEWER_LANES) {
|
|
const declared = bySlug.get(lane.slug);
|
|
assert.ok(declared, `no capability declares a reviewer body for descriptor lane "${lane.slug}"`);
|
|
const { capId, reviewer } = declared;
|
|
|
|
for (const field of TOP_FIELDS) {
|
|
if (NESTED_OBJECT_FIELDS.has(field)) {
|
|
const laneSub = lane[field] || {};
|
|
const manifestSub = reviewer[field] || {};
|
|
const subKeys = new Set([...Object.keys(laneSub), ...Object.keys(manifestSub)]);
|
|
for (const subKey of subKeys) {
|
|
assert.deepEqual(
|
|
manifestSub[subKey], laneSub[subKey],
|
|
`lane "${lane.slug}" (capability "${capId}") field "${field}.${subKey}" diverges from Phase 1's descriptor: ` +
|
|
`expected ${JSON.stringify(laneSub[subKey])}, got ${JSON.stringify(manifestSub[subKey])}`,
|
|
);
|
|
}
|
|
} else {
|
|
assert.deepEqual(
|
|
reviewer[field], lane[field],
|
|
`lane "${lane.slug}" (capability "${capId}") field "${field}" diverges from Phase 1's descriptor: ` +
|
|
`expected ${JSON.stringify(lane[field])}, got ${JSON.stringify(reviewer[field])}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Belt-and-suspenders whole-object comparison, in case a field exists on
|
|
// one side under a name the named-field loop above did not enumerate.
|
|
assert.deepEqual(
|
|
reviewer, lane,
|
|
`lane "${lane.slug}" (capability "${capId}") has a field-set divergence from Phase 1's descriptor`,
|
|
);
|
|
}
|
|
|
|
// Reverse direction: every capability-declared reviewer body maps back to
|
|
// a descriptor lane — no orphaned manifest lane the descriptor doesn't know.
|
|
for (const [slug, { capId }] of bySlug) {
|
|
assert.ok(
|
|
REVIEWER_LANES.some((l) => l.slug === slug),
|
|
`capability "${capId}" declares reviewer.slug "${slug}" with no matching Phase 1 descriptor lane`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── F. Isolated-security-review regressions (#2798) ─────────────────────────
|
|
//
|
|
// Both rows come from an independent adversarial review that reproduced them by
|
|
// execution. Neither is reachable through the checked-in registry — it is
|
|
// generated, JSON-sourced and code-reviewed — but `deriveReviewerSlugs` is
|
|
// EXPORTED for reuse and carries no other validation, so it must not depend on
|
|
// its caller's hygiene.
|
|
describe('F. Isolated-security-review regressions', () => {
|
|
test('whitespaceOnlySlugIsRejectedNotAdmittedToTheRoster', () => {
|
|
for (const blank of [' ', '\t', '\n', ' \t\n ']) {
|
|
const roster = deriveReviewerSlugs({ capabilities: { x: { reviewer: { slug: blank } } } });
|
|
assert.deepEqual(
|
|
roster, [],
|
|
`a whitespace-only slug can never match a real lane but would occupy a roster entry; got: ${JSON.stringify(roster)}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('slugIsTrimmedRatherThanDropped', () => {
|
|
// The fix must NOT discard a slug that merely carries incidental whitespace.
|
|
assert.deepEqual(
|
|
deriveReviewerSlugs({ capabilities: { x: { reviewer: { slug: ' codex ' } } } }),
|
|
['codex'],
|
|
);
|
|
});
|
|
|
|
test('aBlankBodyContributesNothingAndNoAliasRescuesIt', () => {
|
|
// INVERTED by Phase 7 (#2801). While the alias existed, a blank slug fell
|
|
// through to it, so a malformed body could not silently remove a lane that
|
|
// worked before — that was the point of the original row. With the alias
|
|
// gone there is nothing to fall through TO: a blank body is not a
|
|
// declaration, and a declaration is now the only route onto the roster.
|
|
//
|
|
// The row is inverted rather than deleted because it is the combination the
|
|
// single-variable rows miss, and because it is the security-review
|
|
// provenance for the trim: `deriveReviewerSlugs` is exported and carries no
|
|
// other validation, so a whitespace slug must never occupy a roster entry
|
|
// it can never match.
|
|
const roster = deriveReviewerSlugs({
|
|
capabilities: {
|
|
claude: { reviewer: { slug: ' ' }, runtime: { hostBehaviors: { reviewerCli: true } } },
|
|
},
|
|
});
|
|
assert.deepEqual(
|
|
roster, [],
|
|
'a blank body is not a declaration, and the removed alias cannot rescue it',
|
|
);
|
|
});
|
|
|
|
test('moduleLoadSurvivesAHostileRegistryShape', () => {
|
|
// KNOWN_REVIEWER_SLUGS is computed at require() time, so an uncaught throw
|
|
// there breaks import for EVERY consumer rather than degrading selection.
|
|
// The module under test already imported successfully above; assert the
|
|
// derived roster is a usable array rather than a partially-initialised value.
|
|
assert.ok(Array.isArray([...KNOWN_REVIEWER_SLUGS]), 'roster must be iterable after module load');
|
|
assert.equal(KNOWN_REVIEWER_SLUGS.length, 11, 'the real registry still yields the eleven lanes');
|
|
// And the derivation itself is total over the shapes JSON can express.
|
|
for (const hostile of [null, undefined, [], 0, 'x', { capabilities: null }, { capabilities: [] }]) {
|
|
assert.doesNotThrow(
|
|
() => deriveReviewerSlugs(hostile === undefined ? {} : (hostile || {})),
|
|
`deriveReviewerSlugs must tolerate ${JSON.stringify(hostile)}`,
|
|
);
|
|
}
|
|
});
|
|
});
|