diff --git a/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts b/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts index c3ef88c40e29..15a8259d05a3 100644 --- a/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts +++ b/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts @@ -90,7 +90,6 @@ const installRunEmbeddedMocks = () => { }; let runEmbeddedAgent: typeof import("./embedded-agent-runner/run.js").runEmbeddedAgent; -let authProfileUsageTesting: typeof import("./auth-profiles/usage.test-support.js").testing; let createDiagnosticLogRecordCaptureFn: typeof import("../logging/test-helpers/diagnostic-log-capture.js").createDiagnosticLogRecordCapture; let cleanupLogCapture: (() => void) | undefined; let resetLoggerFn: typeof import("../logging/logger.js").resetLogger; @@ -101,7 +100,6 @@ beforeAll(async () => { vi.resetModules(); installRunEmbeddedMocks(); ({ runEmbeddedAgent } = await import("./embedded-agent-runner/run.js")); - ({ testing: authProfileUsageTesting } = await import("./auth-profiles/usage.test-support.js")); ({ createDiagnosticLogRecordCapture: createDiagnosticLogRecordCaptureFn } = await import("../logging/test-helpers/diagnostic-log-capture.js")); ({ resetLogger: resetLoggerFn, setLoggerOverride: setLoggerOverrideFn } = @@ -141,7 +139,6 @@ beforeEach(() => { afterEach(() => { globalThis.fetch = originalFetch; - authProfileUsageTesting.setDepsForTest(null); cleanupLogCapture?.(); cleanupLogCapture = undefined; setLoggerOverrideFn(null); @@ -937,46 +934,6 @@ describe("runEmbeddedAgent auth profile rotation", () => { expect(sleepWithAbortMock).not.toHaveBeenCalled(); }); - it("does not wait for prompt failure cooldown marking before retrying", async () => { - let releaseMark: (() => void) | undefined; - const markCanFinish = new Promise((resolve) => { - releaseMark = resolve; - }); - let markStarted = false; - authProfileUsageTesting.setDepsForTest({ - updateAuthProfileStoreWithLock: async () => { - markStarted = true; - await markCanFinish; - return null; - }, - }); - - try { - await withAgentWorkspace(async ({ agentDir, workspaceDir }) => { - await writeAuthStore(agentDir); - mockPromptErrorThenSuccessfulAttempt("rate limit exceeded"); - - const runPromise = runAutoPinnedOpenAiTurn({ - agentDir, - workspaceDir, - sessionKey: "agent:test:prompt-deferred-mark", - runId: "run:prompt-deferred-mark", - }); - - await vi.waitFor(() => expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(2)); - expect(markStarted).toBe(true); - releaseMark?.(); - releaseMark = undefined; - await runPromise; - - const usageStats = await readUsageStats(agentDir); - expect(typeof usageStats["openai:p2"]?.lastUsed).toBe("number"); - }); - } finally { - releaseMark?.(); - } - }); - it("rotates on timeout without cooling down the timed-out profile", async () => { const { usageStats } = await runAutoPinnedRotationCase({ errorMessage: "request ended without sending any chunks", diff --git a/src/agents/embedded-agent-runner/run/prompt-failure.test.ts b/src/agents/embedded-agent-runner/run/prompt-failure.test.ts new file mode 100644 index 000000000000..d1919f6a8cb9 --- /dev/null +++ b/src/agents/embedded-agent-runner/run/prompt-failure.test.ts @@ -0,0 +1,107 @@ +// Prompt failure tests cover retry composition around profile rotation. +import { describe, expect, it, vi } from "vitest"; +import { handleEmbeddedPromptFailure } from "./prompt-failure.js"; + +type Params = Parameters[0]; + +function makeParams(overrides: Partial = {}): Params { + const provider = "openai"; + const modelId = "gpt-5"; + const defaults: Params = { + runParams: { + config: undefined, + runId: "run:prompt-failure-test", + } as Params["runParams"], + attempt: { + replayMetadata: { + replaySafe: true, + }, + } as Params["attempt"], + promptError: new Error("rate limit exceeded"), + promptErrorSource: "prompt", + activeErrorContext: { provider, model: modelId }, + provider, + modelId, + authProfileId: "openai:p1", + authProfileStore: { + version: 1, + profiles: {}, + }, + sessionIdUsed: "session:prompt-failure-test", + lane: "test", + agentDir: "/tmp/openclaw-prompt-failure-test", + suspensionSessionId: "session:prompt-failure-test", + runtimeAuthRetry: false, + maybeRefreshRuntimeAuthForAuthError: vi.fn(async () => false), + suspendForFailure: vi.fn(), + resolveReplayInvalid: vi.fn(() => false), + setTerminalLifecycleMeta: vi.fn(), + buildErrorAgentMeta: vi.fn(), + startedAtMs: 0, + fallbackConfigured: true, + aborted: false, + externalAbort: false, + pluginHarnessOwnsTransport: false, + timedOutByRunBudget: false, + resolveAuthProfileFailureReason: vi.fn(() => "rate_limit"), + maybeEscalateRateLimitProfileFallback: vi.fn(), + advanceAttemptAuthProfile: vi.fn(async () => true), + maybeMarkAuthProfileFailure: vi.fn(async () => {}), + maybeBackoffBeforeOverloadFailover: vi.fn(async () => {}), + attemptedThinking: new Set(), + thinkLevel: "low", + getThinkLevel: () => "low", + traceAttempts: [], + previousRetryFailoverReason: null, + }; + return { ...defaults, ...overrides }; +} + +describe("handleEmbeddedPromptFailure", () => { + it("returns the profile-rotation retry before failure marking finishes", async () => { + const events: string[] = []; + let releaseMark: (() => void) | undefined; + const markCanFinish = new Promise((resolve) => { + releaseMark = resolve; + }); + const maybeMarkAuthProfileFailure = vi.fn(async () => { + events.push("mark-start"); + await markCanFinish; + events.push("mark-finish"); + }); + + try { + const outcome = await handleEmbeddedPromptFailure( + makeParams({ + advanceAttemptAuthProfile: vi.fn(async () => { + events.push("advance"); + return true; + }), + maybeMarkAuthProfileFailure, + maybeBackoffBeforeOverloadFailover: vi.fn(async () => { + events.push("backoff"); + }), + }), + ); + + expect(outcome).toEqual({ + action: "retry", + thinkLevel: "low", + authRetryPending: false, + lastRetryFailoverReason: "rate_limit", + }); + expect(events).toEqual(["advance", "mark-start", "backoff"]); + expect(maybeMarkAuthProfileFailure).toHaveBeenCalledWith({ + profileId: "openai:p1", + reason: "rate_limit", + modelId: "gpt-5", + }); + } finally { + releaseMark?.(); + } + + await vi.waitFor(() => + expect(events).toEqual(["advance", "mark-start", "backoff", "mark-finish"]), + ); + }); +});