

























@@ -14,6 +14,7 @@ import {
1414CodexAppServerEventProjector,
1515type CodexAppServerToolTelemetry,
1616} from "./event-projector.js";
17+import { rememberCodexRateLimits, resetCodexRateLimitCacheForTests } from "./rate-limit-cache.js";
1718import { createCodexTestModel } from "./test-support.js";
18191920const THREAD_ID = "thread-1";
@@ -86,6 +87,7 @@ beforeEach(() => {
8687afterEach(async () => {
8788resetAgentEventsForTest();
8889resetGlobalHookRunner();
90+resetCodexRateLimitCacheForTests();
8991vi.restoreAllMocks();
9092for (const tempDir of tempDirs) {
9193await fs.rm(tempDir, { recursive: true, force: true });
@@ -140,6 +142,23 @@ function appServerError(params: { message: string; willRetry: boolean }): Projec
140142});
141143}
142144145+function rateLimitsUpdated(resetsAt: number): ProjectorNotification {
146+return {
147+method: "account/rateLimits/updated",
148+params: {
149+rateLimits: {
150+limitId: "codex",
151+limitName: "Codex",
152+primary: { usedPercent: 100, windowDurationMins: 300, resetsAt },
153+secondary: null,
154+credits: null,
155+planType: "plus",
156+rateLimitReachedType: "rate_limit_reached",
157+},
158+},
159+} as ProjectorNotification;
160+}
161+143162function turnCompleted(items: unknown[] = []): ProjectorNotification {
144163return {
145164method: "turn/completed",
@@ -280,6 +299,95 @@ describe("CodexAppServerEventProjector", () => {
280299expect(result.lastAssistant).toBeUndefined();
281300});
282301302+it("uses Codex rate-limit resets for usage-limit app-server errors", async () => {
303+const projector = await createProjector();
304+const resetsAt = Math.ceil(Date.now() / 1000) + 120;
305+306+await projector.handleNotification(rateLimitsUpdated(resetsAt));
307+await projector.handleNotification(
308+forCurrentTurn("error", {
309+error: {
310+message: "You've reached your usage limit.",
311+codexErrorInfo: "usageLimitExceeded",
312+additionalDetails: null,
313+},
314+willRetry: false,
315+}),
316+);
317+318+const result = projector.buildResult(buildEmptyToolTelemetry());
319+320+expect(result.promptError).toContain("You've reached your Codex subscription usage limit.");
321+expect(result.promptError).toContain("Next reset in");
322+expect(result.promptError).toContain("Run /codex account");
323+expect(result.promptErrorSource).toBe("prompt");
324+});
325+326+it("uses Codex rate-limit resets for failed turns", async () => {
327+const projector = await createProjector();
328+const resetsAt = Math.ceil(Date.now() / 1000) + 120;
329+330+await projector.handleNotification(rateLimitsUpdated(resetsAt));
331+await projector.handleNotification(
332+forCurrentTurn("turn/completed", {
333+turn: {
334+id: TURN_ID,
335+status: "failed",
336+error: {
337+message: "You've reached your usage limit.",
338+codexErrorInfo: "usageLimitExceeded",
339+additionalDetails: null,
340+},
341+items: [],
342+},
343+}),
344+);
345+346+const result = projector.buildResult(buildEmptyToolTelemetry());
347+348+expect(result.promptError).toContain("You've reached your Codex subscription usage limit.");
349+expect(result.promptError).toContain("Next reset in");
350+expect(result.promptErrorSource).toBe("prompt");
351+});
352+353+it("uses a recent Codex rate-limit snapshot when failed turns omit reset details", async () => {
354+const projector = await createProjector();
355+const resetsAt = Math.ceil(Date.now() / 1000) + 120;
356+rememberCodexRateLimits({
357+rateLimits: {
358+limitId: "codex",
359+limitName: "Codex",
360+primary: { usedPercent: 100, windowDurationMins: 300, resetsAt },
361+secondary: null,
362+credits: null,
363+planType: "plus",
364+rateLimitReachedType: "rate_limit_reached",
365+},
366+rateLimitsByLimitId: null,
367+});
368+369+await projector.handleNotification(
370+forCurrentTurn("turn/completed", {
371+turn: {
372+id: TURN_ID,
373+status: "failed",
374+error: {
375+message: "You've reached your usage limit.",
376+codexErrorInfo: "usageLimitExceeded",
377+additionalDetails: null,
378+},
379+items: [],
380+},
381+}),
382+);
383+384+const result = projector.buildResult(buildEmptyToolTelemetry());
385+386+expect(result.promptError).toContain("You've reached your Codex subscription usage limit.");
387+expect(result.promptError).toContain("Next reset in");
388+expect(result.promptErrorSource).toBe("prompt");
389+});
390+283391it("normalizes snake_case current token usage fields", async () => {
284392const projector = await createProjector();
285393此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。