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
116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
import { EventEmitter } from "node:events";
|
|
import { PassThrough } from "node:stream";
|
|
|
|
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" }),
|
|
);
|
|
});
|
|
});
|