diff --git a/devlog/_plan/260818_bug_pr_resolution/010_wpv_stabilization_audit.md b/devlog/_plan/260818_bug_pr_resolution/010_wpv_stabilization_audit.md new file mode 100644 index 0000000000..902b9b8743 --- /dev/null +++ b/devlog/_plan/260818_bug_pr_resolution/010_wpv_stabilization_audit.md @@ -0,0 +1,50 @@ +# 010 — WP-V stabilization audit (post-interruption) + +Context: prior session (thread 01a0138d) was interrupted mid-campaign by a codex +runtime error; user reports "too much merged too fast" and asks for a full +main..dev merge appropriateness audit + CI + lidge suite before continuing. + +Range: origin/main (e97fb2621, v2.25.0) .. origin/dev (aaf04690e), 125 commits. +A new push to dev re-opens ALL THREE verifiers (CI, lidge suite, whole-delta diff). + +## Audit lanes (parallel, gpt-5.6-sol medium, read-only) + +- Lane A — campaign land-* merges: #2015(1800) #2016(2007) #2017(1990) + #2018(1889+1883-followup) #2020(1896) #2021(1932). Check: matrix verdict match, + rebase correctness (vpr-* merge shape), tests present, no scope creep. +- Lane B — campaign batch merges: 1991 1931 1912 1859 1847 1845 (merge), + 1935 1725 1851 (squash), 1883 (squash, workflow security). Check: matrix match, + squash-vs-merge shape as prescribed, workflow security for 1883. +- Lane C — pre-campaign merges on dev: 1928 1941 2005 1904 1965 1893 1949 + 1944-1947 1998 1997 + docs 2004-2014 + b5a98d690 release-audit fixes. + Check: each is a reviewed, coherent landing; docs merges are docs-only. +- Lane D — whole-delta security/semantic scan: git diff origin/main..origin/dev + focused on src/ high-risk surfaces, explicitly including: + .github/scripts/install-copilot-cli.sh + run-copilot-inference.cjs (supply chain, + #1883), src/lib/windows-service-wrappers.ts + windows-atomic-replace.ts + (privileged kill/replace), src/server/management/system-routes.ts + shared.ts + (management API), src/codex/auth-api.ts + plan-from-token.ts (#1998/#1932 WHAM + 401 gating), src/oauth/google-antigravity.ts (#1889), src/lib/redact.ts, + scripts/build-release-changelog.ts (#1847), MiniMax loopback pin e9d879b34. + +## Verifiers (PLAN-VERIFIER-REAL-01) + +- gh run watch 32130622133 (Cross-platform CI on aaf04690e) — observes dev head; running now. +- ssh lidge full suite (typecheck + bun test --isolate tests + privacy:scan) in a + DEDICATED git worktree pinned at aaf04690e (~/.wpv-suite-aaf04690e). The shared + ~/Developer/opencodex checkout is owned by a concurrent session (split-wp1b) and + was swapped mid-run — the first suite attempt (ssh session 27978) is VOID. +- Failure baseline: any lidge failure is classified by bisect-attribution into + e97fb2621..aaf04690e (ours) vs reproduction at 0f5ccf9aa pre-campaign tip + (preexisting). No judgment-call classifications. +- Local dirty worktree (#1748 delta) is stashed out of scope for WP-V; it belongs to wp6. + +## Accept criteria + +- Every merge group has a verdict: OK / SUSPECT(reason) / REGRESSION(evidence); + coverage list is exact over all 125 commits (incl. docs #2004). +- CI conclusion recorded for exact SHA aaf04690e (or successor if new pushes land). +- lidge suite exit codes recorded; failures classified ours-vs-preexisting. +- Any REGRESSION gets fix-forward or targeted revert in B, re-verified in C. + +Out of scope: wp6-wp11 work (later cycles). diff --git a/gui/tests/models-empty-provider.test.tsx b/gui/tests/models-empty-provider.test.tsx index 442e96bf17..296c85d2b1 100644 --- a/gui/tests/models-empty-provider.test.tsx +++ b/gui/tests/models-empty-provider.test.tsx @@ -221,8 +221,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a expect(container.querySelector(".badge.badge-amber")?.textContent).toContain("Discovery failed"); expect(container.textContent).not.toContain("Not selected"); - await act(async () => buttonText("Context windows").click()); - const contextDialog = container.querySelector('[role="dialog"][aria-label="Context windows"]')!; + await act(async () => buttonText("Custom windows").click()); + const contextDialog = container.querySelector('[role="dialog"][aria-label="Custom windows"]')!; const contextInputs = contextDialog.querySelectorAll("input"); expect([...contextInputs].map(input => input.value)).toEqual(["256000", "64000"]); const setValue = Object.getOwnPropertyDescriptor( @@ -275,10 +275,10 @@ test("Models page combines final visibility, atomic actions, discovery status, a contextWindow: 350_000, modelContextWindows: { "claude-opus": 100_000, "claude-sonnet": 80_000 }, }); - expect(container.querySelector('[role="dialog"][aria-label="Context windows"]')).toBeNull(); + expect(container.querySelector('[role="dialog"][aria-label="Custom windows"]')).toBeNull(); - await act(async () => buttonText("Context windows").click()); - const refreshFailureDialog = container.querySelector('[role="dialog"][aria-label="Context windows"]')!; + await act(async () => buttonText("Custom windows").click()); + const refreshFailureDialog = container.querySelector('[role="dialog"][aria-label="Custom windows"]')!; failCatalog = true; // Make an actual edit. Apply now compares against the values the modal opened with, so a // reopened-and-untouched dialog sends nothing — which would leave this case asserting the @@ -296,15 +296,15 @@ test("Models page combines final visibility, atomic actions, discovery status, a }); expect(contextBodies).toHaveLength(2); expect(contextBodies.at(-1)).toEqual({ contextWindow: 360_000 }); - expect(container.querySelector('[role="dialog"][aria-label="Context windows"]')).toBeNull(); + expect(container.querySelector('[role="dialog"][aria-label="Custom windows"]')).toBeNull(); expect(container.textContent).toContain("Context windows updated"); failCatalog = false; // An edit that is typed and then restored is not a change — and neither is retyping the // same number in a different shape. Comparing raw text instead of parsed values would // treat "64,000" as an edit and stamp a stale number over whatever else moved. - await act(async () => buttonText("Context windows").click()); - const revertDialog = container.querySelector('[role="dialog"][aria-label="Context windows"]')!; + await act(async () => buttonText("Custom windows").click()); + const revertDialog = container.querySelector('[role="dialog"][aria-label="Custom windows"]')!; const revertInput = revertDialog.querySelectorAll("input.input")[0]!; const openingValue = revertInput.value; await act(async () => { @@ -320,10 +320,10 @@ test("Models page combines final visibility, atomic actions, discovery status, a await new Promise(resolve => testWindow.setTimeout(resolve, 0)); }); expect(contextBodies).toHaveLength(2); - expect(container.querySelector('[role="dialog"][aria-label="Context windows"]')).toBeNull(); + expect(container.querySelector('[role="dialog"][aria-label="Custom windows"]')).toBeNull(); - await act(async () => buttonText("Context windows").click()); - const reformatDialog = container.querySelector('[role="dialog"][aria-label="Context windows"]')!; + await act(async () => buttonText("Custom windows").click()); + const reformatDialog = container.querySelector('[role="dialog"][aria-label="Custom windows"]')!; const reformatInput = reformatDialog.querySelectorAll("input.input")[0]!; const commaFormatted = reformatInput.value.replace(/\B(?=(\d{3})+(?!\d))/g, ","); await act(async () => { @@ -353,8 +353,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a // The poll has to actually run: mutating the mock alone leaves React's `groups` on the // opening values, and then comparing drafts against LIVE state — the defect — would look // identical to comparing against the snapshot. - await act(async () => buttonText("Context windows").click()); - const concurrentDialog = container.querySelector('[role="dialog"][aria-label="Context windows"]')!; + await act(async () => buttonText("Custom windows").click()); + const concurrentDialog = container.querySelector('[role="dialog"][aria-label="Custom windows"]')!; providerContextWindow = 300_000; providerModelContextWindows = { ...providerModelContextWindows, "claude-opus": 96_000 }; await act(async () => { poll(); await new Promise(resolve => testWindow.setTimeout(resolve, 0)); }); @@ -379,8 +379,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a // `groups` instead of the opening snapshot. The cases above cannot see that swap, because // in each of them the user's value genuinely differs from both. This one does — the user // touches a field and puts it back, while the server moves underneath. - await act(async () => buttonText("Context windows").click()); - const staleDialog = container.querySelector('[role="dialog"][aria-label="Context windows"]')!; + await act(async () => buttonText("Custom windows").click()); + const staleDialog = container.querySelector('[role="dialog"][aria-label="Custom windows"]')!; const staleDefaultInput = staleDialog.querySelectorAll("input.input")[0]!; const staleOpeningDefault = staleDefaultInput.value; await act(async () => { @@ -419,8 +419,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a // anyone whose config was hand-edited before the safe-integer bound existed. providerContextWindow = 1e100; await act(async () => { poll(); await new Promise(resolve => testWindow.setTimeout(resolve, 0)); }); - await act(async () => buttonText("Context windows").click()); - const unsafeDefaultDialog = container.querySelector('[role="dialog"][aria-label="Context windows"]')!; + await act(async () => buttonText("Custom windows").click()); + const unsafeDefaultDialog = container.querySelector('[role="dialog"][aria-label="Custom windows"]')!; await pickContextModel("claude-sonnet", unsafeDefaultDialog); const unsafeSiblingInput = unsafeDefaultDialog.querySelectorAll("input.input")[1]!; await act(async () => { @@ -438,8 +438,8 @@ test("Models page combines final visibility, atomic actions, discovery status, a // `Number.isInteger(1e100)` is true, and the server rejects it. Accepting it in the form // would turn a typo into a round-trip error instead of inline feedback. const patchesBeforeUnsafe = contextBodies.length; - await act(async () => buttonText("Context windows").click()); - const unsafeDialog = container.querySelector('[role="dialog"][aria-label="Context windows"]')!; + await act(async () => buttonText("Custom windows").click()); + const unsafeDialog = container.querySelector('[role="dialog"][aria-label="Custom windows"]')!; const unsafeInput = unsafeDialog.querySelectorAll("input.input")[0]!; await act(async () => { setValue.call(unsafeInput, "1e100"); @@ -454,7 +454,7 @@ test("Models page combines final visibility, atomic actions, discovery status, a // Relative, not absolute: an absolute count silently re-targets whenever a case is added // above, and the property under test is "this Apply wrote nothing". expect(contextBodies).toHaveLength(patchesBeforeUnsafe); - expect(container.querySelector('[role="dialog"][aria-label="Context windows"]')).not.toBeNull(); + expect(container.querySelector('[role="dialog"][aria-label="Custom windows"]')).not.toBeNull(); // The modal staying open is not the point — the user has to be TOLD why. Without this the // test passes on a silent no-op that looks identical to a hang. expect(unsafeDialog.textContent).toContain("Context windows must be positive whole numbers"); diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index cda4108adc..59d7f1bfd4 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -3669,7 +3669,12 @@ async function handleResponsesInner( }), }); } else { - upstreamResponse = await fetchWithTransientRetry( + // #1851 scope guard: transient-5xx retry on this generic adapter path is opt-in for + // direct Google AI Studio only (Vertex/Antigravity use fetchResponse above). Other + // adapters keep reset-only retry so combo failover still hops on the first 5xx + // instead of burning ~1.2s of same-target retries per hop. + const fetchWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry; + upstreamResponse = await fetchWithRetryPolicy( recovery => { noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery); return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({ @@ -4099,7 +4104,10 @@ async function handleResponsesInner( }), }); } - return await fetchWithTransientRetry( + // Same #1851 scope guard as the initial send: transient-5xx retry only for direct + // Google AI Studio; every other adapter keeps reset-only semantics here. + const fetchContinuationWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry; + return await fetchContinuationWithRetryPolicy( recovery => { noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind); return fetchWithHeaderTimeout( diff --git a/tests/bridge-raw-reasoning-hidden.test.ts b/tests/bridge-raw-reasoning-hidden.test.ts index 187015c653..f84c896ecd 100644 --- a/tests/bridge-raw-reasoning-hidden.test.ts +++ b/tests/bridge-raw-reasoning-hidden.test.ts @@ -77,17 +77,18 @@ describe("hidden raw reasoning (hideThinkingSummary parity for reasoning_raw_del expect(fc).toMatchObject({ call_id: "call_1", name: "read_file" }); }); - test("streamed visible (flag off): current raw shape unchanged", async () => { + test("streamed visible (flag off): raw reasoning rides the expandable summary channel (#2007)", async () => { const frames = await collectSse(bridgeToResponsesSSE(replay([ { type: "reasoning_raw_delta", text: "visible raw" }, { type: "done" }, ]), "routed/model")); - expect(frames.some(f => f.event === "response.reasoning_text.delta")).toBe(true); + expect(frames.some(f => f.event === "response.reasoning_summary_text.delta")).toBe(true); + expect(frames.some(f => f.event === "response.reasoning_text.delta")).toBe(false); const completed = frames.find(f => f.event === "response.completed")?.data.response as Record; const output = completed.output as Record[]; expect(output[0]).toMatchObject({ - type: "reasoning", summary: [], - content: [{ type: "reasoning_text", text: "visible raw" }], + type: "reasoning", + summary: [{ type: "summary_text", text: "visible raw" }], }); }); @@ -117,14 +118,14 @@ describe("hidden raw reasoning (hideThinkingSummary parity for reasoning_raw_del expect(decodeReasoningEnvelope(reasoning.encrypted_content as string)?.txt).toBe("quiet"); }); - test("non-streaming visible: raw shape unchanged", () => { + test("non-streaming visible: raw reasoning lands in the summary channel (#2007)", () => { const json = buildResponseJSON([ { type: "reasoning_raw_delta", text: "loud" }, { type: "done" }, ], "routed/model", {}); const output = (json as { output: Record[] }).output; expect(output.find(o => o.type === "reasoning")).toMatchObject({ - content: [{ type: "reasoning_text", text: "loud" }], + summary: [{ type: "summary_text", text: "loud" }], }); }); diff --git a/tests/cli-restore-back.test.ts b/tests/cli-restore-back.test.ts index 8acaa301fd..6ddb32c02c 100644 --- a/tests/cli-restore-back.test.ts +++ b/tests/cli-restore-back.test.ts @@ -99,7 +99,12 @@ describe("ocx restore back", () => { CI: "1", }); expect(result.status).toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toContain("Codex integration is OFF; sync skipped and no Codex files changed."); + // #1931: explicit sync now refreshes the ocx-side catalog/cache while OFF when a + // catalog source exists ("refreshed") and reports "refresh skipped" otherwise + // (CI has no Codex catalog source). The durable policy invariant is the same in + // both: Codex config is untouched (mtime asserted below). + const combined = `${result.stdout}\n${result.stderr}`; + expect(combined).toMatch(/Codex integration is OFF; catalog (and models cache refreshed|refresh skipped), Codex config untouched\./); expect(statSync(configPath).mtimeMs).toBe(before); } finally { rmSync(codexHome, { recursive: true, force: true }); diff --git a/tests/codex-app-server-processes.test.ts b/tests/codex-app-server-processes.test.ts index 4a63be2f2e..c3cd7dad56 100644 --- a/tests/codex-app-server-processes.test.ts +++ b/tests/codex-app-server-processes.test.ts @@ -389,7 +389,8 @@ describe("CLI /api sync wiring for stale app-servers (#476)", () => { // The property under test is unchanged: app-servers are touched only after a // write actually landed, never on a refused/failed serialization attempt. expect(syncCacheCase).toContain("withCatalogWriteSerialization"); - expect(syncCacheCase).toContain("invalidateCodexModelsCacheWithPermit(permit, owningCodexHome)"); + // #1931: explicit sync-cache refreshes even when injection is OFF (side profiles). + expect(syncCacheCase).toContain("invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, { allowWhenDesiredDisabled: true })"); const gate = 'if (invalidated.kind === "completed" && invalidated.value)'; expect(syncCacheCase).toContain(gate); expect(syncCacheCase).toContain("afterCatalogWriteHandleAppServers"); diff --git a/tests/commandcode-provider.test.ts b/tests/commandcode-provider.test.ts index 695b3954d0..bb358bf444 100644 --- a/tests/commandcode-provider.test.ts +++ b/tests/commandcode-provider.test.ts @@ -192,7 +192,8 @@ describe("Command Code provider", () => { const deepseek = models.find(row => row.id === "deepseek/deepseek-v4-flash")!; expect(deepseek.contextWindow).toBe(1_000_000); expect(deepseek.owned_by).toBe("command-code"); - expect(deepseek.reasoningEfforts).toEqual([]); + // #1800: discovered models now surface the curated effort table (command-code-efforts.ts). + expect(deepseek.reasoningEfforts).toEqual(["high", "max"]); const haiku = models.find(row => row.id === "claude-haiku-4-5-20251001")!; expect(haiku.contextWindow).toBe(200_000);