


























@@ -260,6 +260,38 @@ function setAuthRuntimeStore(agentDir: string | undefined, store: AuthProfileSto
260260authRuntimeMock.setStore(agentDir, store);
261261}
262262263+function requireRecord(value: unknown, label: string): Record<string, unknown> {
264+if (!value || typeof value !== "object" || Array.isArray(value)) {
265+throw new Error(`expected ${label}`);
266+}
267+return value as Record<string, unknown>;
268+}
269+270+async function captureRejection(promise: Promise<unknown>): Promise<unknown> {
271+try {
272+await promise;
273+} catch (error) {
274+return error;
275+}
276+throw new Error("expected rejection");
277+}
278+279+function requireFallbackSummaryError(error: unknown): FallbackSummaryError {
280+expect(error).toBeInstanceOf(FallbackSummaryError);
281+if (!(error instanceof FallbackSummaryError)) {
282+throw error;
283+}
284+return error;
285+}
286+287+function requireFailoverError(error: unknown): FailoverError {
288+expect(error).toBeInstanceOf(FailoverError);
289+if (!(error instanceof FailoverError)) {
290+throw error;
291+}
292+return error;
293+}
294+263295async function expectFallsBackToHaiku(params: {
264296provider: string;
265297model: string;
@@ -515,24 +547,24 @@ describe("runWithModelFallback", () => {
515547}),
516548);
517549518-await expect(
519-runWithModelFallback({
520- cfg,
521-provider: "openai",
522-model: "gpt-5.4",
523- run,
524-}),
525-).rejects.toMatchObject({
526-name: "FallbackSummaryError",
527-message: expect.stringContaining(rawError),
528-attempts: expect.arrayContaining([
529-expect.objectContaining({
530-error: rawError,
531-reason: "format",
532-status: 400,
550+const error = requireFallbackSummaryError(
551+await captureRejection(
552+runWithModelFallback({
553+ cfg,
554+provider: "openai",
555+model: "gpt-5.4",
556+ run,
533557}),
534-]),
535-});
558+),
559+);
560+expect(error.name).toBe("FallbackSummaryError");
561+expect(error.message).toContain(rawError);
562+const attempt = error.attempts.find((candidate) => candidate.error === rawError);
563+if (!attempt) {
564+throw new Error("expected raw error attempt");
565+}
566+expect(attempt.reason).toBe("format");
567+expect(attempt.status).toBe(400);
536568});
537569538570it("carries request attribution through exhausted fallback summaries", async () => {
@@ -551,33 +583,25 @@ describe("runWithModelFallback", () => {
551583.mockRejectedValueOnce(Object.assign(new Error("rate limit exceeded"), { status: 429 }))
552584.mockRejectedValueOnce(Object.assign(new Error("overloaded"), { status: 503 }));
553585554-try {
555-await runWithModelFallback({
586+const err = await captureRejection(
587+runWithModelFallback({
556588 cfg,
557589provider: "openai",
558590model: "gpt-5.4",
559591runId: "run-42713",
560592sessionId: "session:browser-42713",
561593lane: "answer",
562594 run,
563-});
564-throw new Error("expected fallback summary");
565-} catch (err) {
566-expect(err).toBeInstanceOf(FallbackSummaryError);
567-if (!(err instanceof FallbackSummaryError)) {
568-throw err;
569-}
570-expect(err).toMatchObject({
571-name: "FallbackSummaryError",
572-sessionId: "session:browser-42713",
573-lane: "answer",
574-});
575-expect(err.cause).toMatchObject({
576-name: "FailoverError",
577-sessionId: "session:browser-42713",
578-lane: "answer",
579-});
580-}
595+}),
596+);
597+const summary = requireFallbackSummaryError(err);
598+expect(summary.name).toBe("FallbackSummaryError");
599+expect(summary.sessionId).toBe("session:browser-42713");
600+expect(summary.lane).toBe("answer");
601+const cause = requireFailoverError(summary.cause);
602+expect(cause.name).toBe("FailoverError");
603+expect(cause.sessionId).toBe("session:browser-42713");
604+expect(cause.lane).toBe("answer");
581605});
582606583607it("uses optional result classification to continue to configured fallbacks", async () => {
@@ -618,12 +642,10 @@ describe("runWithModelFallback", () => {
618642expect(result.result).toEqual({ payloads: [{ text: "fallback ok" }] });
619643expect(run).toHaveBeenCalledTimes(2);
620644expect(run.mock.calls[1]).toEqual(["anthropic", "claude-haiku-3-5"]);
621-expect(result.attempts[0]).toMatchObject({
622-provider: "openai-codex",
623-model: "gpt-5.4",
624-reason: "format",
625-code: "empty_result",
626-});
645+expect(result.attempts[0]?.provider).toBe("openai-codex");
646+expect(result.attempts[0]?.model).toBe("gpt-5.4");
647+expect(result.attempts[0]?.reason).toBe("format");
648+expect(result.attempts[0]?.code).toBe("empty_result");
627649});
628650629651it("surfaces classified terminal results when no fallback remains", async () => {
@@ -639,30 +661,31 @@ describe("runWithModelFallback", () => {
639661});
640662const run = vi.fn().mockResolvedValueOnce({ payloads: [] });
641663642-await expect(
643-runWithModelFallback({
644- cfg,
645-provider: "openai-codex",
646-model: "gpt-5.4",
647- run,
648-classifyResult: ({ result }) => {
649-const payloads = (result as { payloads?: unknown[] }).payloads;
650-return Array.isArray(payloads) && payloads.length === 0
651- ? {
652-message: "terminal result contained no visible assistant reply",
653-reason: "format",
654-code: "empty_result",
655-}
656- : null;
657-},
658-}),
659-).rejects.toMatchObject({
660-name: "FailoverError",
661-reason: "format",
662-provider: "openai-codex",
663-model: "gpt-5.4",
664-code: "empty_result",
665-});
664+const error = requireFailoverError(
665+await captureRejection(
666+runWithModelFallback({
667+ cfg,
668+provider: "openai-codex",
669+model: "gpt-5.4",
670+ run,
671+classifyResult: ({ result }) => {
672+const payloads = (result as { payloads?: unknown[] }).payloads;
673+return Array.isArray(payloads) && payloads.length === 0
674+ ? {
675+message: "terminal result contained no visible assistant reply",
676+reason: "format",
677+code: "empty_result",
678+}
679+ : null;
680+},
681+}),
682+),
683+);
684+expect(error.name).toBe("FailoverError");
685+expect(error.reason).toBe("format");
686+expect(error.provider).toBe("openai-codex");
687+expect(error.model).toBe("gpt-5.4");
688+expect(error.code).toBe("empty_result");
666689expect(run).toHaveBeenCalledTimes(1);
667690});
668691@@ -754,16 +777,14 @@ describe("runWithModelFallback", () => {
754777},
755778};
756779757-expect(
758-classifyEmbeddedPiRunResultForModelFallback({
759-provider: "codex",
760-model: "gpt-5.4",
761-result: runResult,
762-}),
763-).toMatchObject({
764-code: "planning_only_result",
765-reason: "format",
780+const classification = classifyEmbeddedPiRunResultForModelFallback({
781+provider: "codex",
782+model: "gpt-5.4",
783+result: runResult,
766784});
785+const classificationRecord = requireRecord(classification, "planning-only classification");
786+expect(classificationRecord.code).toBe("planning_only_result");
787+expect(classificationRecord.reason).toBe("format");
767788});
768789769790it("classifies non-GPT incomplete terminal errors for configured fallback", () => {
@@ -776,16 +797,14 @@ describe("runWithModelFallback", () => {
776797},
777798};
778799779-expect(
780-classifyEmbeddedPiRunResultForModelFallback({
781-provider: "anthropic",
782-model: "claude-opus-4.7",
783-result: runResult,
784-}),
785-).toMatchObject({
786-code: "incomplete_result",
787-reason: "format",
800+const classification = classifyEmbeddedPiRunResultForModelFallback({
801+provider: "anthropic",
802+model: "claude-opus-4.7",
803+result: runResult,
788804});
805+const classificationRecord = requireRecord(classification, "incomplete classification");
806+expect(classificationRecord.code).toBe("incomplete_result");
807+expect(classificationRecord.reason).toBe("format");
789808});
790809791810it("keeps aborted harness-classified GPT-5 runs out of fallback", () => {
@@ -822,13 +841,12 @@ describe("runWithModelFallback", () => {
822841});
823842824843expect(onError).toHaveBeenCalledTimes(1);
825-expect(onError.mock.calls[0]?.[0]).toMatchObject({
826-provider: "openai",
827-model: "gpt-4.1-mini",
828-attempt: 1,
829-total: 2,
830-});
831-expect(onError.mock.calls[0]?.[0]?.error).toBe(unknownError);
844+const errorCall = requireRecord(onError.mock.calls[0]?.[0], "onError payload");
845+expect(errorCall.provider).toBe("openai");
846+expect(errorCall.model).toBe("gpt-4.1-mini");
847+expect(errorCall.attempt).toBe(1);
848+expect(errorCall.total).toBe(2);
849+expect(errorCall.error).toBe(unknownError);
832850});
833851834852it("throws unrecognized error on last candidate", async () => {
@@ -1363,18 +1381,19 @@ describe("runWithModelFallback", () => {
13631381throw Object.assign(new Error("rate limited"), { status: 429 });
13641382});
136513831366-await expect(
1367-runWithModelFallback({
1368- cfg,
1369-provider: "anthropic",
1370-model: "claude-opus-4-5",
1371-agentDir: tempDir,
1372- run,
1373-}),
1374-).rejects.toMatchObject({
1375-name: "FallbackSummaryError",
1376-soonestCooldownExpiry: expiry,
1377-});
1384+const error = requireFallbackSummaryError(
1385+await captureRejection(
1386+runWithModelFallback({
1387+ cfg,
1388+provider: "anthropic",
1389+model: "claude-opus-4-5",
1390+agentDir: tempDir,
1391+ run,
1392+}),
1393+),
1394+);
1395+expect(error.name).toBe("FallbackSummaryError");
1396+expect(error.soonestCooldownExpiry).toBe(expiry);
13781397});
13791398});
13801399@@ -1419,18 +1438,19 @@ describe("runWithModelFallback", () => {
14191438.fn()
14201439.mockRejectedValue(Object.assign(new Error("rate limited"), { status: 429 }));
142114401422-await expect(
1423-runWithModelFallback({
1424- cfg,
1425-provider: "anthropic",
1426-model: "claude-opus-4-5",
1427-agentDir: tempDir,
1428- run,
1429-}),
1430-).rejects.toMatchObject({
1431-name: "FallbackSummaryError",
1432-soonestCooldownExpiry: relevantExpiry,
1433-});
1441+const error = requireFallbackSummaryError(
1442+await captureRejection(
1443+runWithModelFallback({
1444+ cfg,
1445+provider: "anthropic",
1446+model: "claude-opus-4-5",
1447+agentDir: tempDir,
1448+ run,
1449+}),
1450+),
1451+);
1452+expect(error.name).toBe("FallbackSummaryError");
1453+expect(error.soonestCooldownExpiry).toBe(relevantExpiry);
14341454});
14351455});
14361456此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。