





























@@ -13,6 +13,7 @@ import { CommandLaneTaskTimeoutError } from "../process/command-queue.js";
1313import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js";
1414import type { AuthProfileStore } from "./auth-profiles/types.js";
1515import { FailoverError } from "./failover-error.js";
16+import { MissingAgentHarnessError } from "./harness/errors.js";
1617import { LiveSessionModelSwitchError } from "./live-model-switch-error.js";
1718import {
1819FallbackSummaryError,
@@ -556,6 +557,201 @@ describe("runWithModelFallback", () => {
556557expect(result.attempts[0].reason).toBe("unknown");
557558});
558559560+it("fails closed when a strict plugin harness is missing", async () => {
561+const cfg = makeCfg({
562+agents: {
563+defaults: {
564+model: {
565+primary: "openai/gpt-5.5",
566+fallbacks: ["anthropic/claude-sonnet-4-6"],
567+},
568+},
569+},
570+});
571+const run = vi
572+.fn()
573+.mockRejectedValueOnce(new MissingAgentHarnessError("codex"))
574+.mockResolvedValueOnce("wrong fallback");
575+576+await expect(
577+runWithModelFallback({
578+ cfg,
579+provider: "openai",
580+model: "gpt-5.5",
581+ run,
582+}),
583+).rejects.toThrow('Requested agent harness "codex" is not registered.');
584+expect(run).toHaveBeenCalledTimes(1);
585+});
586+587+it("fails closed before auth cooldown skips when a strict plugin harness is missing", async () => {
588+const cfg = makeCfg({
589+models: {
590+providers: {
591+openai: {
592+baseUrl: "https://api.openai.com/v1",
593+agentRuntime: { id: "codex" },
594+models: [],
595+},
596+},
597+},
598+agents: {
599+defaults: {
600+model: {
601+primary: "openai/gpt-5.5",
602+fallbacks: ["anthropic/claude-sonnet-4-6"],
603+},
604+},
605+},
606+});
607+const tempDir = await makeAuthTempDir();
608+setAuthRuntimeStore(tempDir, {
609+version: AUTH_STORE_VERSION,
610+profiles: {
611+"openai:default": { type: "api_key", provider: "openai", key: "test-key" },
612+"anthropic:default": { type: "api_key", provider: "anthropic", key: "test-key" },
613+},
614+usageStats: {
615+"openai:default": {
616+cooldownUntil: Date.now() + 60_000,
617+cooldownReason: "rate_limit",
618+failureCounts: { rate_limit: 1 },
619+},
620+},
621+});
622+const run = vi.fn().mockResolvedValueOnce("wrong fallback");
623+624+await expect(
625+runWithModelFallback({
626+ cfg,
627+provider: "openai",
628+model: "gpt-5.5",
629+agentDir: tempDir,
630+ run,
631+}),
632+).rejects.toThrow('Requested agent harness "codex" is not registered.');
633+expect(run).not.toHaveBeenCalled();
634+});
635+636+it("uses agent runtime context before auth cooldown skips", async () => {
637+const cfg = makeCfg({
638+agents: {
639+list: [
640+{ id: "main", default: true },
641+{
642+id: "worker",
643+models: {
644+"openai/gpt-5.5": { agentRuntime: { id: "codex" } },
645+},
646+},
647+],
648+defaults: {
649+model: {
650+primary: "openai/gpt-5.5",
651+fallbacks: ["anthropic/claude-sonnet-4-6"],
652+},
653+},
654+},
655+});
656+const tempDir = await makeAuthTempDir();
657+setAuthRuntimeStore(tempDir, {
658+version: AUTH_STORE_VERSION,
659+profiles: {
660+"openai:default": { type: "api_key", provider: "openai", key: "test-key" },
661+"anthropic:default": { type: "api_key", provider: "anthropic", key: "test-key" },
662+},
663+usageStats: {
664+"openai:default": {
665+cooldownUntil: Date.now() + 60_000,
666+cooldownReason: "rate_limit",
667+failureCounts: { rate_limit: 1 },
668+},
669+},
670+});
671+const run = vi.fn().mockResolvedValueOnce("wrong fallback");
672+673+await expect(
674+runWithModelFallback({
675+ cfg,
676+provider: "openai",
677+model: "gpt-5.5",
678+agentDir: tempDir,
679+agentId: "worker",
680+ run,
681+}),
682+).rejects.toThrow('Requested agent harness "codex" is not registered.');
683+expect(run).not.toHaveBeenCalled();
684+});
685+686+it("uses session runtime overrides before auth cooldown skips", async () => {
687+const cfg = makeCfg({
688+agents: {
689+defaults: {
690+model: {
691+primary: "openai/gpt-5.5",
692+fallbacks: ["anthropic/claude-sonnet-4-6"],
693+},
694+},
695+},
696+});
697+const tempDir = await makeAuthTempDir();
698+setAuthRuntimeStore(tempDir, {
699+version: AUTH_STORE_VERSION,
700+profiles: {
701+"openai:default": { type: "api_key", provider: "openai", key: "test-key" },
702+"anthropic:default": { type: "api_key", provider: "anthropic", key: "test-key" },
703+},
704+usageStats: {
705+"openai:default": {
706+cooldownUntil: Date.now() + 60_000,
707+cooldownReason: "rate_limit",
708+failureCounts: { rate_limit: 1 },
709+},
710+},
711+});
712+const run = vi.fn().mockResolvedValueOnce("wrong fallback");
713+714+await expect(
715+runWithModelFallback({
716+ cfg,
717+provider: "openai",
718+model: "gpt-5.5",
719+agentDir: tempDir,
720+resolveAgentHarnessRuntimeOverride: (provider) =>
721+provider === "openai" ? "codex" : undefined,
722+ run,
723+}),
724+).rejects.toThrow('Requested agent harness "codex" is not registered.');
725+expect(run).not.toHaveBeenCalled();
726+});
727+728+it("lets configured CLI runtimes reach the run callback", async () => {
729+const cfg = makeCfg({
730+agents: {
731+defaults: {
732+models: {
733+"anthropic/*": { agentRuntime: { id: "claude-cli" } },
734+},
735+model: {
736+primary: "anthropic/claude-sonnet-4-6",
737+},
738+},
739+},
740+});
741+const run = vi.fn().mockResolvedValueOnce("cli ok");
742+743+const result = await runWithModelFallback({
744+ cfg,
745+provider: "anthropic",
746+model: "claude-sonnet-4-6",
747+ run,
748+});
749+750+expect(result.result).toBe("cli ok");
751+expect(run).toHaveBeenCalledTimes(1);
752+expect(run.mock.calls[0]).toEqual(["anthropic", "claude-sonnet-4-6"]);
753+});
754+559755it("does not treat command-lane watchdog timeouts as model fallback failures", async () => {
560756const cfg = makeCfg();
561757const timeoutError = new CommandLaneTaskTimeoutError("cron-nested", 330_000);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。