


























@@ -0,0 +1,233 @@
1+import { beforeAll, beforeEach, describe, expect, it } from "vitest";
2+import { makeModelFallbackCfg } from "../test-helpers/model-fallback-config-fixture.js";
3+import { makeAttemptResult } from "./run.overflow-compaction.fixture.js";
4+import {
5+loadRunOverflowCompactionHarness,
6+MockedFailoverError,
7+mockedClassifyFailoverReason,
8+mockedRunEmbeddedAttempt,
9+overflowBaseRunParams,
10+resetRunOverflowCompactionHarnessMocks,
11+} from "./run.overflow-compaction.harness.js";
12+import type { EmbeddedRunAttemptResult } from "./run/types.js";
13+14+let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
15+16+function codexClientClosedAttempt(
17+overrides: Partial<EmbeddedRunAttemptResult> = {},
18+): EmbeddedRunAttemptResult {
19+return makeAttemptResult({
20+assistantTexts: [],
21+promptError: new Error("codex app-server client closed before turn completed"),
22+promptErrorSource: "prompt",
23+codexAppServerFailure: {
24+kind: "client_closed_before_turn_completed",
25+transport: "stdio",
26+threadId: "thread-1",
27+turnId: "turn-1",
28+replaySafe: true,
29+},
30+ ...overrides,
31+});
32+}
33+34+function successAttempt(): EmbeddedRunAttemptResult {
35+return makeAttemptResult({
36+promptError: null,
37+assistantTexts: ["Done."],
38+});
39+}
40+41+describe("runEmbeddedPiAgent Codex app-server recovery", () => {
42+beforeAll(async () => {
43+({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness());
44+});
45+46+beforeEach(() => {
47+resetRunOverflowCompactionHarnessMocks();
48+mockedClassifyFailoverReason.mockReturnValue(null);
49+});
50+51+it("retries a replay-safe stdio client close once", async () => {
52+mockedRunEmbeddedAttempt
53+.mockResolvedValueOnce(codexClientClosedAttempt())
54+.mockResolvedValueOnce(successAttempt());
55+56+await runEmbeddedPiAgent({
57+ ...overflowBaseRunParams,
58+provider: "codex",
59+model: "gpt-5.5",
60+runId: "run-codex-client-close-retry",
61+});
62+63+expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
64+});
65+66+it("suppresses duplicate Codex prompt mirroring on retry", async () => {
67+mockedRunEmbeddedAttempt
68+.mockResolvedValueOnce(codexClientClosedAttempt())
69+.mockResolvedValueOnce(successAttempt());
70+71+await runEmbeddedPiAgent({
72+ ...overflowBaseRunParams,
73+provider: "codex",
74+model: "gpt-5.5",
75+runId: "run-codex-client-close-retry-mirror",
76+});
77+78+expect(
79+(
80+mockedRunEmbeddedAttempt.mock.calls[1]?.[0] as {
81+suppressNextUserMessagePersistence?: boolean;
82+}
83+).suppressNextUserMessagePersistence,
84+).toBe(true);
85+});
86+87+it("suppresses duplicate user persistence when retrying after the inbound message was persisted", async () => {
88+mockedRunEmbeddedAttempt
89+.mockImplementationOnce(async (attemptParams) => {
90+(
91+attemptParams as {
92+onUserMessagePersisted?: (message: { role: "user"; content: string }) => void;
93+}
94+).onUserMessagePersisted?.({ role: "user", content: overflowBaseRunParams.prompt });
95+return codexClientClosedAttempt();
96+})
97+.mockResolvedValueOnce(successAttempt());
98+99+await runEmbeddedPiAgent({
100+ ...overflowBaseRunParams,
101+provider: "codex",
102+model: "gpt-5.5",
103+runId: "run-codex-client-close-retry-persisted",
104+currentMessageId: "msg-1",
105+});
106+107+expect(
108+(
109+mockedRunEmbeddedAttempt.mock.calls[1]?.[0] as {
110+suppressNextUserMessagePersistence?: boolean;
111+}
112+).suppressNextUserMessagePersistence,
113+).toBe(true);
114+});
115+116+it("does not retry websocket client closes", async () => {
117+mockedRunEmbeddedAttempt.mockResolvedValueOnce(
118+codexClientClosedAttempt({
119+codexAppServerFailure: {
120+kind: "client_closed_before_turn_completed",
121+transport: "websocket",
122+threadId: "thread-1",
123+turnId: "turn-1",
124+replaySafe: true,
125+},
126+}),
127+);
128+129+await expect(
130+runEmbeddedPiAgent({
131+ ...overflowBaseRunParams,
132+provider: "codex",
133+model: "gpt-5.5",
134+runId: "run-codex-client-close-websocket",
135+}),
136+).rejects.toThrow("codex app-server client closed before turn completed");
137+expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1);
138+});
139+140+it("does not retry turn/completed idle timeouts", async () => {
141+mockedRunEmbeddedAttempt.mockResolvedValueOnce(
142+makeAttemptResult({
143+assistantTexts: [],
144+promptError: new Error("codex app-server turn idle timed out waiting for turn/completed"),
145+promptErrorSource: "prompt",
146+codexAppServerFailure: {
147+kind: "turn_completion_idle_timeout",
148+transport: "stdio",
149+threadId: "thread-1",
150+turnId: "turn-1",
151+replaySafe: true,
152+},
153+}),
154+);
155+156+await expect(
157+runEmbeddedPiAgent({
158+ ...overflowBaseRunParams,
159+provider: "codex",
160+model: "gpt-5.5",
161+runId: "run-codex-turn-completion-idle-timeout",
162+}),
163+).rejects.toThrow("codex app-server turn idle timed out waiting for turn/completed");
164+expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1);
165+});
166+167+it("does not hand Codex app-server idle timeouts to model fallback", async () => {
168+mockedClassifyFailoverReason.mockReturnValue("timeout");
169+mockedRunEmbeddedAttempt.mockResolvedValueOnce(
170+makeAttemptResult({
171+assistantTexts: [],
172+promptError: new Error("codex app-server turn idle timed out waiting for turn/completed"),
173+promptErrorSource: "prompt",
174+codexAppServerFailure: {
175+kind: "turn_completion_idle_timeout",
176+transport: "stdio",
177+threadId: "thread-1",
178+turnId: "turn-1",
179+replaySafe: true,
180+},
181+}),
182+);
183+184+const promise = runEmbeddedPiAgent({
185+ ...overflowBaseRunParams,
186+provider: "codex",
187+model: "gpt-5.5",
188+runId: "run-codex-turn-completion-idle-timeout-fallback",
189+config: makeModelFallbackCfg({
190+agents: {
191+defaults: {
192+model: {
193+primary: "openai-codex/gpt-5.5",
194+fallbacks: ["anthropic/claude-opus-4-6"],
195+},
196+},
197+},
198+}),
199+});
200+201+await expect(promise).rejects.not.toBeInstanceOf(MockedFailoverError);
202+await expect(promise).rejects.toThrow(
203+"codex app-server turn idle timed out waiting for turn/completed",
204+);
205+expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1);
206+});
207+208+it("does not retry after visible assistant output", async () => {
209+mockedRunEmbeddedAttempt.mockResolvedValueOnce(
210+codexClientClosedAttempt({
211+assistantTexts: ["partial answer"],
212+codexAppServerFailure: {
213+kind: "client_closed_before_turn_completed",
214+transport: "stdio",
215+threadId: "thread-1",
216+turnId: "turn-1",
217+replaySafe: false,
218+replayBlockedReason: "assistant_output",
219+},
220+}),
221+);
222+223+await expect(
224+runEmbeddedPiAgent({
225+ ...overflowBaseRunParams,
226+provider: "codex",
227+model: "gpt-5.5",
228+runId: "run-codex-client-close-visible-output",
229+}),
230+).rejects.toThrow("codex app-server client closed before turn completed");
231+expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1);
232+});
233+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。