feat(FN-2980): add droid cli provider card to onboarding

Adds a new Droid CLI provider card to the onboarding flow, including a dedicated provider icon, simplified card CSS, and token fallback fixes for droid provider authentication. The changes wire the card into the ModelOnboardingModal and add test coverage for the card component, icon, and onboarding

Fusion-Task-Id: FN-2980
This commit is contained in:
Fusion
2026-05-01 05:15:19 -07:00
committed by gsxdsm
parent eaf9d6adf9
commit bdf5e84981
4 changed files with 114 additions and 0 deletions

View File

@@ -1713,6 +1713,18 @@ export function ModelOnboardingModal({
);
const renderAiProviderCard = (provider: AuthProvider) => {
if (provider.id === "droid-cli" && provider.type === "cli") {
return (
<DroidCliProviderCard
key={provider.id}
authenticated={provider.authenticated}
onToggled={() => {
void loadAuthStatus();
}}
/>
);
}
if (provider.id === "claude-cli" && provider.type === "cli") {
return (
<ClaudeCliProviderCard

View File

@@ -0,0 +1,93 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { DroidCliProviderCard } from "../DroidCliProviderCard";
const fetchDroidCliStatus = vi.fn();
const setDroidCliEnabled = vi.fn();
vi.mock("../../api", () => ({
fetchDroidCliStatus: (...args: unknown[]) => fetchDroidCliStatus(...args),
setDroidCliEnabled: (...args: unknown[]) => setDroidCliEnabled(...args),
}));
vi.mock("lucide-react", async (importOriginal) => {
const actual = await importOriginal<typeof import("lucide-react")>();
return {
...actual,
Loader2: ({ className }: { className?: string }) => <span data-testid="loader" className={className}>loader</span>,
};
});
const baseStatus = {
binary: { available: true, version: "1.2.3", binaryPath: "/usr/local/bin/droid", probeDurationMs: 9 },
enabled: false,
extension: { status: "ok" as const },
ready: false,
};
describe("DroidCliProviderCard", () => {
beforeEach(() => {
vi.clearAllMocks();
fetchDroidCliStatus.mockResolvedValue(baseStatus);
setDroidCliEnabled.mockResolvedValue({ enabled: true, restartRequired: true });
});
it("renders with loading state before probe resolves", () => {
fetchDroidCliStatus.mockReturnValue(new Promise(() => {}));
render(<DroidCliProviderCard authenticated={false} />);
expect(screen.getByText(/Probing local CLI/i)).toBeInTheDocument();
});
it("renders compact mode", async () => {
render(<DroidCliProviderCard authenticated={false} compact />);
const card = await screen.findByTestId("droid-cli-provider-card");
expect(card).toHaveClass("auth-provider-card");
});
it("renders full mode", async () => {
render(<DroidCliProviderCard authenticated={false} compact={false} />);
const card = await screen.findByTestId("droid-cli-provider-card");
expect(card).toHaveClass("onboarding-provider-card");
});
it("Test button re-fetches status", async () => {
render(<DroidCliProviderCard authenticated={false} />);
await screen.findByText(/Click Enable to route AI calls through it/i);
fireEvent.click(screen.getByRole("button", { name: "Test" }));
await waitFor(() => expect(fetchDroidCliStatus).toHaveBeenCalledTimes(2));
});
it("Enable calls setDroidCliEnabled(true) and disables button when binary unavailable", async () => {
fetchDroidCliStatus.mockResolvedValueOnce({ ...baseStatus, binary: { ...baseStatus.binary, available: false } });
render(<DroidCliProviderCard authenticated={false} />);
const enable = await screen.findByRole("button", { name: "Enable" });
expect(enable).toBeDisabled();
fetchDroidCliStatus.mockResolvedValue({ ...baseStatus, binary: { ...baseStatus.binary, available: true } });
render(<DroidCliProviderCard authenticated={false} />);
const secondEnable = await screen.findAllByRole("button", { name: "Enable" });
fireEvent.click(secondEnable[1]);
await waitFor(() => expect(setDroidCliEnabled).toHaveBeenCalledWith(true));
});
it("Disable calls setDroidCliEnabled(false)", async () => {
fetchDroidCliStatus.mockResolvedValue({ ...baseStatus, enabled: true, ready: true });
setDroidCliEnabled.mockResolvedValue({ enabled: false, restartRequired: true });
render(<DroidCliProviderCard authenticated={true} />);
fireEvent.click(await screen.findByRole("button", { name: "Disable" }));
await waitFor(() => expect(setDroidCliEnabled).toHaveBeenCalledWith(false));
});
it("shows restart-required toast after toggle", async () => {
render(<DroidCliProviderCard authenticated={false} />);
fireEvent.click(await screen.findByRole("button", { name: "Enable" }));
expect(await screen.findByText(/Factory AI \(via Droid CLI\) models are now visible/i)).toBeInTheDocument();
});
it("shows error toast when toggle fails", async () => {
setDroidCliEnabled.mockRejectedValueOnce(new Error("boom"));
render(<DroidCliProviderCard authenticated={false} />);
fireEvent.click(await screen.findByRole("button", { name: "Enable" }));
expect(await screen.findByText("boom")).toBeInTheDocument();
});
});

View File

@@ -85,6 +85,14 @@ vi.mock("../ClaudeCliProviderCard", () => ({
),
}));
vi.mock("../DroidCliProviderCard", () => ({
DroidCliProviderCard: ({ authenticated }: { authenticated: boolean }) => (
<div data-testid="droid-cli-provider-card" data-authenticated={authenticated ? "true" : "false"}>
Factory AI via Droid CLI
</div>
),
}));
vi.mock("lucide-react", async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>;
return {

View File

@@ -255,6 +255,7 @@ html {
--provider-cerebras: #f1592a;
--provider-groq: #f55036;
--provider-vercel: var(--text);
--provider-droid-cli: var(--text);
/* Runtime-plugin marks. */
--provider-hermes: #d4961c;
--provider-openclaw: #ff4f40;