`review-lane plan` resolved every cross-AI reviewer lane's reasoning effort by spawning `query resolve-execution gsd-plan-checker --host <slug>`. The agent id was a hardcoded literal, so `--host` chose only the argv RENDERING while the LEVEL always came from the installed plan-checker's frontmatter — `low` under every shipped model profile. Every prompt-fed lane therefore ran at a fast structural verifier's effort, and because the rendered argument is a CLI config override it silently beat the effort the operator had configured for that CLI. At `low` a large source-grounded prompt makes a model end its turn with no final message, so the lane came back empty and its stub read as a crash. Effort is a property of the review, so the lane declares it. Two new fields on ReviewerLane — `effortConfigKey` (`review.effort.<slug>`) and `defaultEffort` — carried through each capability manifest and the generated registry, set on the three lanes with an argv effort channel and null on the other nine. A new pure `resolveLaneEffort()` resolves config key -> lane default -> nothing, where "nothing" emits no effort argument at all and the reviewer CLI's own configuration decides; `inherit` selects that path explicitly and an unrecognized level falls back to the lane default rather than being forwarded to a CLI that would reject it. The host's negotiated effortSurface still gates the rendering, so ADR-1239/#2481's trust boundary holds on this path too. Resolving in-process also removes up to twelve subprocess spawns per review. The empty-output stub now names the effort the lane ran at and distinguishes a clean exit from a timeout kill, a non-zero exit, and a process that never ran — `status` is null for both a timeout and a signal, so those were indistinguishable before. The hint is hedged: a clean empty exit is most often a model stopping short, but it is also consistent with a CLI writing its output elsewhere. Also: the capability validator now knows both fields, rejects a malformed key or an out-of-vocabulary default, and rejects a default declared without a config key (a level the operator could never override). An existing end-to-end row in tests/effort-surface-axis.test.cjs asserted the old coupling; it now configures the lane's own key and pins the decoupling in the same real spawn, with the agent execution tier set to a level that must not appear. Emitted-Drift-Ack-Growth: review.md — the effort/model resolution-order table this fix adds. The workflow is where an operator looks to find out which knob set a lane's model and effort; leaving the new key undocumented there is the same invisibility that made the plan-checker coupling survive this long. Emitted-Drift-Ack-Growth: review.md — the effort/model resolution-order table this fix adds. The workflow is where an operator looks to find out which knob set a lane's model and effort, so leaving the new key undocumented there is the same invisibility that let the plan-checker coupling survive. Claude-Session: https://claude.ai/code/session_01CRMEuzNMWn3gs5uUW2ghcF Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
committed by
GitHub
parent
925a363879
commit
5ad9a36f35
@@ -580,14 +580,20 @@ describe('#2481 — ADR-443 mechanism callers, as they actually exist', () => {
|
||||
describe('#2481 review workflow resolves effort per reviewer', () => {
|
||||
test('shipped orchestration: the live claude lane genuinely receives --effort <level> in its spawned argv', () => {
|
||||
// Phase 5b (#2799) moved the call out of review.md's per-lane bash and into the review-lane
|
||||
// route's `effortFor()`, which SPAWNS `query resolve-execution … --pick effort_argv_string`
|
||||
// once per selected lane and folds the result into that lane's argv template. A text grep for
|
||||
// the string "resolve-execution" in gsd-tools.cjs would pass even if effortFor's result were
|
||||
// silently dropped before reaching the spawned reviewer, or if the call were dead code. This
|
||||
// drives the REAL `review-lane invoke` route end-to-end — real cp.spawnSync, a real project
|
||||
// config, a real claude-shaped shim on PATH — and inspects the argv the shim actually received,
|
||||
// which is the only way to prove the resolved effort reaches the invocation rather than merely
|
||||
// that some file mentions the command name.
|
||||
// route's `effortFor()`, which folds a resolved level into that lane's argv template. A text
|
||||
// grep in gsd-tools.cjs would pass even if the result were silently dropped before reaching
|
||||
// the spawned reviewer, or if the call were dead code. This drives the REAL `review-lane
|
||||
// invoke` route end-to-end — real cp.spawnSync, a real project config, a real claude-shaped
|
||||
// shim on PATH — and inspects the argv the shim actually received, which is the only way to
|
||||
// prove the resolved effort reaches the invocation rather than merely that some file mentions
|
||||
// the command name.
|
||||
//
|
||||
// #4255 changed WHERE the level comes from, not whether it must arrive. It used to be read
|
||||
// from the `gsd-plan-checker` AGENT's execution settings via a hardcoded id, so this row
|
||||
// configured `effort.routing_tier_defaults` and expected that value in the reviewer's argv.
|
||||
// A reviewer lane is not that agent, and the coupling is the bug. The row now configures the
|
||||
// lane's OWN `review.effort.claude` and pins the decoupling in the same spawn: the execution
|
||||
// routing tier is set to a DIFFERENT level, and leaking it into the reviewer is a failure.
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2481-orchestration-e2e-'));
|
||||
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2481-orchestration-project-'));
|
||||
try {
|
||||
@@ -637,9 +643,14 @@ describe('#2481 review workflow resolves effort per reviewer', () => {
|
||||
fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectDir, '.planning', 'config.json'),
|
||||
// #3531: pin every tier so the expected value is agent-independent —
|
||||
// the reviewer lane's tier decides, not effort.default.
|
||||
JSON.stringify({ effort: { routing_tier_defaults: { light: 'xhigh', standard: 'xhigh', heavy: 'xhigh' } } }, null, 2),
|
||||
// Two levels, deliberately different (#4255). `review.effort.claude` is the reviewer
|
||||
// lane's own key and is what must reach the shim. `effort.routing_tier_defaults` drives
|
||||
// the AGENT execution axis and is pinned across every tier to a level that must NOT
|
||||
// appear — before #4255 it, through gsd-plan-checker, was the only thing that could.
|
||||
JSON.stringify({
|
||||
review: { effort: { claude: 'xhigh' } },
|
||||
effort: { routing_tier_defaults: { light: 'minimal', standard: 'minimal', heavy: 'minimal' } },
|
||||
}, null, 2),
|
||||
);
|
||||
|
||||
const r = cp.spawnSync(
|
||||
@@ -663,7 +674,15 @@ describe('#2481 review workflow resolves effort per reviewer', () => {
|
||||
const argv = fs.readFileSync(seenArgv, 'utf8').trim().split(/\r?\n/);
|
||||
assert.ok(
|
||||
argv.includes('--effort') && argv.includes('xhigh'),
|
||||
`resolved effort ("xhigh") did not reach the spawned claude reviewer's argv: ${JSON.stringify(argv)}`,
|
||||
`the lane's own review effort ("xhigh") did not reach the spawned claude reviewer's argv: ${JSON.stringify(argv)}`,
|
||||
);
|
||||
// The decoupling half (#4255), and the reason this row is worth a real spawn: the agent
|
||||
// execution tier is pinned to `minimal` above. Seeing it here would mean a reviewer lane is
|
||||
// still taking its effort from an agent's execution settings.
|
||||
assert.ok(
|
||||
!argv.includes('minimal'),
|
||||
'the AGENT execution routing tier leaked into the reviewer lane\'s argv: '
|
||||
+ `${JSON.stringify(argv)} — a lane's effort must come from its own review key`,
|
||||
);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
|
||||
@@ -1045,3 +1045,49 @@ describe('review-lane CLI overlay invocation (#2927, rows 9–10)', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('#4255 — every lane declares where its review effort comes from', () => {
|
||||
// The two fields exist so a lane's effort is DATA about the lane, resolvable without asking any
|
||||
// agent for its execution settings. These rows pin the declaration itself: a new lane that adds
|
||||
// an argv effort channel without saying what effort to run at would otherwise silently inherit
|
||||
// whatever the resolver's fallback happens to be — the exact shape of the original bug.
|
||||
test('a lane with an argv effort channel declares BOTH a config key and a default', () => {
|
||||
for (const lane of REVIEWER_LANES) {
|
||||
if (lane.invoke.effortChannel !== 'argv') continue;
|
||||
assert.equal(typeof lane.effortConfigKey, 'string',
|
||||
`${lane.slug} renders an effort argument but declares no effortConfigKey`);
|
||||
assert.equal(lane.effortConfigKey, `review.effort.${lane.slug}`,
|
||||
`${lane.slug}: the key is read from config by this exact name`);
|
||||
assert.equal(typeof lane.defaultEffort, 'string',
|
||||
`${lane.slug} renders an effort argument but declares no review default`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a lane with no effort channel declares neither', () => {
|
||||
for (const lane of REVIEWER_LANES) {
|
||||
if (lane.invoke.effortChannel === 'argv') continue;
|
||||
assert.equal(lane.effortConfigKey, null, `${lane.slug} has no channel to feed`);
|
||||
assert.equal(lane.defaultEffort, null, `${lane.slug} has no channel to feed`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the prompt-fed, source-grounded lanes default no lower than high', () => {
|
||||
// These lanes read the repository against a plan set. Effort is load-bearing exactly here:
|
||||
// it is where a large prompt at a low level ends the turn with no final message.
|
||||
const RANK = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
||||
for (const lane of REVIEWER_LANES) {
|
||||
if (lane.defaultEffort === null) continue;
|
||||
assert.equal(lane.evidenceClass, 'source-grounded', `${lane.slug}: unexpected class for a default`);
|
||||
assert.ok(RANK.indexOf(lane.defaultEffort) >= RANK.indexOf('high'),
|
||||
`${lane.slug} defaults to ${lane.defaultEffort}, below the review floor`);
|
||||
}
|
||||
});
|
||||
|
||||
test('no declared default is the `inherit` sentinel — that is what a null key means', () => {
|
||||
// `inherit` is a legitimate CONFIGURED value (it selects "emit nothing"), but as a declared
|
||||
// default it would be a second spelling for `null` and split one behaviour across two shapes.
|
||||
for (const lane of REVIEWER_LANES) {
|
||||
assert.notEqual(lane.defaultEffort, 'inherit', `${lane.slug}: use null, not 'inherit'`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ const {
|
||||
} = require('../gsd-core/bin/lib/review-lane-descriptor.cjs');
|
||||
const {
|
||||
resolveLanePlan,
|
||||
resolveLaneEffort,
|
||||
resolveTimeoutMs,
|
||||
isEmptyReview,
|
||||
normalizeHost,
|
||||
@@ -722,3 +723,142 @@ describe('#2358 design principle: run-scoped temp dirs never collide across proj
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#4255 — lane effort comes from REVIEW config, never from another agent', () => {
|
||||
// Before this fix `review-lane plan` resolved every lane's effort by spawning
|
||||
// `query resolve-execution gsd-plan-checker --host <slug>`. The agent id was a hardcoded
|
||||
// literal, so `--host` chose only the argv RENDERING while the LEVEL always came from the
|
||||
// installed plan-checker's frontmatter — `low` under every shipped model profile. A cross-AI
|
||||
// review is the opposite workload from a fast structural verifier, the rendered argument is a
|
||||
// CLI config OVERRIDE (so it beat the effort the operator had set for that CLI), and at `low` a
|
||||
// large source-grounded prompt makes the model end its turn with no final message: the lane
|
||||
// came back empty and its stub read as a crash.
|
||||
//
|
||||
// These rows pin the replacement. `renderArgv` is injected, so they assert the RESOLUTION —
|
||||
// which level wins, and whether an argument is emitted at all — independently of any host's
|
||||
// argv syntax. The syntax itself stays pinned by the GOLDEN table above.
|
||||
|
||||
/** Stand-in for the host renderer: echoes the level back in a recognisable shape. */
|
||||
const render = (host, level) => ({ argv: ['-c', `effort=${level}`], value: level });
|
||||
/** A host that declares no argv effort surface — renders nothing, as ADR-2481 requires. */
|
||||
const renderNone = () => ({ argv: [], value: null });
|
||||
const lane = (slug) => REVIEWER_LANES.find((l) => l.slug === slug);
|
||||
|
||||
test('the declared default is used when nothing is configured — high on the prompt-fed lanes', () => {
|
||||
for (const slug of ['codex', 'claude', 'opencode']) {
|
||||
const r = resolveLaneEffort(lane(slug), () => undefined, render);
|
||||
assert.equal(r.value, 'high', `${slug} must default to high, not to another agent's level`);
|
||||
assert.equal(r.source, 'lane-default');
|
||||
assert.deepEqual(r.argv, ['-c', 'effort=high']);
|
||||
}
|
||||
});
|
||||
|
||||
test("a lane's own config key wins over the declared default", () => {
|
||||
const r = resolveLaneEffort(lane('codex'), (k) => (k === 'review.effort.codex' ? 'xhigh' : undefined), render);
|
||||
assert.equal(r.value, 'xhigh');
|
||||
assert.equal(r.source, 'config');
|
||||
});
|
||||
|
||||
test('each lane reads ONLY its own key — one lane’s effort never leaks into another', () => {
|
||||
const configGet = (k) => (k === 'review.effort.codex' ? 'minimal' : undefined);
|
||||
assert.equal(resolveLaneEffort(lane('codex'), configGet, render).value, 'minimal');
|
||||
assert.equal(resolveLaneEffort(lane('claude'), configGet, render).value, 'high',
|
||||
'claude must fall back to its OWN default, not pick up the codex key');
|
||||
});
|
||||
|
||||
test('a configured `inherit` emits NO argument, so the reviewer CLI’s own config decides', () => {
|
||||
const r = resolveLaneEffort(lane('codex'), () => 'inherit', render);
|
||||
assert.deepEqual(r.argv, []);
|
||||
assert.equal(r.value, null);
|
||||
assert.equal(r.source, 'none');
|
||||
});
|
||||
|
||||
test('a lane declaring no review effort at all emits no argument', () => {
|
||||
// The non-vacuity half of the row above: `none` must be reachable from the DECLARATION too,
|
||||
// not only from an explicit `inherit`. Nine shipped lanes have no effort channel to feed.
|
||||
for (const l of REVIEWER_LANES.filter((x) => x.effortConfigKey === null)) {
|
||||
const r = resolveLaneEffort(l, () => 'high', render);
|
||||
assert.deepEqual(r.argv, [], `${l.slug} declares no effort key and must emit nothing`);
|
||||
assert.equal(r.source, 'none');
|
||||
}
|
||||
});
|
||||
|
||||
test('an unrecognized level is REFUSED, not forwarded to the CLI', () => {
|
||||
// Forwarding a typo renders an argument the CLI rejects, which kills the lane outright — a
|
||||
// strictly worse outcome than the level the operator meant. Fall back to the declared default.
|
||||
for (const bogus of ['hihg', 'HIGH ', 'very-high', '', ' ', 'null']) {
|
||||
const r = resolveLaneEffort(lane('codex'), () => bogus, render);
|
||||
assert.equal(r.value, 'high', `'${bogus}' must fall back to the lane default`);
|
||||
assert.equal(r.source, 'lane-default');
|
||||
}
|
||||
});
|
||||
|
||||
test('a non-string configured value is ignored rather than rendered', () => {
|
||||
for (const bogus of [42, true, null, {}, ['high']]) {
|
||||
const r = resolveLaneEffort(lane('codex'), () => bogus, render);
|
||||
assert.equal(r.value, 'high', `${JSON.stringify(bogus)} must not reach the renderer`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the host’s negotiated surface still decides — a renderer that emits nothing yields none', () => {
|
||||
// ADR-1239/#2481's trust-boundary invariant: a lane cannot talk a host into accepting an
|
||||
// argument its negotiated effortSurface does not declare, however the lane is configured.
|
||||
const r = resolveLaneEffort(lane('codex'), () => 'xhigh', renderNone);
|
||||
assert.deepEqual(r.argv, []);
|
||||
assert.equal(r.source, 'none');
|
||||
});
|
||||
|
||||
test('the documented clamp is what the catalog actually does', () => {
|
||||
// The configuration reference tells the operator which levels survive per host, and a prose
|
||||
// claim about behaviour is a claim that can rot. Pin it against the real renderer so the two
|
||||
// cannot drift. (No file is read here — the lane's clamp table IS the thing being asserted.)
|
||||
const mc = require('../gsd-core/bin/lib/model-catalog.cjs');
|
||||
const seen = {};
|
||||
for (const host of ['codex', 'claude', 'opencode']) {
|
||||
seen[host] = Object.fromEntries(
|
||||
['minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
||||
.map((l) => [l, mc.renderEffortArgv(host, l, 'argv').value]),
|
||||
);
|
||||
}
|
||||
assert.equal(seen.codex.minimal, 'low', 'codex clamps minimal to low');
|
||||
assert.equal(seen.claude.minimal, 'low', 'claude clamps minimal to low');
|
||||
assert.equal(seen.opencode.minimal, 'minimal', 'opencode accepts minimal as-is');
|
||||
for (const host of ['codex', 'claude', 'opencode']) {
|
||||
for (const l of ['low', 'medium', 'high', 'xhigh', 'max']) {
|
||||
assert.equal(seen[host][l], l, `${host} must pass ${l} through unchanged`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('the renderer’s clamped value is recorded, not the level asked for', () => {
|
||||
// #2295 records the effort in REVIEWS.md as `model (reasoning=LEVEL)`. When a host clamps
|
||||
// (`max` -> `xhigh` on codex), the recorded level must be what actually ran.
|
||||
const clamping = () => ({ argv: ['-c', 'effort=xhigh'], value: 'xhigh' });
|
||||
const r = resolveLaneEffort(lane('codex'), () => 'max', clamping);
|
||||
assert.equal(r.value, 'xhigh', 'the clamped level is what ran, so it is what is recorded');
|
||||
});
|
||||
|
||||
test('a malformed lane object degrades to no effort instead of throwing', () => {
|
||||
// The resolver is called from gsd-tools.cjs (untyped) with a lane looked up by slug, which
|
||||
// can miss. Losing one lane's effort is recoverable; a throw there takes down every lane.
|
||||
for (const bad of [null, undefined, {}, { slug: 'x' }]) {
|
||||
assert.deepEqual(resolveLaneEffort(bad, () => 'high', render).argv, []);
|
||||
}
|
||||
});
|
||||
|
||||
test('resolved effort reaches argv only for lanes declaring effortChannel argv', () => {
|
||||
for (const l of REVIEWER_LANES) {
|
||||
const eff = resolveLaneEffort(l, () => undefined, render);
|
||||
const r = resolveLanePlan({
|
||||
lane: l, configGet: () => undefined, runDir: RUN, repoRoot: ROOT,
|
||||
effortArgs: eff.argv, effortValue: eff.value,
|
||||
});
|
||||
if (!r.ok || r.plan.transport !== 'spawn') continue;
|
||||
const carriesEffort = r.plan.argv.some((a) => String(a).startsWith('effort='));
|
||||
assert.equal(carriesEffort, l.invoke.effortChannel === 'argv',
|
||||
`${l.slug}: effort argv must appear exactly when the lane declares the channel`);
|
||||
assert.equal(r.plan.effort, l.invoke.effortChannel === 'argv' ? 'high' : null,
|
||||
`${l.slug}: the recorded effort must match what actually expanded`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ const path = require('node:path');
|
||||
const fc = require('fast-check');
|
||||
|
||||
const { REVIEWER_LANES } = require('../gsd-core/bin/lib/review-lane-descriptor.cjs');
|
||||
const { resolveLanePlan, LANE_UNAVAILABLE } = require('../gsd-core/bin/lib/review-lane-invocation.cjs');
|
||||
const { resolveLanePlan, resolveLaneEffort, LANE_UNAVAILABLE } = require('../gsd-core/bin/lib/review-lane-invocation.cjs');
|
||||
const {
|
||||
checkEgressHost,
|
||||
probeLane,
|
||||
@@ -233,6 +233,108 @@ describe('runner — empty-output policy (#2494 / #2605 / #2794)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A plan built through the SAME two steps the CLI runs (#4255): resolve the lane's review
|
||||
* effort, then expand it into argv. `plan()` above passes no effort at all, which is the right
|
||||
* default for rows that do not care — but a stub row asserting the effort must not invent it.
|
||||
*/
|
||||
function planWithEffort(slug) {
|
||||
const lane = REVIEWER_LANES.find((l) => l.slug === slug);
|
||||
const eff = resolveLaneEffort(lane, () => undefined, (host, level) => ({
|
||||
argv: ['-c', `model_reasoning_effort=${level}`], value: level,
|
||||
}));
|
||||
const r = resolveLanePlan({
|
||||
lane, configGet: () => undefined, runDir: RUN, repoRoot: ROOT,
|
||||
effortArgs: eff.argv, effortValue: eff.value,
|
||||
});
|
||||
assert.equal(r.ok, true, `${slug} failed to resolve`);
|
||||
return r.plan;
|
||||
}
|
||||
|
||||
test('#4255 — the stub names the effort and says the exit was clean', () => {
|
||||
// A crash, a timeout kill and a model that ended its turn without writing a final message all
|
||||
// reach the stub as the same zero bytes. The third is what a too-low reasoning effort produces
|
||||
// on a large source-grounded prompt, and it was indistinguishable from the first two — the
|
||||
// operator read "Codex failed" and went looking for a broken CLI. The stub now names the level
|
||||
// it ran at (the value they would change) and how the process actually ended.
|
||||
const p = planWithEffort('codex');
|
||||
const d = deps();
|
||||
writeReviewOrStub(p, '', d, undefined, { status: 0 });
|
||||
const out = d.files[p.reviewPath];
|
||||
assert.ok(out.includes('failed or returned empty output'),
|
||||
'the header every downstream reader greps for must be untouched');
|
||||
assert.ok(/ran at effort=high/.test(out), 'the stub must name the effort the lane ran at');
|
||||
assert.ok(/exited cleanly inside the timeout/.test(out));
|
||||
assert.ok(/ending its turn without writing a final message/.test(out),
|
||||
'a clean exit with no output must be named as the likeliest cause, not left reading as a crash');
|
||||
assert.ok(/most often/.test(out),
|
||||
'the cause is hedged on purpose: a clean empty exit is consistent with a stopped-short model '
|
||||
+ 'AND with a CLI writing its output somewhere this lane did not read');
|
||||
});
|
||||
|
||||
test('#4255 — a timeout kill and a crash are NOT reported as stopping short', () => {
|
||||
// The non-vacuity half: the stopped-short hint must be earned by a clean exit, or it is
|
||||
// advice that sends the operator to raise effort on a lane that was killed or crashed.
|
||||
const timedOut = deps();
|
||||
writeReviewOrStub(planWithEffort('codex'), '', timedOut, undefined, { status: null, errorCode: 'ETIMEDOUT' });
|
||||
const t = timedOut.files[planWithEffort('codex').reviewPath];
|
||||
assert.ok(/killed by the outer timeout/.test(t));
|
||||
assert.ok(!/most often/.test(t), 'a timeout is not a model stopping short');
|
||||
|
||||
const crashed = deps();
|
||||
writeReviewOrStub(planWithEffort('codex'), '', crashed, undefined, { status: 127 });
|
||||
const c = crashed.files[planWithEffort('codex').reviewPath];
|
||||
assert.ok(/exited with status 127/.test(c));
|
||||
assert.ok(!/most often/.test(c), 'a non-zero exit is not a model stopping short');
|
||||
|
||||
// `status` is null for a process that never started or died on a signal, exactly as it is for
|
||||
// a timeout kill. Reporting "status null" named nothing the operator could act on, and the
|
||||
// two must not collapse into one another (Codex review of #4255).
|
||||
for (const [label, out, expect] of [
|
||||
['binary missing', { status: null, errorCode: 'ENOENT' }, /did not exit normally \(ENOENT\)/],
|
||||
['killed by a signal', { status: null }, /did not exit normally \(killed by a signal\)/],
|
||||
]) {
|
||||
const d = deps();
|
||||
writeReviewOrStub(planWithEffort('codex'), '', d, undefined, out);
|
||||
const text = d.files[planWithEffort('codex').reviewPath];
|
||||
assert.match(text, expect, `${label} must be named, not reported as "status null"`);
|
||||
assert.ok(!/status null/.test(text), `${label}: "status null" tells the operator nothing`);
|
||||
assert.ok(!/most often/.test(text), `${label} is not a model stopping short`);
|
||||
}
|
||||
});
|
||||
|
||||
test('#4255 — an HTTP lane is not described as having a reviewer CLI', () => {
|
||||
// ollama is reached directly over HTTP: there is no CLI, so "the CLI's own configuration
|
||||
// applied" would be a lie about what ran (Codex review of #4255).
|
||||
const p = plan('ollama');
|
||||
const d = deps();
|
||||
writeReviewOrStub(p, '', d, undefined, { status: 0 });
|
||||
const out = d.files[p.reviewPath];
|
||||
assert.ok(/HTTP lane/.test(out));
|
||||
assert.ok(!/reviewer CLI/.test(out), 'an HTTP lane has no reviewer CLI to attribute anything to');
|
||||
assert.ok(!/effort=/.test(out), 'no level may be claimed for a transport that carries none');
|
||||
});
|
||||
|
||||
test('#4255 — a lane that sent no effort argument says so, rather than naming a level', () => {
|
||||
// gemini declares no effort channel, so `plan.effort` is null. Printing a level there would
|
||||
// be a lie about what reached the CLI; the stub says the CLI's own configuration applied.
|
||||
const p = plan('gemini');
|
||||
const d = deps();
|
||||
writeReviewOrStub(p, '', d, undefined, { status: 0 });
|
||||
const out = d.files[p.reviewPath];
|
||||
assert.ok(/no effort argument, so the reviewer CLI's own configuration applied/.test(out));
|
||||
assert.ok(!/effort=/.test(out), 'no level may be claimed for a lane that sent none');
|
||||
});
|
||||
|
||||
test('#4255 — the diagnosis is added even when the outcome is unknown', () => {
|
||||
// The HTTP path has no spawn outcome to pass. The effort half still applies, so the line is
|
||||
// still written — just without the exit clause it cannot honestly make.
|
||||
const p = planWithEffort('codex');
|
||||
const d = deps();
|
||||
writeReviewOrStub(p, '', d);
|
||||
assert.ok(/ran at effort=high/.test(d.files[p.reviewPath]));
|
||||
});
|
||||
|
||||
test('the stub is distinguishable from a real review', () => {
|
||||
// The ambiguity between "failed" and "ran cleanly with nothing to report" IS the defect.
|
||||
const p = plan('gemini');
|
||||
|
||||
Reference in New Issue
Block a user