test(agents): isolate prompt cooldown scheduling

This commit is contained in:
Vincent Koc
2026-08-01 06:15:07 +08:00
parent 1786aca43f
commit 811d25f736
2 changed files with 107 additions and 43 deletions

View File

@@ -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<void>((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",

View File

@@ -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<typeof handleEmbeddedPromptFailure>[0];
function makeParams(overrides: Partial<Params> = {}): 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<void>((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"]),
);
});
});