


























@@ -27,6 +27,7 @@ import { CODEX_GPT5_BEHAVIOR_CONTRACT } from "../../prompt-overlay.js";
2727import { defaultCodexAppInventoryCache } from "./app-inventory-cache.js";
2828import * as authBridge from "./auth-bridge.js";
2929import { resolveCodexAppServerEnvApiKeyCacheKey } from "./auth-bridge.js";
30+import { CodexAppServerRpcError } from "./client.js";
3031import { readCodexPluginConfig, resolveCodexAppServerRuntimeOptions } from "./config.js";
3132import {
3233CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
@@ -65,6 +66,7 @@ import {
6566} from "./sandbox-exec-server.js";
6667import { createSandboxContext } from "./sandbox-exec-server.test-helpers.js";
6768import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js";
69+import * as sharedClientModule from "./shared-client.js";
6870import { createCodexTestModel } from "./test-support.js";
6971import { buildTurnStartParams, startOrResumeThread } from "./thread-lifecycle.js";
7072@@ -3489,6 +3491,134 @@ describe("runCodexAppServerAttempt", () => {
34893491]);
34903492});
349134933494+it("does not retire the shared Codex client when a spawned helper run fails with a logical thread/start error", async () => {
3495+const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent");
3496+clearSpy.mockClear();
3497+let failedClient: unknown;
3498+setCodexAppServerClientFactoryForTest(async () => {
3499+const c = {
3500+request: vi.fn(async (method: string) => {
3501+if (method === "thread/start") {
3502+throw new CodexAppServerRpcError(
3503+{ message: "401 authentication_error: Invalid bearer token" },
3504+"thread/start",
3505+);
3506+}
3507+return {};
3508+}),
3509+addNotificationHandler: vi.fn(() => () => undefined),
3510+addRequestHandler: vi.fn(() => () => undefined),
3511+};
3512+failedClient = c;
3513+return c as never;
3514+});
3515+const params = createParams(
3516+path.join(tempDir, "session.jsonl"),
3517+path.join(tempDir, "workspace"),
3518+);
3519+params.spawnedBy = "agent:main:session-parent";
3520+3521+await expect(runCodexAppServerAttempt(params)).rejects.toThrow("Invalid bearer token");
3522+const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === failedClient);
3523+expect(calledWithFailedClient).toBe(false);
3524+clearSpy.mockRestore();
3525+});
3526+3527+it("retires the shared Codex client when a spawned helper run times out during thread/start", async () => {
3528+const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent");
3529+clearSpy.mockClear();
3530+let failedClient: unknown;
3531+setCodexAppServerClientFactoryForTest(async () => {
3532+const c = {
3533+request: vi.fn(async (method: string) => {
3534+if (method === "thread/start") {
3535+return await new Promise<never>(() => undefined);
3536+}
3537+return {};
3538+}),
3539+addNotificationHandler: vi.fn(() => () => undefined),
3540+addRequestHandler: vi.fn(() => () => undefined),
3541+};
3542+failedClient = c;
3543+return c as never;
3544+});
3545+const params = createParams(
3546+path.join(tempDir, "session.jsonl"),
3547+path.join(tempDir, "workspace"),
3548+);
3549+params.spawnedBy = "agent:main:session-parent";
3550+params.timeoutMs = 1;
3551+3552+await expect(runCodexAppServerAttempt(params, { startupTimeoutFloorMs: 1 })).rejects.toThrow(
3553+"codex app-server startup timed out",
3554+);
3555+const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === failedClient);
3556+expect(calledWithFailedClient).toBe(true);
3557+clearSpy.mockRestore();
3558+});
3559+3560+it("retires the shared Codex client when a spawned helper hits a thread/start write failure", async () => {
3561+const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent");
3562+clearSpy.mockClear();
3563+let failedClient: unknown;
3564+setCodexAppServerClientFactoryForTest(async () => {
3565+const c = {
3566+request: vi.fn(async (method: string) => {
3567+if (method === "thread/start") {
3568+throw new Error("write EPIPE");
3569+}
3570+return {};
3571+}),
3572+addNotificationHandler: vi.fn(() => () => undefined),
3573+addRequestHandler: vi.fn(() => () => undefined),
3574+};
3575+failedClient = c;
3576+return c as never;
3577+});
3578+const params = createParams(
3579+path.join(tempDir, "session.jsonl"),
3580+path.join(tempDir, "workspace"),
3581+);
3582+params.spawnedBy = "agent:main:session-parent";
3583+3584+await expect(runCodexAppServerAttempt(params)).rejects.toThrow("write EPIPE");
3585+const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === failedClient);
3586+expect(calledWithFailedClient).toBe(true);
3587+clearSpy.mockRestore();
3588+});
3589+3590+it("retires the shared Codex client when a top-level run fails with a logical thread/start error", async () => {
3591+const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent");
3592+clearSpy.mockClear();
3593+let failedClient: unknown;
3594+setCodexAppServerClientFactoryForTest(async () => {
3595+const c = {
3596+request: vi.fn(async (method: string) => {
3597+if (method === "thread/start") {
3598+throw new CodexAppServerRpcError(
3599+{ message: "401 authentication_error: Invalid bearer token" },
3600+"thread/start",
3601+);
3602+}
3603+return {};
3604+}),
3605+addNotificationHandler: vi.fn(() => () => undefined),
3606+addRequestHandler: vi.fn(() => () => undefined),
3607+};
3608+failedClient = c;
3609+return c as never;
3610+});
3611+const params = createParams(
3612+path.join(tempDir, "session.jsonl"),
3613+path.join(tempDir, "workspace"),
3614+);
3615+3616+await expect(runCodexAppServerAttempt(params)).rejects.toThrow("Invalid bearer token");
3617+const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === failedClient);
3618+expect(calledWithFailedClient).toBe(true);
3619+clearSpy.mockRestore();
3620+});
3621+34923622it("passes configured app-server policy, sandbox, service tier, and model on resume", async () => {
34933623const sessionFile = path.join(tempDir, "session.jsonl");
34943624const workspaceDir = path.join(tempDir, "workspace");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。