import { useEffect, type ReactNode } from "react"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { resetAgentHostApiForTests, setAgentHostApiForTests } from "../../../agentActivityHost.tsx"; import type { AgentHostAgentTargetSetupSnapshot, AgentHostAgentTargetSetupState, AgentHostAgentTargetSetupWatch, AgentHostInputApi } from "../../../host/agentHostApi.ts"; import { AgentTargetSetupControllerProvider, useCreateAgentTargetSetupController } from "../../../shared/agentEnv/agentTargetSetupController.tsx"; import type { AgentGUIAgentTarget } from "../../../types.ts"; import { AGENT_GUI_PROVIDER_RAIL_PREFERENCES_STORAGE_KEY, serializeAgentGUIProviderRailPreferences } from "../model/agentGuiProviderRailOrder.ts"; import { AgentTargetSetupGate } from "./AgentTargetSetupGate.tsx"; import { AgentVisibleErrorPresentationProvider } from "../../../shared/visibleError/AgentVisibleErrorPresentationContext.tsx"; import { AgentTargetSetupRoot, useAgentTargetSetupRoot } from "./AgentTargetSetupRoot.tsx"; beforeAll(() => { Object.defineProperties(HTMLElement.prototype, { hasPointerCapture: { configurable: true, value: () => false }, releasePointerCapture: { configurable: true, value: () => undefined }, setPointerCapture: { configurable: true, value: () => undefined } }); }); afterEach(() => { cleanup(); resetAgentHostApiForTests(); globalThis.localStorage?.removeItem( AGENT_GUI_PROVIDER_RAIL_PREFERENCES_STORAGE_KEY ); }); const codebuddyTarget = target("codebuddy", "CodeBuddy Code"); const geminiTarget = target("gemini", "Gemini CLI"); describe("AgentTargetSetupGate", () => { it("does not expose this device's setup workflow to a shared caller", () => { const install = vi.fn(); const setup = createWatch(notInstalled("extension:codebuddy"), { install }); installHost(new Map([["extension:codebuddy", setup.watch]])); render( ); expect(screen.getByText("composer ready")).toBeTruthy(); expect(screen.queryByTestId("agent-target-setup-gate")).toBeNull(); expect(screen.queryByRole("button", { name: "Open setup" })).toBeNull(); expect(install).not.toHaveBeenCalled(); }); it("gates while checking, then reveals the composer when ready", async () => { const setup = createWatch({ snapshot: null, loading: true, failed: false }); installHost(new Map([["extension:codebuddy", setup.watch]])); render(); expect(screen.queryByText("composer ready")).toBeNull(); expect( screen.getByText("Checking local and Tutti-managed runtimes…") ).toBeTruthy(); expect(screen.queryByRole("button", { name: "Open setup" })).toBeNull(); act(() => setup.publish(ready("extension:codebuddy"))); expect(await screen.findByText("composer ready")).toBeTruthy(); expect(setup.subscribe).toHaveBeenCalledTimes(1); }); it("shows the signed-in account and reauthenticates with its method", async () => { const authenticate = vi.fn(); const setup = createWatch( { snapshot: { ...ready("extension:codebuddy"), authMethods: [ { id: "iOA", name: "Login with iOA" }, { id: "external", name: "Login with Google/GitHub" } ], account: { id: "user-1", displayName: "Rhinoc", authMethodId: "iOA", organization: "Tutti" } }, loading: false, failed: false }, { authenticate } ); installHost(new Map([["extension:codebuddy", setup.watch]])); render(); expect(await screen.findByText("Signed-in account")).toBeTruthy(); expect(screen.getByText("Rhinoc · Tutti")).toBeTruthy(); expect(screen.queryByRole("combobox")).toBeNull(); expect(screen.getByText("CodeBuddy Code is ready.")).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Sign in again" })); await waitFor(() => expect(authenticate).toHaveBeenCalledTimes(1)); expect(authenticate.mock.calls[0]?.[0]).toMatchObject({ methodId: "iOA" }); }); it("stops showing Opening sign in once setup becomes ready while authenticate is still in flight", async () => { let resolveAuthenticate: (() => void) | undefined; const authenticate = vi.fn( () => new Promise((resolve) => { resolveAuthenticate = resolve; }) ); const setup = createWatch(authRequired("extension:gemini"), { authenticate }); installHost(new Map([["extension:gemini", setup.watch]])); render(); fireEvent.click( await screen.findByRole("button", { name: "Continue to sign in" }) ); await waitFor(() => expect(authenticate).toHaveBeenCalledTimes(1)); expect( screen.getByRole("button", { name: "Opening sign in…" }) ).toBeTruthy(); act(() => setup.publish({ ...ready("extension:gemini"), authMethods: authRequired("extension:gemini").snapshot!.authMethods, account: { id: "user-1", displayName: "sunhello135@gmail.com", authMethodId: "oauth-personal", organization: null } }) ); expect(await screen.findByText("Signed-in account")).toBeTruthy(); expect(screen.getByText("sunhello135@gmail.com")).toBeTruthy(); expect(screen.getByRole("button", { name: "Sign in again" })).toBeTruthy(); expect( screen.queryByRole("button", { name: "Opening sign in…" }) ).toBeNull(); resolveAuthenticate?.(); }); it("installs from the shared dialog and can reopen after dismissal", async () => { const install = vi.fn(); const setup = createWatch(notInstalled("extension:codebuddy"), { install }); installHost(new Map([["extension:codebuddy", setup.watch]])); render(); fireEvent.click(await screen.findByRole("button", { name: "Open setup" })); expect(await screen.findByRole("dialog")).toBeTruthy(); expect(screen.getAllByRole("dialog")).toHaveLength(1); fireEvent.click(screen.getByRole("button", { name: "Install runtime" })); await waitFor(() => expect(install).toHaveBeenCalledTimes(1)); expect(install.mock.calls[0]?.[0]).toMatchObject({ planDigest: "a".repeat(64) }); fireEvent.click(screen.getByRole("button", { name: "Close" })); await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull()); fireEvent.click(screen.getByRole("button", { name: "Open setup" })); expect(await screen.findByRole("dialog")).toBeTruthy(); }); it("reinstalls from a persisted failed install action", async () => { const install = vi.fn(); const setup = createWatch(failedInstall("extension:codebuddy"), { install }); installHost(new Map([["extension:codebuddy", setup.watch]])); render(); expect(await screen.findByText("Runtime setup failed")).toBeTruthy(); expect(screen.getByText("fixture install failed")).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Reinstall runtime" })); await waitFor(() => expect(install).toHaveBeenCalledTimes(1)); expect(install.mock.calls[0]?.[0]).toMatchObject({ planDigest: "a".repeat(64) }); expect(install.mock.calls[0]?.[0].clientActionId).not.toBe( "failed-client-action" ); }); it("shows the account failure returned by the runtime probe", async () => { const setup = createWatch({ snapshot: { agentTargetId: "extension:gemini", status: "failed", runtimeSource: "managed", runtimeVersion: "0.28.0", reason: "subscription_required", authMethods: [], account: null, plan: null, action: null }, loading: false, failed: false }); installHost(new Map([["extension:gemini", setup.watch]])); render(); expect( await screen.findAllByText( "Gemini CLI requires an active subscription or an eligible plan for this request" ) ).toHaveLength(3); expect(screen.getByText("Runtime setup failed")).toBeTruthy(); }); it("authenticates with the selected method and renders action errors", async () => { const authenticate = vi.fn(); const setup = createWatch(authRequired("extension:gemini"), { authenticate }); installHost(new Map([["extension:gemini", setup.watch]])); render(); fireEvent.click(await screen.findByRole("button", { name: "Open setup" })); fireEvent.pointerDown( screen.getByRole("combobox", { name: "Sign-in method" }), { button: 0, ctrlKey: false, pointerType: "mouse" } ); fireEvent.click( await screen.findByRole("option", { name: "Gemini API key" }) ); fireEvent.click( screen.getByRole("button", { name: "Continue to sign in" }) ); await waitFor(() => expect(authenticate).toHaveBeenCalledTimes(1)); expect(authenticate.mock.calls[0]?.[0]).toMatchObject({ methodId: "gemini-api-key" }); act(() => setup.publish({ ...authRequired("extension:gemini").snapshot!, action: { actionId: "auth-1", clientActionId: "client-auth-1", kind: "authenticate", status: "failed", phase: "complete", errorCode: "authentication_failed", errorMessage: "This account is not supported by this client" } }) ); expect( await screen.findByText("Authentication did not complete") ).toBeTruthy(); expect( screen.getByText("This account is not supported by this client") ).toBeTruthy(); }); it("guides terminal sign-in methods with a copyable command instead of ACP authenticate", async () => { const authenticate = vi.fn(); const setup = createWatch( { snapshot: { ...authRequired("extension:gemini").snapshot!, authMethods: [ { id: "login", name: "Login with Kimi account", type: "terminal", terminalCommand: "/opt/kimi-code/bin/kimi login" } ] }, loading: false, failed: false }, { authenticate } ); installHost(new Map([["extension:gemini", setup.watch]])); render(); expect( await screen.findByText("/opt/kimi-code/bin/kimi login") ).toBeTruthy(); expect(screen.getByText(/must be completed in a terminal/)).toBeTruthy(); expect( screen.queryByRole("button", { name: "Continue to sign in" }) ).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "Copy command" })); expect(await screen.findByRole("button", { name: "Copied" })).toBeTruthy(); expect(authenticate).not.toHaveBeenCalled(); }); function terminalLoginSetup( overrides: Partial = {} ) { return createWatch( { snapshot: { ...authRequired("extension:gemini").snapshot!, authMethods: [ { id: "login", name: "Login with Kimi account", type: "terminal", terminalCommand: "/opt/kimi-code/bin/kimi", terminalStartupAction: { type: "slash_command", commandName: "login", readyText: "Welcome to Kimi Code!" } } ] }, loading: false, failed: false }, overrides ); } it("launches an in-app terminal for terminal sign-in and closes it once ready", async () => { const close = vi.fn(); const run = vi.fn( async (_input: { command: string; cwd?: string; startupAction?: { type: "slash_command"; commandName: string; readyText: string; }; }) => ({ close, completion: new Promise<"ready" | "timed_out">(() => {}) }) ); const setup = terminalLoginSetup(); installHost(new Map([["extension:gemini", setup.watch]]), { terminalLogin: { run, supportedStartupActionTypes: ["slash_command"] } }); render(); fireEvent.click( await screen.findByRole("button", { name: "Start sign in" }) ); await waitFor(() => expect(run).toHaveBeenCalledTimes(1)); expect(run.mock.calls[0]?.[0]).toEqual({ agentTargetId: "extension:gemini", command: "/opt/kimi-code/bin/kimi", startupAction: { type: "slash_command", commandName: "login", readyText: "Welcome to Kimi Code!" } }); expect( await screen.findByRole("button", { name: "Open setup" }) ).toBeTruthy(); expect(screen.queryByRole("dialog")).toBeNull(); act(() => setup.publish(ready("extension:gemini"))); await waitFor(() => expect(close).toHaveBeenCalledTimes(1)); }); it("keeps terminal sign-in running when the Agent UI unmounts", async () => { let complete: ((result: "ready" | "timed_out") => void) | undefined; const completion = new Promise<"ready" | "timed_out">((resolve) => { complete = resolve; }); const close = vi.fn(); const run = vi.fn(async () => ({ close, completion })); const setup = terminalLoginSetup(); installHost(new Map([["extension:gemini", setup.watch]]), { terminalLogin: { run, supportedStartupActionTypes: ["slash_command"] } }); const view = render(); fireEvent.click( await screen.findByRole("button", { name: "Start sign in" }) ); await waitFor(() => expect(run).toHaveBeenCalledTimes(1)); view.unmount(); await waitFor(() => expect(setup.unsubscribe).toHaveBeenCalledTimes(1)); expect(close).not.toHaveBeenCalled(); complete?.("ready"); await waitFor(() => expect(close).toHaveBeenCalledTimes(1)); }); it("cancels a waiting terminal sign-in and closes the terminal", async () => { const close = vi.fn(); const run = vi.fn(async (_input: { command: string; cwd?: string }) => ({ close, completion: new Promise<"ready" | "timed_out">(() => {}) })); const setup = terminalLoginSetup(); installHost(new Map([["extension:gemini", setup.watch]]), { terminalLogin: { run, supportedStartupActionTypes: ["slash_command"] } }); render(); fireEvent.click( await screen.findByRole("button", { name: "Start sign in" }) ); fireEvent.click(await screen.findByRole("button", { name: "Open setup" })); expect( await screen.findByText(/terminal has been opened in the workspace/) ).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Cancel" })); await waitFor(() => expect(close).toHaveBeenCalledTimes(1)); expect( screen.queryByText(/terminal has been opened in the workspace/) ).toBeNull(); expect( await screen.findByRole("button", { name: "Start sign in" }) ).toBeTruthy(); }); it("keeps the copy fallback when the terminal cannot be launched", async () => { const run = vi.fn(async () => { throw new Error("Terminal login is unavailable in this window."); }); const setup = terminalLoginSetup(); installHost(new Map([["extension:gemini", setup.watch]]), { terminalLogin: { run, supportedStartupActionTypes: ["slash_command"] } }); render(); fireEvent.click( await screen.findByRole("button", { name: "Start sign in" }) ); expect( await screen.findByText(/could not be opened in this window/) ).toBeTruthy(); expect(screen.getByText("/opt/kimi-code/bin/kimi")).toBeTruthy(); expect(screen.getByRole("button", { name: "Copy command" })).toBeTruthy(); }); it("keeps the manual fallback when the host does not support the startup action", async () => { const run = vi.fn(async () => undefined); const setup = terminalLoginSetup(); installHost(new Map([["extension:gemini", setup.watch]]), { terminalLogin: { run } }); render(); expect(screen.queryByRole("button", { name: "Start sign in" })).toBeNull(); expect(screen.getByText("/opt/kimi-code/bin/kimi")).toBeTruthy(); expect(screen.getByRole("button", { name: "Copy command" })).toBeTruthy(); expect(run).not.toHaveBeenCalled(); }); it("stops terminal sign-in and keeps the copy fallback after timeout", async () => { let complete: ((result: "ready" | "timed_out") => void) | undefined; const completion = new Promise<"ready" | "timed_out">((resolve) => { complete = resolve; }); const close = vi.fn(); const run = vi.fn(async () => ({ close, completion })); const setup = terminalLoginSetup(); installHost(new Map([["extension:gemini", setup.watch]]), { terminalLogin: { run, supportedStartupActionTypes: ["slash_command"] } }); render(); fireEvent.click( await screen.findByRole("button", { name: "Start sign in" }) ); complete?.("timed_out"); fireEvent.click(await screen.findByRole("button", { name: "Open setup" })); expect(await screen.findByText(/Timed out waiting/)).toBeTruthy(); expect(close).toHaveBeenCalledTimes(1); expect(screen.getByRole("button", { name: "Copy command" })).toBeTruthy(); }); it("resets dialog and auth selection when the target changes", async () => { const gemini = createWatch(authRequired("extension:gemini")); const codebuddy = createWatch({ snapshot: { ...authRequired("extension:codebuddy").snapshot!, authMethods: [{ id: "browser-login", name: "Browser login" }] }, loading: false, failed: false }); installHost( new Map([ ["extension:gemini", gemini.watch], ["extension:codebuddy", codebuddy.watch] ]) ); const view = render(); fireEvent.click(await screen.findByRole("button", { name: "Open setup" })); fireEvent.pointerDown( screen.getByRole("combobox", { name: "Sign-in method" }), { button: 0, ctrlKey: false, pointerType: "mouse" } ); fireEvent.click( await screen.findByRole("option", { name: "Gemini API key" }) ); view.rerender(); await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull()); expect(gemini.unsubscribe).toHaveBeenCalledTimes(1); expect(codebuddy.subscribe).toHaveBeenCalledTimes(1); fireEvent.click(screen.getByRole("button", { name: "Open setup" })); expect( await screen.findByRole("combobox", { name: "Sign-in method" }) ).toHaveTextContent("Browser login"); }); it("uses the same fallback target for the hidden home selection and setup controller", async () => { globalThis.localStorage?.setItem( AGENT_GUI_PROVIDER_RAIL_PREFERENCES_STORAGE_KEY, serializeAgentGUIProviderRailPreferences({ hiddenTargetIds: [geminiTarget.targetId], order: [geminiTarget.targetId, codebuddyTarget.targetId] }) ); const codebuddy = createWatch(notInstalled("extension:codebuddy")); const gemini = createWatch({ snapshot: ready("extension:gemini"), loading: false, failed: false }); const hostWatch = installHost( new Map([ ["extension:codebuddy", codebuddy.watch], ["extension:gemini", gemini.watch] ]) ); render(); expect(screen.getByTestId("projected-home-target")).toHaveTextContent( "CodeBuddy Code" ); expect(await screen.findByText("Set up CodeBuddy Code")).toBeTruthy(); expect(hostWatch).toHaveBeenCalledTimes(1); expect(hostWatch).toHaveBeenCalledWith({ agentTargetId: "extension:codebuddy" }); }); }); function RootHarness(): React.JSX.Element { const { controller, homeTargetProjection } = useAgentTargetSetupRoot({ activeConversationId: null, agentTargets: [geminiTarget, codebuddyTarget], environmentProvider: geminiTarget.provider, selectedAgentTarget: geminiTarget }); return (
{homeTargetProjection.selectedAgentTarget?.label}
composer ready
); } function Harness({ target: selectedTarget, children =
composer ready
, openDialog = false }: { target: AgentGUIAgentTarget; children?: ReactNode; openDialog?: boolean; }): React.JSX.Element { const controller = useCreateAgentTargetSetupController(selectedTarget); useEffect(() => { if (openDialog) controller.setDialogOpen(true); }, [controller, openDialog]); return ( {children} ); } function createWatch( initial: AgentHostAgentTargetSetupState, overrides: Partial = {} ) { let state = initial; const listeners = new Set<(state: AgentHostAgentTargetSetupState) => void>(); const unsubscribe = vi.fn(); const subscribe = vi.fn( (listener: (state: AgentHostAgentTargetSetupState) => void) => { listeners.add(listener); return () => { listeners.delete(listener); unsubscribe(); }; } ); const watch: AgentHostAgentTargetSetupWatch = { getSnapshot: () => state, subscribe, install: async () => undefined, authenticate: async () => undefined, refresh: async () => undefined, ...overrides }; return { publish(snapshot: AgentHostAgentTargetSetupSnapshot) { state = { snapshot, loading: false, failed: false }; for (const listener of listeners) listener(state); }, subscribe, unsubscribe, watch }; } function installHost( watches: Map, extra?: { terminalLogin?: AgentHostInputApi["terminalLogin"] } ) { const watch = vi.fn(({ agentTargetId }: { agentTargetId: string }) => { const targetWatch = watches.get(agentTargetId); if (!targetWatch) throw new Error(`Missing watch for ${agentTargetId}`); return targetWatch; }); const api: AgentHostInputApi = { agentTargetSetup: { watch }, clipboard: { writeText: async () => undefined }, ...(extra?.terminalLogin ? { terminalLogin: extra.terminalLogin } : {}), filesystem: { readFileText: async () => ({ content: "" }) }, workspace: { ensureDirectory: async () => undefined, readFile: async () => ({ bytes: new Uint8Array() }), selectDirectory: async () => null, selectFiles: async () => [], writeFileText: async () => undefined } }; setAgentHostApiForTests(api); return watch; } function target(id: string, label: string): AgentGUIAgentTarget { return { targetId: `extension:${id}`, agentTargetId: `extension:${id}`, provider: `acp:${id}`, ref: { kind: "agent-directory", provider: `acp:${id}`, setupKind: "target_runtime" }, label }; } function ready(agentTargetId: string): AgentHostAgentTargetSetupSnapshot { return { agentTargetId, status: "ready", authMethods: [], account: null, runtimeSource: "managed", runtimeVersion: "1.0.0", reason: null, plan: null, action: null }; } function notInstalled(agentTargetId: string): AgentHostAgentTargetSetupState { return { snapshot: { agentTargetId, status: "not_installed", authMethods: [], account: null, runtimeSource: null, runtimeVersion: null, reason: null, plan: { packageName: "@tutti-os/example-agent", packageVersion: "1.0.0", runner: "npm", installRoot: "/state/agent/runtimes/example/1.0.0", planDigest: "a".repeat(64) }, action: null }, loading: false, failed: false }; } function authRequired(agentTargetId: string): AgentHostAgentTargetSetupState { return { snapshot: { agentTargetId, status: "auth_required", authMethods: [ { id: "oauth-personal", name: "Log in with Google" }, { id: "gemini-api-key", name: "Gemini API key" } ], account: null, runtimeSource: "managed", runtimeVersion: "1.0.0", reason: null, plan: null, action: null }, loading: false, failed: false }; } function failedInstall(agentTargetId: string): AgentHostAgentTargetSetupState { return { snapshot: { ...notInstalled(agentTargetId).snapshot!, status: "failed", reason: "install_failed", action: { actionId: "failed-install-action", clientActionId: "failed-client-action", kind: "install", status: "failed", phase: "complete", errorCode: "install_failed", errorMessage: "fixture install failed" } }, loading: false, failed: false }; }