Files
oh-my-pi/packages/coding-agent/test/issue-2750-subagent-runtime-fallback.test.ts
T
roboomp 8c9910318d fix(agent): retried subagent model fallbacks
Installed subagent-scoped retry fallback chains from ordered task model candidates so provider failures can advance to the next configured worker model.

Updated task result tracking to surface the fallback-applied final model and added focused regression coverage for the executor wiring.

Fixes #2750
2026-06-16 06:48:55 +00:00

107 lines
3.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "bun:test";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { buildModel } from "@oh-my-pi/pi-catalog/build";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import * as sdkModule from "@oh-my-pi/pi-coding-agent/sdk";
import type { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { runSubprocess } from "@oh-my-pi/pi-coding-agent/task/executor";
import type { AgentDefinition } from "@oh-my-pi/pi-coding-agent/task/types";
function model(provider: string, id: string): Model<Api> {
return buildModel({
provider,
id,
name: id,
api: "openai-completions",
baseUrl: `https://${provider}.example.test`,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
});
}
function createYieldingSession(): AgentSession {
const listeners: Array<(event: { type: string; [key: string]: unknown }) => void> = [];
const session = {
agent: { state: { systemPrompt: ["test"] } },
state: { messages: [] },
extensionRunner: undefined,
sessionManager: { appendSessionInit: () => {} },
getActiveToolNames: () => ["yield"],
setActiveToolsByName: async () => {},
subscribe: (listener: (event: { type: string; [key: string]: unknown }) => void) => {
listeners.push(listener);
return () => {};
},
prompt: async () => {
for (const listener of listeners) {
listener({
type: "retry_fallback_applied",
from: "primary/bad-runtime-model",
to: "fallback/working-model",
role: "subagent:issue-2750",
});
listener({
type: "tool_execution_end",
toolCallId: "tool-yield",
toolName: "yield",
result: { content: [{ type: "text", text: "Result submitted." }], details: { status: "success" } },
isError: false,
});
}
},
waitForIdle: async () => {},
getLastAssistantMessage: () => undefined,
abort: async () => {},
dispose: async () => {},
};
return session as unknown as AgentSession;
}
describe("issue #2750: subagent runtime model fallback", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("passes ordered subagent candidates as a child retry fallback chain", async () => {
const primary = model("primary", "bad-runtime-model");
const fallback = model("fallback", "working-model");
let childFallbackChains: Record<string, string[]> | undefined;
vi.spyOn(sdkModule, "createAgentSession").mockImplementation(async options => {
if (!options) throw new Error("Expected createAgentSession options");
childFallbackChains = options.settings?.get("retry.fallbackChains") as Record<string, string[]> | undefined;
return { session: createYieldingSession(), extensionsResult: {}, setToolUIContext: () => {} } as never;
});
const agent: AgentDefinition = { name: "task", description: "test", systemPrompt: "test", source: "bundled" };
const result = await runSubprocess({
cwd: "/tmp",
agent,
task: "work",
index: 0,
id: "issue-2750",
modelOverride: ["primary/bad-runtime-model", "fallback/working-model"],
settings: Settings.isolated(),
modelRegistry: {
refresh: async () => {},
getAvailable: () => [primary, fallback],
getApiKey: async () => "test-key",
} as never,
enableLsp: false,
});
let fallbackChain: string[] | undefined;
for (const role in childFallbackChains) {
const chain = childFallbackChains[role];
if (chain?.includes("fallback/working-model")) {
fallbackChain = chain;
}
}
expect(fallbackChain).toEqual(["fallback/working-model"]);
expect(result.modelOverride).toEqual(["primary/bad-runtime-model", "fallback/working-model"]);
expect(result.resolvedModel).toBe("fallback/working-model");
});
});