feat(FN-3938): add integration tests for droid-cli extension entrypoint

Adds integration test coverage for the droid-cli extension entrypoint, with a minor README refresh.

Fusion-Task-Id: FN-3938
This commit is contained in:
Fusion
2026-05-10 09:55:14 -07:00
committed by gsxdsm
parent b31d79943b
commit 4b54a7ddbc
2 changed files with 236 additions and 3 deletions

View File

@@ -1,8 +1,13 @@
# @fusion/droid-cli
Compatibility shim package for Fusion's Droid integration.
Compatibility-shim package for Fusion's Droid integration extension entrypoint.
Runtime/provider implementation now lives in:
Runtime/provider internals live in:
- `@fusion-plugin-examples/droid-runtime` (`plugins/fusion-plugin-droid-runtime`)
This package preserves the historical pi extension entrypoint and delegates to the plugin-owned implementation so existing imports continue to work.
This package preserves the historical pi extension entrypoint and wires runtime helpers into the real extension surface (`registerProvider`, `session_start` tool activation, model discovery, and MCP handoff) so existing imports continue to work.
## Testing boundary
- `packages/droid-cli/src/__tests__/index.test.ts` owns integration coverage for this shim entrypoint (`index.ts`) and verifies provider registration and wiring behavior.
- Runtime internals (`streamViaCli`, CLI process handling, MCP schema generation, parsing, etc.) remain covered in the plugin package tests under `plugins/fusion-plugin-droid-runtime`.

View File

@@ -0,0 +1,228 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
type ToolDescriptor = {
name: string;
description: string;
parameters: Record<string, unknown>;
};
const runtimeMocks = vi.hoisted(() => {
return {
streamViaCli: vi.fn(() => ({ push: vi.fn(), end: vi.fn() })),
discoverDroidModels: vi.fn(async () => ["droid-pro", "droid-max"]),
validateCliPresenceAsync: vi.fn(async () => ({ ok: true })),
validateCliAuthAsync: vi.fn(async () => undefined),
killAllProcesses: vi.fn(),
getCustomToolDefs: vi.fn(() => [
{ name: "fn_read", description: "Read", input_schema: { type: "object" } },
]),
toolsFromContext: vi.fn((tools?: readonly ToolDescriptor[]) =>
Array.isArray(tools)
? tools.map((tool) => ({
name: tool.name,
description: tool.description,
input_schema: tool.parameters,
}))
: [],
),
writeMcpConfig: vi.fn((_: unknown, hash: string) => `/tmp/droid-mcp-${hash}.json`),
};
});
vi.mock("@fusion-plugin-examples/droid-runtime", () => runtimeMocks);
const flushAsyncRegistration = async () => {
await Promise.resolve();
await Promise.resolve();
};
describe("droid-cli extension entrypoint", () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
runtimeMocks.discoverDroidModels.mockResolvedValue(["droid-pro", "droid-max"]);
runtimeMocks.validateCliPresenceAsync.mockResolvedValue({ ok: true });
runtimeMocks.validateCliAuthAsync.mockResolvedValue(undefined);
runtimeMocks.toolsFromContext.mockImplementation((tools?: readonly ToolDescriptor[]) =>
Array.isArray(tools)
? tools.map((tool) => ({
name: tool.name,
description: tool.description,
input_schema: tool.parameters,
}))
: [],
);
});
afterEach(() => {
vi.restoreAllMocks();
});
it("registers provider droid-cli with discovered model mapping and streamSimple", async () => {
const registerProvider = vi.fn();
const mockPi = {
registerProvider,
on: vi.fn(),
getAllTools: vi.fn(() => []),
setActiveTools: vi.fn(),
};
const mod = await import("../../index");
mod.default(mockPi as never);
await flushAsyncRegistration();
expect(runtimeMocks.validateCliPresenceAsync).toHaveBeenCalledTimes(1);
expect(runtimeMocks.validateCliAuthAsync).toHaveBeenCalledTimes(1);
expect(runtimeMocks.discoverDroidModels).toHaveBeenCalledTimes(1);
expect(registerProvider).toHaveBeenCalledTimes(1);
const [providerId, config] = registerProvider.mock.calls[0] as [string, {
baseUrl: string;
api: string;
apiKey: string;
models: Array<{ id: string; name: string; contextWindow: number; maxTokens: number }>;
streamSimple: Function;
}];
expect(providerId).toBe("droid-cli");
expect(config.baseUrl).toBe("droid-cli");
expect(config.api).toBe("droid-cli");
expect(config.apiKey).toBe("unused");
expect(config.models).toEqual([
expect.objectContaining({ id: "droid-pro", name: "droid-pro", contextWindow: 200_000, maxTokens: 8_192 }),
expect.objectContaining({ id: "droid-max", name: "droid-max", contextWindow: 200_000, maxTokens: 8_192 }),
]);
expect(typeof config.streamSimple).toBe("function");
});
it("activates all registered tools on session_start", async () => {
const sessionStartHandlers: Array<() => Promise<void>> = [];
const mockPi = {
registerProvider: vi.fn(),
on: vi.fn((event: string, handler: () => Promise<void>) => {
if (event === "session_start") sessionStartHandlers.push(handler);
}),
getAllTools: vi.fn(() => [{ name: "find" }, { name: "grep" }]),
setActiveTools: vi.fn(),
};
const mod = await import("../../index");
mod.default(mockPi as never);
await flushAsyncRegistration();
expect(sessionStartHandlers).toHaveLength(1);
await sessionStartHandlers[0]();
expect(mockPi.setActiveTools).toHaveBeenCalledWith(["find", "grep"]);
});
it("warns but still registers provider when cli presence check fails", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
runtimeMocks.validateCliPresenceAsync.mockResolvedValue({
ok: false,
error: { message: "droid CLI missing" },
} as any);
const mockPi = {
registerProvider: vi.fn(),
on: vi.fn(),
getAllTools: vi.fn(() => []),
setActiveTools: vi.fn(),
};
const mod = await import("../../index");
mod.default(mockPi as never);
await flushAsyncRegistration();
expect(warnSpy).toHaveBeenCalledWith("[droid-cli] droid CLI missing");
expect(runtimeMocks.validateCliAuthAsync).not.toHaveBeenCalled();
expect(mockPi.registerProvider).toHaveBeenCalledTimes(1);
});
it("falls back to empty models when discovery throws", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
runtimeMocks.discoverDroidModels.mockRejectedValue(new Error("boom"));
const registerProvider = vi.fn();
const mockPi = {
registerProvider,
on: vi.fn(),
getAllTools: vi.fn(() => []),
setActiveTools: vi.fn(),
};
const mod = await import("../../index");
mod.default(mockPi as never);
await flushAsyncRegistration();
const config = registerProvider.mock.calls[0]?.[1] as { models: unknown[] };
expect(config.models).toEqual([]);
expect(warnSpy).toHaveBeenCalledWith(
"[droid-cli] model auto-discovery failed; registering provider with empty model list",
expect.any(Error),
);
});
it("wires context tools to mcp config and passes mcpConfigPath into streamViaCli", async () => {
const registerProvider = vi.fn();
const mockPi = {
registerProvider,
on: vi.fn(),
getAllTools: vi.fn(() => [{ name: "find" }]),
setActiveTools: vi.fn(),
};
const mod = await import("../../index");
mod.default(mockPi as never);
await flushAsyncRegistration();
const config = registerProvider.mock.calls[0]?.[1] as {
streamSimple: (model: unknown, context: unknown, options: Record<string, unknown>) => unknown;
};
const context = {
tools: [
{
name: "fn_web_fetch",
description: "Fetch URL",
parameters: { type: "object", properties: { url: { type: "string" } } },
},
],
};
config.streamSimple({ id: "droid-pro" }, context, { temperature: 0.2 });
expect(runtimeMocks.toolsFromContext).toHaveBeenCalledWith(context.tools);
expect(runtimeMocks.writeMcpConfig).toHaveBeenCalledTimes(1);
expect(runtimeMocks.streamViaCli).toHaveBeenCalledWith(
{ id: "droid-pro" },
context,
expect.objectContaining({ temperature: 0.2, mcpConfigPath: expect.stringContaining("/tmp/droid-mcp-") }),
);
});
it("falls back to getCustomToolDefs when context tools are missing", async () => {
runtimeMocks.toolsFromContext.mockReturnValue([]);
const registerProvider = vi.fn();
const mockPi = {
registerProvider,
on: vi.fn(),
getAllTools: vi.fn(() => [{ name: "ls" }]),
setActiveTools: vi.fn(),
};
const mod = await import("../../index");
mod.default(mockPi as never);
await flushAsyncRegistration();
const config = registerProvider.mock.calls[0]?.[1] as {
streamSimple: (model: unknown, context: unknown, options?: Record<string, unknown>) => unknown;
};
config.streamSimple({ id: "droid-pro" }, { messages: [] }, {});
expect(runtimeMocks.getCustomToolDefs).toHaveBeenCalledWith(mockPi);
expect(runtimeMocks.writeMcpConfig).toHaveBeenCalledTimes(1);
});
});