diff --git a/extensions/gentle-ai.ts b/extensions/gentle-ai.ts index 096284e5e..2cd6a8eee 100644 --- a/extensions/gentle-ai.ts +++ b/extensions/gentle-ai.ts @@ -6526,7 +6526,7 @@ function isRetainedNativeCaptureRoute(selection: RetainedNativeStatusSelection | function retainNativeCaptureRoutes(selections: Map, workspaceRoot: string, status: ReviewStatusV3, baseRef: string | undefined): void { const lineageId = status.authority?.lineageId; - if (status.applicability !== "current_target" || !isCanonicalProcessString(lineageId) || isTerminalReviewAuthorityState(status.authority?.state)) return; + if (!canOfferNativeCaptureBindings(status) || !isCanonicalProcessString(lineageId)) return; const routes = (status.nextTransition?.kind === "collect" ? status.nextTransition.collect?.inputs ?? [] : []).map((input) => ({ key: reviewCaptureSelectionStorageKey(canonicalReviewCaptureBinding(input)), route: Object.freeze({ workspaceRoot, lineageId, ...(baseRef === undefined ? {} : { baseRef, committedOnly: true as const }) }) })); for (const { key, route } of routes) { const existing = selections.get(key); @@ -6542,6 +6542,12 @@ function readRetainedNativeCaptureRoute(selections: Map, workspaceRoot: string, lineageId: string): void { @@ -7217,8 +7223,13 @@ const INSPECT_UNTRACKED_SELECTION_NEXT_STEP = 'The intended-untracked selection is required before START. The expected_untracked_inventory digest covers untracked path names only (git ls-files --others --exclude-standard); nothing is read or hashed at inventory time, and file content is hashed only for selected paths at candidate freeze. Either call gentle_review with operation "select-intended-untracked" passing this selectionBinding and intendedUntracked ([] excludes every eligible path, a subset includes only those paths), or call inspect again with untrackedScope ("exclude", or "select" with intendedUntracked) to resolve the round trip in one call. To keep a path out of the inventory permanently, ignore it through .gitignore or .git/info/exclude.'; interface PublicReviewCaptureBinding { collectBinding: string; } +function canOfferNativeCaptureBindings(status: ReviewStatusV3): boolean { + return status.applicability === "current_target" && isCanonicalProcessString(status.authority?.lineageId) + && isCanonicalProcessString(status.targetIdentity) && !isTerminalReviewAuthorityState(status.authority?.state); +} + function publicReviewCaptureBindings(status: ReviewStatusV3): readonly PublicReviewCaptureBinding[] { - if (status.nextTransition?.kind !== "collect") return []; + if (!canOfferNativeCaptureBindings(status) || status.nextTransition?.kind !== "collect") return []; return (status.nextTransition.collect?.inputs ?? []).filter((input) => input.captureOperation !== "external.select_intended_untracked").map((input) => ({ collectBinding: canonicalReviewCaptureBinding(input) })); } @@ -7249,7 +7260,7 @@ function selectExactReviewCapture( !isCanonicalProcessString(lineageId) || !isCanonicalProcessString(statusLineageId) || !isCanonicalProcessString(statusTargetIdentity) || - status.applicability !== "current_target" || + !canOfferNativeCaptureBindings(status) || statusLineageId !== lineageId ) { return captureBindingRejected("current STATUS does not offer one non-empty matching lineage and target identity"); @@ -7402,16 +7413,17 @@ async function executeReviewCaptureOperation( } const canonicalBinding = parseCanonicalReviewCaptureBinding(parameters.collectBinding); const cwd = resolveReviewControllerWorkspaceRoot(parameters.workspaceRoot, sessionCwd, candidateViews, parameters.lineageId); - const route = readRetainedNativeCaptureRoute(retainedUntrackedSelections, canonicalBinding); - if (requireRegisteredRoute && (route === undefined || route.workspaceRoot !== cwd || route.lineageId !== parameters.lineageId)) { - return captureBindingRejected("collectBinding is unknown, expired, or belongs to a different session route"); + let route = readRetainedNativeCaptureRoute(retainedUntrackedSelections, canonicalBinding); + if (requireRegisteredRoute && route !== undefined && (route.workspaceRoot !== cwd || route.lineageId !== parameters.lineageId)) { + return captureBindingRejected("collectBinding belongs to a different registered route"); } + const baseRef = trustedNativeCaptureBaseRef(route, candidateViews, cwd, parameters.lineageId); let status: ReviewStatusV3; try { const negotiated = await negotiatedStatusForHostTransport(nativeReviewCli, { cwd, lineageId: parameters.lineageId, - ...(route?.baseRef === undefined ? {} : { baseRef: route.baseRef, committedOnly: true }), + ...(baseRef === undefined ? {} : { baseRef, committedOnly: true }), ...readRetainedNativeUntrackedSelection(retainedUntrackedSelections, cwd, parameters.lineageId), ...(signal === undefined ? {} : { signal }), }, retainedUntrackedSelections, cwd); @@ -7422,6 +7434,9 @@ async function executeReviewCaptureOperation( } const selected = selectExactReviewCapture(status, parameters.lineageId, canonicalBinding); if (!isSelectedReviewCapture(selected)) return selected; + // Exact fresh native admission validates this routing snapshot independently + // of cache capacity. Carry its trusted selector into every downstream path. + route = { workspaceRoot: cwd, lineageId: parameters.lineageId, ...(baseRef === undefined ? {} : { baseRef, committedOnly: true }) }; // During correction the flow carries both the original authority target // identity and a distinct provider-issued correction target identity @@ -7565,13 +7580,14 @@ async function executeReviewCaptureGroupOperation( const canonicalBindings = parameters.collectBindings.map((binding) => parseCanonicalReviewCaptureBinding(binding)); const cwd = resolveReviewControllerWorkspaceRoot(parameters.workspaceRoot, sessionCwd, candidateViews, parameters.lineageId); const routes = canonicalBindings.map((binding) => readRetainedNativeCaptureRoute(retainedUntrackedSelections, binding)); - const route = routes[0]; - if (requireRegisteredRoute && (route === undefined || routes.some((candidate) => candidate === undefined || candidate.workspaceRoot !== cwd || candidate.lineageId !== parameters.lineageId || candidate.baseRef !== route.baseRef))) { - return captureGroupRejected("collectBindings are unknown, expired, or belong to different session routes"); + let route = routes.find((candidate) => candidate !== undefined); + if (requireRegisteredRoute && routes.some((candidate) => candidate !== undefined && (candidate.workspaceRoot !== cwd || candidate.lineageId !== parameters.lineageId || candidate.baseRef !== route?.baseRef))) { + return captureGroupRejected("collectBindings belong to different registered routes"); } + const baseRef = trustedNativeCaptureBaseRef(route, candidateViews, cwd, parameters.lineageId); const freshStatus = () => negotiatedStatusForHostTransport(nativeReviewCli, { cwd, lineageId: parameters.lineageId, - ...(route?.baseRef === undefined ? {} : { baseRef: route.baseRef, committedOnly: true }), + ...(baseRef === undefined ? {} : { baseRef, committedOnly: true }), ...readRetainedNativeUntrackedSelection(retainedUntrackedSelections, cwd, parameters.lineageId), ...(signal === undefined ? {} : { signal }), }, retainedUntrackedSelections, cwd); @@ -7585,6 +7601,7 @@ async function executeReviewCaptureGroupOperation( } const group = selectExactReviewCaptureGroup(status, parameters.lineageId, canonicalBindings); if (!("slots" in group && "binding" in group)) return group; + route = { workspaceRoot: cwd, lineageId: parameters.lineageId, ...(baseRef === undefined ? {} : { baseRef, committedOnly: true }) }; if (parameters.reviewerRunAcknowledged !== true) { return { tool: "gentle_review_capture_group", diff --git a/odd/tasks/issue-1316-native-status-capture.md b/odd/tasks/issue-1316-native-status-capture.md new file mode 100644 index 000000000..40127108f --- /dev/null +++ b/odd/tasks/issue-1316-native-status-capture.md @@ -0,0 +1,56 @@ +# Recover capture routes from current native STATUS (#1316) + +Prepare the minimal single/group capture fix without treating volatile route metadata as provider authority. User authorized updating the feature branch, revalidation, commits, native review and PR creation; **merge and auto-merge are not authorized**. + +## Scope and boundaries + +- Approved issue: Gentleman-Programming/gentle-shell#1316; related cluster: gentle-ai#4498. +- Branch: `fix/1316-native-status-capture`, dedicated isolated worktree. +- Original base: `664bfdd295e3bd3b2f291344e77bda94f58d5931`. +- Current update target: `4b6b148b7dbd741b3097c00d5888f0209a567f94` (one unrelated upstream commit, including runtime-harness changes). +- PR #1542 remains draft diagnostics only; this fix does not claim to identify every reported miss branch. +- Allowed implementation: `extensions/gentle-ai.ts` and the native/host-relay routing tests. No telemetry, persistence, TTL, retries, flags or new native states. + +Fresh Pi-bound STATUS must validate exact current-target lineage/target/binding before relay or capture. Recover committed selectors only from matching trusted routes/candidate projections. Preserve group order/completeness, stale/foreign/terminal rejection, forecast acknowledgement, no-replay rules, registration collisions and downstream selector continuity. Align binding publication and registration eligibility. + +## Execution + +| Setting | Value | +| --- | --- | +| Workflow | Delegated direct ODD; not SDD; one writer | +| TDD | Strict, explicitly selected by user; `node --experimental-strip-types --test` | +| RDD | Global on; candidate-native consent and acknowledgement remain separate | +| Delivery | One coherent work unit/PR; source/test diff 191 authored lines before upstream update, plus this record; below 400 | +| Memory | Local document authoritative; Engram mirror pending incompatible installed provider | + +## Tasks + +- [x] **T1 — Baseline/dependencies:** frozen install with scripts disabled; existing bounded tests 25/25, typecheck accepted 187 diagnostics with no regressions. +- [x] **T2 — Reproduce/repair:** initial three recovery regressions RED, then six GREEN. Stale STATUS-count expectation separately RED/GREEN; four exact STATUS requests and zero stale captures asserted. Source/test diff +176/-15. +- [x] **T3 — Scoped candidate verification:** independent committed-unit recheck on updated base passed 135/135, typecheck baseline187, modules8/8 and diff checks; clean tree and publication-privacy readback. Native consolidated review approved and its exact acknowledgement burned authority. Global full-suite green and defect-specific native-provider E2E remain unclaimed. +- [x] **T4 — Classify original-base failures:** independent clean original base reproduced all other 18 failure signatures, same cancellation/timeout and empty-persona harness failure. This proves attribution at the original base, not a fresh full-suite pass at the new update target. +- [x] **T5 — Diagnose harness side effect:** extended fixture invoked existing dispatch hydration and created an owned view. Later exact view/marker/admin-dir/registration absence confirmed, no manual cleanup performed, removal actor unknown. No native lifecycle-authority mutation found in that hydration path. +- [x] **T6 — Update/revalidate against current main:** fast-forwarded only this feature branch to the pinned target; exact patch/task-record hashes preserved through path-limited stash restore, no conflicts, backup retained. Complete affected files 135/135; typecheck baseline187, modules8/8 and diff checks pass. Full suite was not rerun on this target. +- [ ] **T7 — Commit/review/PR** (publication in progress): code work unit `7b0e7ab9735c5a5c39d631eeeb26a0bf89026636`, 247 authored lines including record, medium native tier with one consolidated `review-reliability` lens; approval acknowledged and authority burned. Publish only feature branch, open issue-linked PR with one `type:bug` label, report CI; never merge. + +## Observed evidence before update + +| Check | Outcome | +| --- | --- | +| Recovery RED/GREEN | Three initial failures on unchanged production; final six new tests pass | +| Stale request correction | Expected 3 versus observed 4 before correction; corrected exact vector passes, zero stale captures | +| Complete affected routing files | Independent 135/135, no failures/cancellations/skips; approximately 30.5 seconds | +| Typecheck | Exit 0 against accepted 187 diagnostics, 11 improved pairs; not diagnostic-free TS | +| Runtime-module/diff checks | Eight generated modules match; whitespace check passes | +| Full original candidate suite | Timeout 124 after 4088 tests: 4018 pass, 19 fail, 1 cancelled, 50 skipped; candidate-only count expectation corrected afterwards | +| Clean original base suite | Timeout 124 after 4082 tests: 4013 pass, 18 fail, 1 cancelled, 50 skipped; matching remaining signatures | +| Standalone original harness | Candidate and clean base both fail empty persona assertion; later full-suite phases remain unverified | +| Genuine Pi wiring | Runtime3 loaded registered tools through SDK loader/AgentSession and passed recovery/negative fixtures; fake STATUS, no native-provider E2E claim | + +Operator logs retained outside the repository use prefixes `gentle-1316-independent-verify`, `gentle-1316-clean-base-` and `gentle-1316-final-recheck-`. Runtime3 log is evidence only: the temporary script was subsequently extended into failed runtime4 and must not be rerun against shared Git. Future dispatch-hydration experiments require an independent temporary Git common-directory. + +## Next step and rollback + +Work unit `7b0e7ab9735c5a5c39d631eeeb26a0bf89026636` is committed and independently rechecked (135/135). Native review approved and exact acknowledgement completed. ASSESS remains schema-incompatible, so conservative independent verification was performed; native START itself classified this scoped candidate as medium. Record-only evidence updates do not change executable behavior. Publish the feature branch and open the PR; CI outcomes are pending, with no merge authorization. + +Rollback only this fix's coherent work-unit commit (source, tests and record); preserve unrelated authority and worktrees. No push to main, merge or auto-merge authorization exists. diff --git a/tests/review-controller-native-routing.test.ts b/tests/review-controller-native-routing.test.ts index 823832cdf..8e1f78762 100644 --- a/tests/review-controller-native-routing.test.ts +++ b/tests/review-controller-native-routing.test.ts @@ -463,7 +463,10 @@ test("interleaved sessions sharing a CLI retain only their own capture routes", const index = requests.length - 1; if (index < 2) { [readyA, readyB][index]!(); await waits[index]!; } const lineageId = String(request.lineageId); - return status(lineageId, [inputs.get(lineageId)!]); + // Cache absence alone is not foreign-session evidence. The provider + // rejects selectorless requests for these committed-range candidates. + return request.baseRef === (lineageId === a ? "base-a" : "base-b") + ? status(lineageId, [inputs.get(lineageId)!]) : status(lineageId, []); }, captureCorrectionPlan: async ({ argumentTokens }: { argumentTokens: readonly string[] }) => { captures += 1; @@ -487,7 +490,25 @@ test("interleaved sessions sharing a CLI retain only their own capture routes", outcomes: [ownA, ownB, foreign, cleaned].map(({ details }) => (details as { outcome?: string }).outcome), revalidationCalls: requests.length - 2, captures, - }, { outcomes: ["native-last-event-closure", "native-last-event-closure", "capture-binding-rejected", "capture-binding-rejected"], revalidationCalls: 2, captures: 2 }); + }, { outcomes: ["native-last-event-closure", "native-last-event-closure", "capture-binding-rejected", "capture-binding-rejected"], revalidationCalls: 4, captures: 2 }); +}); + +test("capture route recovery uses a trusted committed projection through unknown-outcome reconciliation", async (t) => { + const candidateViews = new CandidateViewRegistry(); + t.after(() => candidateViews.cleanupAll()); + const cwd = repository(t), lineageId = "recovered-committed", input = correctionPlanInput(lineageId); + const view = candidateViews.create({ contributorRoot: cwd, baseRef: execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim(), committedOnly: true }); + candidateViews.retain(view.token, lineageId); + const target = candidateViews.resolveProjection(lineageId, cwd), requests: Array> = []; + const native = { + targetStatus: async (request: Record) => { requests.push(request); return status(lineageId, [input]); }, + captureCorrectionPlan: async () => { throw Object.assign(new Error("lost response"), { mutationOutcome: "unknown", nextAction: "review.status" }); }, + } as unknown as NativeReviewCli; + const result = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input), correctionLines: 1 }, cwd, native, undefined, candidateViews, new Map(), true); + assert.equal(result.outcome, "native-capture-outcome-unknown"); + assert.equal(requests.length, 2, "one admission STATUS and one reconciliation, never capture replay"); + assert.ok(requests.every((request) => request.cwd === cwd && request.lineageId === lineageId && request.baseRef === target.baseCommit && request.committedOnly === true)); + assert.equal(requests[0]!.agent, "pi"); }); test("REPAIR retains frozen committed collect selectors and leaves workspace routes unselected", async (t) => { @@ -649,7 +670,8 @@ test("STATUS preserves retained intended-untracked selection through selectorles selections, true, ); - assert.deepEqual({ outcome: stale.outcome, requests: requests.length, captures }, { outcome: "capture-binding-rejected", requests: 3, captures: 0 }); + assert.deepEqual({ outcome: stale.outcome, requests: requests.length, captures }, { outcome: "capture-binding-rejected", requests: 4, captures: 0 }); + assert.deepEqual(requests[3], { cwd, lineageId, agent: "pi", ...selectedUntracked }); const captured = await __testing.executeReviewCaptureOperation( { lineageId, collectBinding: bindingB, correctionLines: 1 }, @@ -666,6 +688,7 @@ test("STATUS preserves retained intended-untracked selection through selectorles { cwd, lineageId, agent: "pi", baseRef: "main", committedOnly: true }, { cwd, lineageId, agent: "pi", ...selectedUntracked }, { cwd, lineageId, agent: "pi", ...selectedUntracked }, + { cwd, lineageId, agent: "pi", ...selectedUntracked }, ]); assert.equal(captures, 1); @@ -676,10 +699,14 @@ test("STATUS preserves retained intended-untracked selection through selectorles }); test("route retention caps, rejects collisions and invalid selectors, and clears every terminal state", async () => { - const native = { targetStatus: async (request: Record) => status(String(request.lineageId)) } as unknown as NativeReviewCli; + const requests: Array> = []; + const native = { targetStatus: async (request: Record) => { requests.push(request); return status(String(request.lineageId), request.baseRef === undefined ? [] : [collectInput(String(request.lineageId))]); } } as unknown as NativeReviewCli; const selections = new Map(); for (let index = 0; index <= 64; index += 1) await __testing.executeReviewControllerOperation({ operation: "status", lineageId: `bounded-${index}`, input: JSON.stringify({ baseRef: `base-${index}`, committedOnly: true }) }, process.cwd(), native, undefined, undefined, undefined, selections); + assert.equal(selections.size, 64, "route retention remains bounded before native refresh"); const evicted = await __testing.executeReviewCaptureOperation({ lineageId: "bounded-0", collectBinding: JSON.stringify(collectInput("bounded-0")) }, process.cwd(), native, undefined, undefined, selections, true); + assert.equal(requests.length, 66, "eviction must negotiate fresh native STATUS before rejection"); + assert.deepEqual(requests.at(-1), { cwd: process.cwd(), lineageId: "bounded-0", agent: "pi" }); const collision = new Map(), lineageId = "route-collision", input = correctionPlanInput(lineageId); await __testing.executeReviewControllerOperation({ operation: "status", lineageId, input: JSON.stringify({ baseRef: "base-a", committedOnly: true }) }, process.cwd(), { targetStatus: async () => status(lineageId, [input]) } as unknown as NativeReviewCli, undefined, undefined, undefined, collision); const rejected = await __testing.executeReviewControllerOperation({ operation: "status", lineageId, input: JSON.stringify({ baseRef: "base-b", committedOnly: true }) }, process.cwd(), { targetStatus: async () => status(lineageId, [input]) } as unknown as NativeReviewCli, undefined, undefined, undefined, collision); diff --git a/tests/review-host-relay-routing.test.ts b/tests/review-host-relay-routing.test.ts index c77dc5261..91ba53c0b 100644 --- a/tests/review-host-relay-routing.test.ts +++ b/tests/review-host-relay-routing.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { __testing } from "../extensions/gentle-ai.ts"; +import { CandidateViewRegistry } from "../lib/review-candidate-view.ts"; import { localProfilePinPath, repoProfileDeclarationPath, writeProfilePinSync } from "../lib/agent-profile-pin.ts"; import { createProfile, emptyProfilesFile, profilesFilePath, writeProfilesFileSync } from "../lib/agent-profiles.ts"; import type { AgentModelConfig } from "../lib/model-routing-authority.ts"; @@ -232,6 +233,122 @@ async function runCapture(cwd: string, harness: RoutingHarness, lineageId: strin ) as Record; } +// Fake STATUS/relay evidence exercises facade admission, not native-provider E2E. +test("capture route recovery revalidates a single forecast after route loss", async (t) => { + t.after(() => __testing.setReviewHostRelayRunnerForTesting()); + const cwd = repository(t), lineageId = "recovered-single", input = relayCollectInput(lineageId, "review-risk", 0); + const selections = new Map(), current = finalizeStatus(lineageId, [input]); + const harness = nativeHarness([current, current, current]); + let relays = 0; + __testing.setReviewHostRelayRunnerForTesting(async () => { + relays += 1; + return { promptByteLength: 64, resultByteLength: 32, submission: '{"admission_decision":"completed"}' }; + }); + await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, cwd, harness.native, undefined, null, undefined, selections); + const parameters = { lineageId, collectBinding: JSON.stringify(input) }; + const forecast = await __testing.executeReviewCaptureOperation(parameters, cwd, harness.native, undefined, null, selections, true); + assert.equal(forecast.outcome, "reviewer-model-run-forecast"); + assert.equal(relays, 0); + selections.clear(); + const result = await __testing.executeReviewCaptureOperation({ ...parameters, reviewerRunAcknowledged: true }, cwd, harness.native, undefined, null, selections, true); + assert.equal(result.status, "captured"); + assert.equal(relays, 1); + assert.equal(harness.statusCalls.length, 3); + assert.ok(harness.statusCalls.every((request) => request.agent === "pi")); +}); + +test("capture route recovery revalidates a complete group after forecast eviction", async (t) => { + t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting()); + const cwd = repository(t), lineageId = "recovered-group", inputs = groupInputs(lineageId), selections = new Map(); + const harness = nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), ...inputs.map((_input, index) => finalizeStatus(lineageId, inputs.slice(index))), finalizeStatus(lineageId)]); + let relays = 0, submissions = 0; + __testing.setReviewHostRelayGroupRunnersForTesting(async (requests) => { relays += requests.length; return requests.map(prepared); }, async () => { + submissions += 1; + return { promptByteLength: 64, resultByteLength: 32, submission: "{}" }; + }); + await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, cwd, harness.native, undefined, null, undefined, selections); + const parameters = { lineageId, collectBindings: inputs.map((input) => JSON.stringify(input)) }; + const forecast = await __testing.executeReviewCaptureGroupOperation(parameters, cwd, harness.native, undefined, null, selections, true); + assert.equal(forecast.outcome, "reviewer-model-run-forecast"); + assert.equal(relays, 0); + selections.clear(); + const result = await __testing.executeReviewCaptureGroupOperation({ ...parameters, reviewerRunAcknowledged: true }, cwd, harness.native, undefined, null, selections, true); + assert.equal(result.outcome, "native-reviewer-group-status-reconciled"); + assert.equal(relays, 4); + assert.equal(submissions, 4); + assert.equal(harness.statusCalls.length, 8); +}); + +test("capture route recovery preserves the committed group selector after partial route loss", async (t) => { + const candidateViews = new CandidateViewRegistry(); + t.after(() => { candidateViews.cleanupAll(); __testing.setReviewHostRelayGroupRunnersForTesting(); }); + const cwd = repository(t), lineageId = "recovered-committed-group", inputs = groupInputs(lineageId), selections = new Map(); + const view = candidateViews.create({ contributorRoot: cwd, baseRef: execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim(), committedOnly: true }); + candidateViews.retain(view.token, lineageId); + const target = candidateViews.resolveProjection(lineageId, cwd), requests: Array> = []; + const statuses = [finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), ...inputs.map((_input, index) => finalizeStatus(lineageId, inputs.slice(index))), finalizeStatus(lineageId)]; + const native = { targetStatus: async (request: Record) => { requests.push(request); return statuses.shift()!; } } as unknown as NativeReviewCli; + __testing.setReviewHostRelayGroupRunnersForTesting(async (requests) => requests.map(prepared), async () => ({ promptByteLength: 64, resultByteLength: 32, submission: "{}" })); + const parameters = { lineageId, collectBindings: inputs.map((input) => JSON.stringify(input)) }; + const forecast = await __testing.executeReviewCaptureGroupOperation(parameters, cwd, native, undefined, candidateViews, selections, true); + assert.equal(forecast.outcome, "reviewer-model-run-forecast"); + // Leave only a later matching route, rather than assuming the first survives. + for (const key of [...selections.keys()].slice(0, 3)) selections.delete(key); + const result = await __testing.executeReviewCaptureGroupOperation({ ...parameters, reviewerRunAcknowledged: true }, cwd, native, undefined, candidateViews, selections, true); + assert.equal(result.outcome, "native-reviewer-group-status-reconciled"); + assert.equal(requests.length, 7); + assert.ok(requests.every((request) => request.baseRef === target.baseCommit && request.committedOnly === true && request.agent === "pi")); +}); + +test("capture route recovery rejects known workspace, lineage and selector route mismatches", async (t) => { + const cwd = repository(t), other = repository(t), lineageId = "known-route-boundaries", inputs = groupInputs(lineageId), selections = new Map(); + const harness = nativeHarness([finalizeStatus(lineageId, inputs)]); + await __testing.executeReviewControllerOperation({ operation: "status", lineageId, input: JSON.stringify({ baseRef: "trusted-base", committedOnly: true }) }, cwd, harness.native, undefined, null, undefined, selections); + for (const parameters of [{ lineageId, workspaceRoot: other }, { lineageId: "foreign" }]) { + const single = await __testing.executeReviewCaptureOperation({ ...parameters, collectBinding: JSON.stringify(inputs[0]), reviewerRunAcknowledged: true }, cwd, harness.native, undefined, null, selections, true); + assert.equal(single.outcome, "capture-binding-rejected"); + const group = await __testing.executeReviewCaptureGroupOperation({ ...parameters, collectBindings: inputs.map((input) => JSON.stringify(input)), reviewerRunAcknowledged: true }, cwd, harness.native, undefined, null, selections, true); + assert.equal(group.outcome, "capture-group-rejected"); + } + const key = [...selections.keys()][1]!; + selections.set(key, { ...selections.get(key), baseRef: "wrong-base" }); + const mixed = await __testing.executeReviewCaptureGroupOperation({ lineageId, collectBindings: inputs.map((input) => JSON.stringify(input)), reviewerRunAcknowledged: true }, cwd, harness.native, undefined, null, selections, true); + assert.equal(mixed.outcome, "capture-group-rejected"); + assert.equal(harness.statusCalls.length, 1, "known route mismatches never retarget native STATUS or launch capture"); +}); + +test("capture route recovery rejects fresh foreign, terminal, stale and malformed groups without relay", async (t) => { + t.after(() => { __testing.setReviewHostRelayRunnerForTesting(); __testing.setReviewHostRelayGroupRunnersForTesting(); }); + const cwd = repository(t), lineageId = "recovery-negatives", inputs = groupInputs(lineageId); + let relays = 0; + __testing.setReviewHostRelayRunnerForTesting(async () => { relays += 1; throw new Error("unexpected relay"); }); + __testing.setReviewHostRelayGroupRunnersForTesting(async () => { relays += 1; throw new Error("unexpected group"); }, async () => { throw new Error("unexpected submission"); }); + const current = finalizeStatus(lineageId, inputs); + for (const fresh of [ + finalizeStatus(lineageId), finalizeStatus("foreign", inputs), + { ...current, targetIdentity: `sha256:${"9".repeat(64)}` }, + { ...current, applicability: "unrelated" }, + { ...current, authority: { ...current.authority!, state: "approved" } }, + finalizeStatus(lineageId, groupInputs(lineageId, PHASE_REVISION)), + ] as ReviewStatusV3[]) { + const single = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(inputs[0]), reviewerRunAcknowledged: true }, cwd, nativeHarness([fresh]).native, undefined, null, new Map(), true); + assert.equal(single.outcome, "capture-binding-rejected"); + if (fresh.applicability !== "current_target" || fresh.authority?.state === "approved") { + const routes = new Map(); + const listed = await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, cwd, nativeHarness([fresh]).native, undefined, null, undefined, routes); + assert.deepEqual(listed.collectBindings, [], "publication follows route-registration eligibility"); + assert.equal(routes.size, 0); + } + const group = await __testing.executeReviewCaptureGroupOperation({ lineageId, collectBindings: inputs.map((input) => JSON.stringify(input)), reviewerRunAcknowledged: true }, cwd, nativeHarness([fresh]).native, undefined, null, new Map(), true); + assert.equal(group.outcome, "capture-group-rejected"); + } + for (const submitted of [inputs.slice(1), [inputs[0], inputs[0], ...inputs.slice(2)], [...inputs].reverse(), [...inputs.slice(0, 3), relayCollectInput("foreign", "review-reliability", 3)]]) { + const group = await __testing.executeReviewCaptureGroupOperation({ lineageId, collectBindings: submitted.map((input) => JSON.stringify(input)), reviewerRunAcknowledged: true }, cwd, nativeHarness([current]).native, undefined, null, new Map(), true); + assert.equal(group.outcome, "capture-group-rejected"); + } + assert.equal(relays, 0); +}); + test("one materialize binding routes exactly one provider slot through the host relay", async (t) => { t.after(() => __testing.setReviewHostRelayRunnerForTesting()); const cwd = repository(t);