feat(FN-3328): refactor droid-cli into compatibility shim backed by fusion-
Merged FN-3328: Refactored the droid integration by converting `droid-cli` into a lightweight compatibility shim that delegates to `fusion-plugin-droid-runtime`, reducing droid-cli by ~5,400 lines of code while moving the actual runtime logic into the plugin scaffold. Updated the plugin loader to al Fusion-Task-Id: FN-3328
This commit is contained in:
@@ -1,10 +1,115 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import * as processManager from "../process-manager.js";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
|
||||
describe("provider dependencies", () => {
|
||||
it("deduplicates discovered model ids", async () => {
|
||||
vi.spyOn(processManager, "discoverDroidModels").mockResolvedValue(["a", "b", "a"]);
|
||||
const ids = Array.from(new Set(await processManager.discoverDroidModels()));
|
||||
expect(ids).toEqual(["a", "b"]);
|
||||
const mocks = vi.hoisted(() => ({
|
||||
spawnDroid: vi.fn(),
|
||||
writeUserMessage: vi.fn(),
|
||||
captureStderr: vi.fn(() => () => ""),
|
||||
registerProcess: vi.fn(),
|
||||
cleanupProcess: vi.fn(),
|
||||
forceKillProcess: vi.fn(),
|
||||
cleanupSystemPromptFile: vi.fn(),
|
||||
buildDroidSpawnArgs: vi.fn(() => ["--model", "droid-pro"]),
|
||||
parseLine: vi.fn(),
|
||||
bridgeHandleEvent: vi.fn(),
|
||||
bridgeGetOutput: vi.fn(() => ({
|
||||
role: "assistant",
|
||||
content: [],
|
||||
api: "droid-cli",
|
||||
provider: "droid-cli",
|
||||
model: "droid-pro",
|
||||
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
||||
stopReason: "stop",
|
||||
timestamp: 0,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../process-manager.js", () => ({
|
||||
spawnDroid: mocks.spawnDroid,
|
||||
writeUserMessage: mocks.writeUserMessage,
|
||||
captureStderr: mocks.captureStderr,
|
||||
registerProcess: mocks.registerProcess,
|
||||
cleanupProcess: mocks.cleanupProcess,
|
||||
forceKillProcess: mocks.forceKillProcess,
|
||||
cleanupSystemPromptFile: mocks.cleanupSystemPromptFile,
|
||||
buildDroidSpawnArgs: mocks.buildDroidSpawnArgs,
|
||||
}));
|
||||
|
||||
vi.mock("../stream-parser.js", () => ({ parseLine: mocks.parseLine }));
|
||||
vi.mock("../event-bridge.js", () => ({
|
||||
createEventBridge: () => ({ handleEvent: mocks.bridgeHandleEvent, getOutput: mocks.bridgeGetOutput }),
|
||||
}));
|
||||
|
||||
import { streamViaCli } from "../provider.js";
|
||||
|
||||
function makeProc() {
|
||||
const proc = new EventEmitter() as any;
|
||||
proc.stdin = { write: vi.fn(), end: vi.fn() };
|
||||
proc.stdout = new PassThrough();
|
||||
proc.stderr = new EventEmitter();
|
||||
proc.killed = false;
|
||||
proc.exitCode = null;
|
||||
proc.kill = vi.fn();
|
||||
proc.pid = 123;
|
||||
return proc;
|
||||
}
|
||||
|
||||
describe("streamViaCli", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("spawns droid and writes prompt", async () => {
|
||||
const proc = makeProc();
|
||||
mocks.spawnDroid.mockReturnValue(proc);
|
||||
mocks.parseLine.mockReturnValueOnce({ type: "result", subtype: "success" });
|
||||
|
||||
const stream = streamViaCli({ id: "droid-pro", provider: "droid-cli" } as any, { messages: [{ role: "user", content: "hi" }] } as any);
|
||||
expect(stream).toBeDefined();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
proc.stdout.write('{"type":"result","subtype":"success"}\n');
|
||||
proc.emit("close", 0, null);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
expect(mocks.spawnDroid).toHaveBeenCalled();
|
||||
expect(mocks.writeUserMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forwards stream events to bridge", async () => {
|
||||
const proc = makeProc();
|
||||
mocks.spawnDroid.mockReturnValue(proc);
|
||||
mocks.parseLine
|
||||
.mockReturnValueOnce({ type: "stream_event", event: { type: "message_start" } })
|
||||
.mockReturnValueOnce({ type: "result", subtype: "success" });
|
||||
|
||||
streamViaCli({ id: "droid-pro", provider: "droid-cli" } as any, { messages: [{ role: "user", content: "hi" }] } as any);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
proc.stdout.write('a\n');
|
||||
proc.stdout.write('b\n');
|
||||
proc.emit("close", 0, null);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
expect(mocks.bridgeHandleEvent).toHaveBeenCalledWith({ type: "message_start" });
|
||||
});
|
||||
|
||||
it("includes mcp config path in spawn args options", async () => {
|
||||
const proc = makeProc();
|
||||
mocks.spawnDroid.mockReturnValue(proc);
|
||||
mocks.parseLine.mockReturnValueOnce({ type: "result", subtype: "success" });
|
||||
|
||||
streamViaCli(
|
||||
{ id: "droid-pro", provider: "droid-cli" } as any,
|
||||
{ messages: [{ role: "user", content: "hi" }] } as any,
|
||||
{ mcpConfigPath: "/tmp/mcp.json" } as any,
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(mocks.spawnDroid).toHaveBeenCalledWith(
|
||||
"droid-pro",
|
||||
expect.anything(),
|
||||
expect.objectContaining({ mcpConfigPath: "/tmp/mcp.json" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user