feat(FN-2898): expand Claude model coverage and subprocess diagnostics
- Add missing Claude model entries and extend provider metadata handling for model extras - Improve subprocess diagnostics in pi-claude-cli process management for clearer failure visibility - Add targeted tests for provider model extras and process-manager diagnostic behavior - Update Settings modal copy for project default model guidance and record changes in pi-claude-cli changelog Fusion-Task-Id: FN-2898
This commit is contained in:
@@ -41,6 +41,7 @@ vi.mock("node:os", () => ({
|
||||
import { spawn, execSync } from "node:child_process";
|
||||
import {
|
||||
spawnClaude,
|
||||
buildClaudeSpawnArgs,
|
||||
writeUserMessage,
|
||||
cleanupProcess,
|
||||
captureStderr,
|
||||
@@ -52,6 +53,32 @@ import {
|
||||
cleanupSystemPromptFile,
|
||||
} from "../process-manager";
|
||||
|
||||
describe("buildClaudeSpawnArgs", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.writeFileSync.mockReset();
|
||||
mocks.tmpdir.mockReset();
|
||||
mocks.tmpdir.mockReturnValue("/mock-tmp");
|
||||
});
|
||||
|
||||
it("builds args including model and optional session/mcp flags", () => {
|
||||
const args = buildClaudeSpawnArgs("claude-sonnet-4-6", undefined, {
|
||||
resumeSessionId: "sess-1",
|
||||
effort: "high",
|
||||
mcpConfigPath: "/tmp/mcp.json",
|
||||
});
|
||||
|
||||
expect(args).toContain("--model");
|
||||
expect(args).toContain("claude-sonnet-4-6");
|
||||
expect(args).toContain("--resume");
|
||||
expect(args).toContain("sess-1");
|
||||
expect(args).toContain("--effort");
|
||||
expect(args).toContain("high");
|
||||
expect(args).toContain("--mcp-config");
|
||||
expect(args).toContain("/tmp/mcp.json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("spawnClaude", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -66,6 +66,7 @@ vi.mock("@mariozechner/pi-ai", () => ({
|
||||
}));
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { getModels } from "@mariozechner/pi-ai";
|
||||
import { streamViaCli } from "../provider";
|
||||
|
||||
describe("provider registration (default export)", () => {
|
||||
@@ -115,16 +116,71 @@ describe("provider registration (default export)", () => {
|
||||
expect(firstModel.maxTokens).toBe(8192);
|
||||
expect(firstModel.cost).toBeDefined();
|
||||
});
|
||||
|
||||
it("includes all extra Claude model entries", async () => {
|
||||
const registerProvider = vi.fn();
|
||||
const mockPi = { registerProvider, on: vi.fn() } as any;
|
||||
|
||||
const mod = await import("../../index");
|
||||
mod.default(mockPi);
|
||||
|
||||
const config = registerProvider.mock.calls[0][1];
|
||||
const modelIds = new Set(config.models.map((m: { id: string }) => m.id));
|
||||
|
||||
for (const id of [
|
||||
"claude-opus-4-7",
|
||||
"claude-sonnet-4-6",
|
||||
"claude-sonnet-4-5",
|
||||
"claude-haiku-4-5",
|
||||
]) {
|
||||
expect(modelIds.has(id)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("deduplicates extra models when catalog already includes them", async () => {
|
||||
const registerProvider = vi.fn();
|
||||
const mockPi = { registerProvider, on: vi.fn() } as any;
|
||||
const getModelsMock = vi.mocked(getModels);
|
||||
|
||||
getModelsMock.mockReturnValueOnce([
|
||||
...mockModels,
|
||||
{
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
api: "anthropic",
|
||||
provider: "anthropic",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 16384,
|
||||
} as any,
|
||||
] as any);
|
||||
|
||||
const mod = await import("../../index");
|
||||
mod.default(mockPi);
|
||||
|
||||
const config = registerProvider.mock.calls[0][1];
|
||||
const matches = config.models.filter(
|
||||
(m: { id: string }) => m.id === "claude-sonnet-4-6",
|
||||
);
|
||||
expect(matches).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("streamViaCli", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
delete process.env.PI_CLAUDE_CLI_DEBUG;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
delete process.env.PI_CLAUDE_CLI_DEBUG;
|
||||
});
|
||||
|
||||
it("returns an AssistantMessageEventStream", () => {
|
||||
@@ -140,6 +196,24 @@ describe("streamViaCli", () => {
|
||||
expect(result.end).toBeDefined();
|
||||
});
|
||||
|
||||
it("logs PID and spawn args when debug mode is enabled", async () => {
|
||||
process.env.PI_CLAUDE_CLI_DEBUG = "1";
|
||||
|
||||
const model = mockModels[0] as any;
|
||||
const context = {
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
};
|
||||
|
||||
const errorSpy = vi.spyOn(console, "error");
|
||||
|
||||
streamViaCli(model, context);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("spawned claude subprocess pid=99999 args="),
|
||||
);
|
||||
});
|
||||
|
||||
it("spawns subprocess and writes user message to stdin", async () => {
|
||||
const model = mockModels[0] as any;
|
||||
const context = {
|
||||
@@ -1179,6 +1253,50 @@ describe("streamViaCli", () => {
|
||||
expect(doneEvent.message.content).toBeDefined();
|
||||
});
|
||||
|
||||
it("logs stderr at warn level on close even with exit code 0", async () => {
|
||||
const model = mockModels[0] as any;
|
||||
const context = {
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
};
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn");
|
||||
|
||||
streamViaCli(model, context);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
const proc = (spawn as any).mock.results[0].value;
|
||||
|
||||
proc.stderr.emit("data", Buffer.from("minor warning from cli"));
|
||||
proc.emit("close", 0, null);
|
||||
proc.stdout.end();
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("minor warning from cli"),
|
||||
);
|
||||
});
|
||||
|
||||
it("warns when subprocess closes successfully with no content events", async () => {
|
||||
const model = mockModels[0] as any;
|
||||
const context = {
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
};
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn");
|
||||
|
||||
streamViaCli(model, context);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
const proc = (spawn as any).mock.results[0].value;
|
||||
proc.emit("close", 0, null);
|
||||
proc.stdout.end();
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("closed without content events"),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not push error on normal close (code 0)", async () => {
|
||||
const model = mockModels[0] as any;
|
||||
const context = {
|
||||
|
||||
Reference in New Issue
Block a user