// @vitest-environment jsdom import { useState } from "react"; import { createRoot, type Root } from "react-dom/client"; import { flushSync } from "react-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { Agent, Environment, UserSecretDefinition } from "@paperclipai/shared"; import { getEnvironmentCapabilities } from "@paperclipai/shared"; import { TooltipProvider } from "@/components/ui/tooltip"; import { ToastProvider } from "../context/ToastContext"; import { AgentConfigForm, AdapterLoginPanel, type AdapterLoginDescriptor } from "./AgentConfigForm"; import { defaultCreateValues } from "./agent-config-defaults"; import { buildNewAgentHirePayload } from "../lib/new-agent-hire-payload"; import { ApiError } from "../api/client"; const mockAgentsApi = vi.hoisted(() => ({ adapterModels: vi.fn(), detectModel: vi.fn(), list: vi.fn(), testEnvironment: vi.fn(), startAdapterAuthLogin: vi.fn(), getAdapterAuthLoginStatus: vi.fn(), cancelAdapterAuthLogin: vi.fn(), startClaudeSetupTokenLogin: vi.fn(), getClaudeSetupTokenLoginStatus: vi.fn(), getClaudeSetupTokenLoginPrompt: vi.fn(), submitClaudeSetupTokenBrowserCode: vi.fn(), completeClaudeSetupTokenLogin: vi.fn(), cancelClaudeSetupTokenLogin: vi.fn(), getClaudeOAuthTokenStatus: vi.fn(), })); const mockClipboard = vi.hoisted(() => ({ copyTextToClipboard: vi.fn(), })); const mockEnvironmentsApi = vi.hoisted(() => ({ list: vi.fn(), capabilities: vi.fn(), })); const mockInstanceSettingsApi = vi.hoisted(() => ({ get: vi.fn(), getExperimental: vi.fn(), getGeneral: vi.fn(), })); const mockSecretsApi = vi.hoisted(() => ({ list: vi.fn(), listProposals: vi.fn(), listUserSecretDefinitions: vi.fn(async () => [] as unknown[]), })); vi.mock("../api/agents", () => ({ agentsApi: mockAgentsApi, })); vi.mock("../api/environments", () => ({ environmentsApi: mockEnvironmentsApi, })); vi.mock("../api/instanceSettings", () => ({ instanceSettingsApi: mockInstanceSettingsApi, })); vi.mock("../api/secrets", () => ({ secretsApi: mockSecretsApi, })); vi.mock("../lib/clipboard", () => ({ copyTextToClipboard: mockClipboard.copyTextToClipboard, })); vi.mock("../context/CompanyContext", () => ({ useCompany: () => ({ companies: [{ id: "company-1", name: "Paperclip" }], selectedCompanyId: "company-1", selectedCompany: { id: "company-1", name: "Paperclip" }, selectionSource: "bootstrap", loading: false, error: null, setSelectedCompanyId: vi.fn(), reloadCompanies: vi.fn(), createCompany: vi.fn(), }), })); vi.mock("../adapters", () => ({ getUIAdapter: (type: string) => ({ type, label: type === "hermes_gateway" ? "Hermes Gateway" : "Codex", // The stand-in also records the two gates the form resolves for every // adapter, so a test can assert the plumbing without rendering a real // adapter's fields. ConfigFields: ({ adapterType, hideInstructionsFile, managedSandboxOnly }: { adapterType: string; hideInstructionsFile?: boolean; managedSandboxOnly?: boolean; }) => adapterType === "hermes_gateway" ?
Hermes Gateway fields
: (
), buildAdapterConfig: (values: { model?: string }) => ({ model: values.model || undefined, }), parseStdoutLine: () => [], }), })); // The projected login capability per adapter type. The server projects these // safe scalar fields. `codex_local` drives the displayed-code panel; `claude_local` // drives the submitted-browser-code panel. A test overrides this map to add a // third adapter with a projected login capability. const mockLoginProjections = vi.hoisted( () => new Map([ ["codex_local", { panelMode: "displayed_code", timeoutPolicy: "caller_bounded" }], ["grok_local", { panelMode: "displayed_code", timeoutPolicy: "caller_bounded" }], ["claude_local", { panelMode: "submitted_browser_code", timeoutPolicy: "fixed" }], // A third adapter, not a built-in, with a projected displayed-code login. ["vendor_local", { panelMode: "displayed_code", timeoutPolicy: "caller_bounded" }], // A non-built-in adapter with a submitted-browser-code login. Every login // runs on a real pseudo-terminal, so the gate requires the provider pty // capability from the login capability, not the adapter name. ["pty_vendor_local", { panelMode: "submitted_browser_code", timeoutPolicy: "fixed" }], ]), ); vi.mock("../adapters/use-adapter-capabilities", () => ({ useAdapterCapabilities: () => (adapterType: string) => { const login = mockLoginProjections.get(adapterType); return adapterType === "hermes_gateway" ? { supportsInstructionsBundle: false, supportsSkills: false, supportsLocalAgentJwt: false, requiresMaterializedRuntimeSkills: false, supportsAcp: false, } : { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: false, supportsAcp: true, ...(login ? { login } : {}), }; }, })); vi.mock("../adapters/use-disabled-adapters", () => ({ useDisabledAdaptersSync: () => [], })); vi.mock("./MarkdownEditor", () => ({ MarkdownEditor: ({ value, onChange, placeholder, }: { value: string; onChange: (value: string) => void; placeholder?: string; }) => (