// Run explicitly through session-capability-card.test.tsx so Radix sees the DOM at import time. import { t } from "../../lib/console-text"; import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; import { act, StrictMode } from "react"; import { CapabilityCatalogItem } from "@opengeni/contracts"; import type { AuthNeededItem } from "../../../timeline"; import type { Session, UpdateSessionToolPolicyRequest } from "@opengeni/sdk"; const catalogItem = CapabilityCatalogItem.parse({ id: "example", kind: "mcp", source: "manual", name: "Example", providerDomain: "api.example.com", mcpUrl: "https://api.example.com/mcp", authKind: "api_key", runtime: { available: true, mcpServerId: "example" }, tools: [{ kind: "mcp", id: "example" }], }); let personal = false; let liveCatalogItem = catalogItem; let enabled = false; let createdCatalogItem: CapabilityCatalogItem | null = null; let connections: unknown[] = []; const row = { id: "connection", providerDomain: "api.example.com", kind: "api_key", subjectId: null, status: "active", }; const createConnection = mock(async () => { connections.push(row); return row; }); const updateConnection = mock(async () => row); const enableCapability = mock(async () => { enabled = true; }); const getGitHubApp = mock(async () => ({ status: "unbound", configured: true, linkUrl: "https://api.example.test/github/connect", })); const refreshGitHub = mock(async () => {}); let sessionAccess: Map | null = null; const updateSessionToolPolicy = mock( async (_workspaceId: string, _id: string, request: UpdateSessionToolPolicyRequest) => ({ firstPartyMcpTools: request.mode === "explicit" ? request.firstPartyMcpTools : [], }) as Session, ); const context = { client: { connectTransport: () => ({}), listCapabilities: async () => ({ items: [ { ...liveCatalogItem, enabled, ...(personal ? { connectionRef: { subjectScope: "subject", providerDomain: "api.example.com", kind: "api_key", }, } : {}), }, ...(createdCatalogItem ? [createdCatalogItem] : []), ], }), createCapability: mock( async (_workspaceId: string, input: { name: string; endpointUrl: string }) => { createdCatalogItem = CapabilityCatalogItem.parse({ id: "mcp:reviewed", kind: "mcp", source: "manual", name: input.name, endpointUrl: input.endpointUrl, runtime: { available: true, mcpServerId: "reviewed" }, }); return createdCatalogItem; }, ), inspectMcpAuthentication: mock( async (): Promise<{ kind: "oauth2" | "none" | "unknown"; message?: string | undefined; }> => ({ kind: "none" }), ), listConnections: async () => connections, listSocialConnections: async () => [], listSlackInstallationBindings: async () => [], listIntegrationDefinitions: async () => ({ definitions: [] }), listApiIntegrations: async () => ({ integrations: [] }), catalogAssetUrl: (path: string) => path, getGitHubApp, createConnection, updateConnection, enableCapability, updateSessionToolPolicy, getSession: async (_workspaceId: string, id: string): Promise => sessionAccess?.get(id) ?? ({ id: "session", workspaceId: "workspace", tenancy: { visibility: "workspace", authorityEpoch: 4 }, tools: [{ kind: "mcp", id: "example" }], toolPolicy: { mode: "explicit" }, firstPartyMcpTools: [], toolPolicyVersion: 1, } as unknown as Session), }, workspaceCapabilityCatalog: [catalogItem], githubStatus: null as { status: string } | null, githubRepos: [], githubCatalogReady: true, githubStatusFailed: false, repoBusy: false, personalGitHubBusy: false, refreshGitHub, refreshPersonalGitHub: async () => {}, captureWorkspaceInvocation: () => ({ revision: 1 }), workspaces: [], refreshWorkspaceMcpServers: async () => {}, accessContext: { subjectId: "human", workspaceGrants: [{ workspaceId: "workspace", permissions: ["connections:read"] }], }, }; mock.module("../../app-context", () => ({ useAppContext: () => context })); mock.module("sonner", () => ({ toast: { success: () => {}, error: () => {} } })); GlobalRegistrator.register(); const { sessionAuthRecommendation } = await import("../../../session-auth-recommendation"); const { buildTimeline } = await import("../../.."); const { createRoot } = await import("react-dom/client"); const { SessionCapabilityCard } = await import("./session-capability-card"); const item = { id: "notice", kind: "auth-needed", serverId: "opengeni", providerDomain: "api.example.com", reason: "missing_connection", capability: { id: "example", name: "Example", kind: "mcp", action: "add_credentials", rationale: "Use Example for your report.", requiredVariables: [], }, } as unknown as AuthNeededItem; beforeAll(() => { (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean | undefined }).IS_REACT_ACT_ENVIRONMENT = true; }); afterAll(() => { mock.restore(); GlobalRegistrator.unregister(); }); async function render( personalAccount = false, currentCatalogItem = catalogItem, cachedCatalogItem = catalogItem, missingGrant = false, visibility: "private" | "workspace" = "workspace", noticeOverride: AuthNeededItem = item, ) { sessionAccess = null; updateSessionToolPolicy.mockClear(); context.accessContext.subjectId = "human"; personal = personalAccount; liveCatalogItem = currentCatalogItem; context.workspaceCapabilityCatalog = [cachedCatalogItem]; enabled = personalAccount; connections = personalAccount ? [{ ...row, subjectId: "owner", authorityId: "authority" }] : []; updateConnection.mockClear(); createConnection.mockClear(); enableCapability.mockClear(); const container = document.createElement("div"); document.body.append(container); const root = createRoot(container); let notice = noticeOverride; if (missingGrant) { const startupEvents = [ { id: "startup-auth", workspaceId: "workspace", sessionId: "session", turnId: "turn", turnAttemptId: "attempt", sequence: 1, type: "tool.auth_needed", occurredAt: "2026-09-11T00:00:00.000Z", payload: { serverId: "example", providerDomain: "api.example.com", reason: "personal_authority_unavailable", }, }, ]; expect(buildTimeline(t, startupEvents)).toEqual([]); const requested = buildTimeline(t, [ ...startupEvents, { ...startupEvents[0]!, id: "requested-auth", sequence: 2, payload: { serverId: "example", toolName: "capability_authorization_request", providerDomain: "api.example.com", reason: "missing_connection", capability: { id: "example", name: "Example", kind: "mcp", source: "manual", action: "connect", rationale: "Review permission to use your personal account for this request.", requiredVariables: [], }, }, }, ]); const auth = requested.find((entry) => entry.kind === "auth-needed"); expect(auth).toBeDefined(); if (!auth) throw new Error("Requested authorization event was lost from the timeline"); const recommendation = sessionAuthRecommendation( t, auth, (await context.client.listCapabilities()).items as CapabilityCatalogItem[], ); expect(recommendation).toBeDefined(); if (!recommendation) throw new Error("Requested authorization did not resolve to consent"); notice = recommendation; } await act(async () => root.render( , ), ); return { container: document.body, host: container, rerender: async (workspaceId: string) => { await act(async () => root.render( , ), ); }, close: async () => { await act(async () => root.unmount()); container.remove(); }, }; } function button(container: HTMLElement, label: string) { const result = [...container.querySelectorAll("button")].find((node) => node.textContent?.includes(label), ); if (!result) throw new Error(`Missing ${label}: ${container.textContent}`); return result; } describe("conversation connection card", () => { test("1Password setup preserves the chat and links to this workspace's protected form", async () => { const onePassword = CapabilityCatalogItem.parse({ id: "api:1password", name: "1Password", kind: "api", source: "built_in", surfaceType: "first_party_onepassword", providerDomain: "1password.com", runtime: { available: true }, }); const notice = { ...item, capability: { ...item.capability!, id: onePassword.id, name: "1Password", kind: "api" as const, }, }; const h = await render(false, onePassword, onePassword, false, "workspace", notice); try { await act(async () => button(h.container, "Connect").click()); const link = h.container.querySelector('a[href*="/secrets"]'); expect(link?.getAttribute("href")).toBe( "/workspaces/workspace/secrets?view=connect-1password", ); expect(link?.getAttribute("target")).toBe("_blank"); expect(createConnection).not.toHaveBeenCalled(); expect(enableCapability).not.toHaveBeenCalled(); } finally { await h.close(); } }); const fiken = CapabilityCatalogItem.parse({ id: "api:fiken", name: "Fiken", kind: "api", source: "built_in", surfaceType: "first_party_fiken", providerDomain: "fiken.no", enabled: true, tools: [{ kind: "mcp", id: "opengeni" }], metadata: { firstPartyMcpTools: ["fiken_companies_list", "fiken_invoices_list"] }, }); async function renderFiken() { const notice = { ...item, capability: { ...item.capability!, id: fiken.id, name: "Fiken", kind: "api" as const }, }; const h = await render(false, fiken, fiken, false, "workspace", notice); enabled = true; connections = [ { ...row, providerDomain: "fiken.no", metadata: { credentialRole: "fiken_api_token" } }, ]; sessionAccess = new Map( ["parent", "session"].map((id) => [ id, { id, title: id === "parent" ? "Accounting parent" : "Accounting child", parentSessionId: id === "parent" ? null : "parent", tools: [{ kind: "mcp", id: "existing" }], firstPartyMcpTools: ["session_pause"], toolPolicyVersion: 3, toolPolicy: { mode: "explicit", inheritedFromSessionId: id === "parent" ? null : "parent", }, effectiveToolPolicy: { mandatoryIds: ["opengeni"] }, } as Session, ]), ); await act(async () => button(h.container, "Review").click()); return h; } test("child Fiken review makes no writes until explicit parent confirmation", async () => { const h = await renderFiken(); try { await act(async () => button(h.container, "Add tools").click()); expect(h.container.textContent).toContain("This chat inherits its tool access"); expect(h.container.textContent).toContain("Accounting parent"); expect( h.container.querySelector('a[href="/workspaces/workspace/sessions/parent"]'), ).not.toBeNull(); expect(h.container.textContent).not.toContain("Ask an admin"); expect(updateSessionToolPolicy).not.toHaveBeenCalled(); await act(async () => button(h.container, "Enable in parents and add tools").click()); expect(updateSessionToolPolicy.mock.calls.map((call) => call[1])).toEqual([ "parent", "session", ]); expect(createConnection).not.toHaveBeenCalled(); expect(updateConnection).not.toHaveBeenCalled(); } finally { await h.close(); } }); test("canceling parent review makes no policy changes", async () => { const h = await renderFiken(); try { await act(async () => button(h.container, "Add tools").click()); await act(async () => button(h.container, "Cancel").click()); expect(updateSessionToolPolicy).not.toHaveBeenCalled(); } finally { await h.close(); } }); test("stale parent review stays incomplete and requires another review", async () => { const h = await renderFiken(); try { await act(async () => button(h.container, "Add tools").click()); sessionAccess!.get("parent")!.toolPolicyVersion++; await act(async () => button(h.container, "Enable in parents and add tools").click()); expect(h.container.textContent).toContain("Chat access changed"); expect(button(h.container, "Add tools")).toBeDefined(); expect(updateSessionToolPolicy).not.toHaveBeenCalled(); } finally { await h.close(); } }); test("changing the human actor invalidates a pending parent approval", async () => { const h = await renderFiken(); try { await act(async () => button(h.container, "Add tools").click()); context.accessContext.subjectId = "other-human"; await h.rerender("workspace"); expect(h.container.textContent).not.toContain("Enable in parents and add tools"); expect(updateSessionToolPolicy).not.toHaveBeenCalled(); } finally { await h.close(); } }); test("retired in-flight preparation cannot block the new actor or clear its work", async () => { const h = await renderFiken(); const original = context.client.getSession; let completeRead!: (session: Session) => void; context.client.getSession = async () => await new Promise((resolve) => { completeRead = resolve; }); try { await act(async () => button(h.container, "Add tools").click()); expect(button(h.container, "Add tools").disabled).toBe(true); context.accessContext.subjectId = "other-human"; context.client.getSession = original; await h.rerender("workspace"); expect(button(h.container, "Add tools").disabled).toBe(false); await act(async () => button(h.container, "Add tools").click()); expect(h.container.textContent).toContain("Enable in parents and add tools"); await act(async () => completeRead(sessionAccess!.get("session")!)); expect(h.container.textContent).toContain("Enable in parents and add tools"); expect(updateSessionToolPolicy).not.toHaveBeenCalled(); } finally { context.client.getSession = original; await h.close(); } }); test("URL-less setup keeps provider guidance without offering an ineffective retry", async () => { context.client.inspectMcpAuthentication.mockClear(); const custom = CapabilityCatalogItem.parse({ ...catalogItem, authKind: null, mcpUrl: null, endpointUrl: null, metadata: {}, }); const h = await render(false, custom, custom); try { await act(async () => button(h.container, "Connect Example").click()); expect(h.container.textContent).toContain("Check the provider's instructions."); expect( [...h.container.querySelectorAll("button")].some((node) => node.textContent === "Retry"), ).toBe(false); expect(context.client.inspectMcpAuthentication).not.toHaveBeenCalled(); } finally { await h.close(); } }); test("shows discovery failures and retries into OAuth without writing a connection", async () => { let inspections = 0; context.client.inspectMcpAuthentication.mockImplementation(async () => { inspections += 1; return inspections === 1 ? { kind: "unknown", message: "The provider returned HTTP 403 while checking how to sign in.", } : { kind: "oauth2" }; }); const custom = { ...catalogItem, authKind: null, metadata: { authDiscovery: "unknown" } }; const h = await render(false, custom, custom); try { await act(async () => button(h.container, "Connect Example").click()); expect(h.container.textContent).toContain("HTTP 403"); await act(async () => button(h.container, "Retry").click()); expect(inspections).toBe(2); expect(h.container.textContent).not.toContain("HTTP 403"); expect(button(h.container, "Connect for workspace").disabled).toBe(false); expect(createConnection).not.toHaveBeenCalled(); expect(enableCapability).not.toHaveBeenCalled(); } finally { context.client.inspectMcpAuthentication.mockImplementation(async () => ({ kind: "none" })); await h.close(); } }); test("reviews an agent-suggested URL before a human adds the MCP catalog entry", async () => { createdCatalogItem = null; context.client.createCapability.mockClear(); const suggested = { ...item, id: "custom-mcp-notice", capability: null, providerDomain: "mcp.example.test", setupRequest: { kind: "mcp" as const, name: "Internal Tools", endpointUrl: "https://mcp.example.test/mcp", rationale: "Find the records you asked about.", }, } as AuthNeededItem; const h = await render(false, catalogItem, catalogItem, false, "workspace", suggested); try { expect(h.host.textContent).toContain("Find the records you asked about."); expect(context.client.createCapability).not.toHaveBeenCalled(); await act(async () => { button(h.host, "Review server").click(); await new Promise((resolve) => setTimeout(resolve, 20)); }); expect(h.host.querySelector('[data-state="setup"]')).not.toBeNull(); expect(h.container.textContent).toContain("Server URL"); expect(button(h.container, "Add MCP server").disabled).toBe(true); expect(h.container.textContent).toContain("A workspace admin needs to add this server"); context.accessContext.workspaceGrants = [ { workspaceId: "workspace", permissions: ["capabilities:manage", "connections:read"] }, ]; await h.rerender("workspace"); expect(button(h.container, "Add MCP server").disabled).toBe(false); await act(async () => button(h.container, "Add MCP server").click()); expect(context.client.createCapability).toHaveBeenCalledTimes(1); expect(context.client.createCapability).toHaveBeenCalledWith("workspace", { kind: "mcp", source: "manual", name: "Internal Tools", endpointUrl: "https://mcp.example.test/mcp", }); expect(h.host.textContent).toContain("Connect Internal Tools"); await act(async () => new Promise((resolve) => setTimeout(resolve, 40))); expect(document.activeElement).toBe(button(h.container, "Connect Internal Tools")); await act(async () => button(h.container, "Connect Internal Tools").click()); expect(h.container.textContent).toContain("Add to workspace"); } finally { context.accessContext.workspaceGrants = [ { workspaceId: "workspace", permissions: ["connections:read"] }, ]; createdCatalogItem = null; await h.close(); } }); test("a viewer returning after an admin adds the proposed server sees the connection action", async () => { createdCatalogItem = CapabilityCatalogItem.parse({ id: "mcp:reviewed", kind: "mcp", source: "manual", name: "Internal Tools", endpointUrl: "https://mcp.example.test/mcp", runtime: { available: true, mcpServerId: "reviewed" }, }); context.accessContext.workspaceGrants = [ { workspaceId: "workspace", permissions: ["connections:read"] }, ]; context.client.createCapability.mockClear(); const suggested = { ...item, id: "custom-mcp-notice-return", capability: null, providerDomain: "mcp.example.test", setupRequest: { kind: "mcp" as const, name: "Internal Tools", endpointUrl: "https://mcp.example.test/mcp", rationale: "Find the requested records.", }, } as AuthNeededItem; const h = await render(false, catalogItem, catalogItem, false, "workspace", suggested); try { await act(async () => new Promise((resolve) => setTimeout(resolve, 20))); expect(h.host.textContent).toContain("Connect Internal Tools"); expect(h.host.textContent).not.toContain("A workspace admin needs to add this server"); expect(context.client.createCapability).not.toHaveBeenCalled(); } finally { createdCatalogItem = null; await h.close(); } }); const githubItem = { ...catalogItem, id: "api:github-app", kind: "api" as const, name: "GitHub App", providerDomain: "github.com", }; const allowGitHub = () => { context.accessContext.workspaceGrants = [ { workspaceId: "workspace", permissions: ["connections:read", "github:use", "sessions:control"], }, ]; }; const githubNotice = { ...item, capability: { ...item.capability!, id: "api:github-app", kind: "api" as const, name: "GitHub App", }, } as AuthNeededItem; test("GitHub's bundled logo and verified binding persist in the conversation card", async () => { allowGitHub(); context.githubStatus = { status: "bound" }; const h = await render(false, githubItem, githubItem, false, "workspace", githubNotice); try { // Happy DOM cannot serve static assets; the source path is checked in // capability-logo-source.test.ts, while this verifies the live card state. expect(h.container.querySelector('[data-state="complete"]')).not.toBeNull(); expect(h.container.textContent).toContain("Connected to this workspace"); expect(h.container.textContent).not.toContain("Available in this conversation"); expect(h.container.textContent).not.toContain("Connect GitHub App"); } finally { await h.close(); context.githubStatus = null; } }); test("a returning GitHub binding refreshes the existing card, including later disconnection", async () => { allowGitHub(); context.githubStatus = { status: "unbound" }; const h = await render(false, githubItem, githubItem, false, "workspace", githubNotice); try { expect(h.container.querySelector('[data-state="suggested"]')).not.toBeNull(); context.githubStatus = { status: "bound" }; await h.rerender("workspace"); expect(h.container.querySelector('[data-state="complete"]')).not.toBeNull(); context.githubStatus = { status: "unbound" }; await h.rerender("workspace"); expect(h.container.querySelector('[data-state="suggested"]')).not.toBeNull(); } finally { await h.close(); context.githubStatus = null; } }); test("GitHub starts on one click, visibly waits, ignores a second click, and confirms a binding", async () => { allowGitHub(); context.githubStatus = null; getGitHubApp.mockClear(); refreshGitHub.mockClear(); let resolveStatus!: (value: Awaited>) => void; getGitHubApp.mockImplementationOnce( () => new Promise((resolve) => { resolveStatus = resolve; }), ); const h = await render(false, githubItem, githubItem, false, "workspace", githubNotice); try { await act(async () => button(h.container, "Connect GitHub App").click()); const waiting = button(h.container, "Opening GitHub…"); expect(waiting.disabled).toBe(true); expect(getGitHubApp).toHaveBeenCalledTimes(1); await act(async () => waiting.click()); expect(getGitHubApp).toHaveBeenCalledTimes(1); expect(getGitHubApp).toHaveBeenCalledWith("workspace", { returnPath: "/workspaces/workspace/sessions/session", }); await act(async () => { resolveStatus({ status: "bound", configured: true, linkUrl: "" }); }); expect(h.container.querySelector('[data-state="complete"]')).not.toBeNull(); expect(h.container.textContent).toContain("Connected to this workspace"); expect(refreshGitHub).toHaveBeenCalledTimes(1); } finally { await h.close(); } }); test("a BFCache return unlocks GitHub and ignores an old pending status response", async () => { allowGitHub(); context.githubStatus = null; refreshGitHub.mockClear(); let resolveOldStatus!: (value: Awaited>) => void; getGitHubApp.mockImplementationOnce( () => new Promise((resolve) => { resolveOldStatus = resolve; }), ); const h = await render(false, githubItem, githubItem, false, "workspace", githubNotice); try { await act(async () => button(h.container, "Connect GitHub App").click()); expect(button(h.container, "Opening GitHub…").disabled).toBe(true); await act(async () => { const restored = new Event("pageshow") as PageTransitionEvent; Object.defineProperty(restored, "persisted", { value: true }); window.dispatchEvent(restored); }); expect(button(h.container, "Connect GitHub App").disabled).toBe(false); expect(refreshGitHub).toHaveBeenCalledWith("workspace"); await act(async () => resolveOldStatus({ status: "bound", configured: true, linkUrl: "" })); expect(h.container.querySelector('[data-state="suggested"]')).not.toBeNull(); getGitHubApp.mockImplementationOnce(async () => ({ status: "bound", configured: true, linkUrl: "", })); await act(async () => button(h.container, "Connect GitHub App").click()); expect(h.container.querySelector('[data-state="complete"]')).not.toBeNull(); } finally { await h.close(); } }); test("GitHub's failed start offers a retry in the existing dialog", async () => { allowGitHub(); getGitHubApp.mockImplementationOnce(async () => { throw new Error("Temporary status failure"); }); const h = await render(false, githubItem, githubItem, false, "workspace", githubNotice); try { await act(async () => button(h.container, "Connect GitHub App").click()); expect(h.container.textContent).toContain("Temporary status failure"); expect(button(h.container, "Try again")).toBeDefined(); } finally { await h.close(); } }); test("OAuth CTA retains the live provider name after renamed setup is opened and cancelled", async () => { const cached = { ...catalogItem, authKind: "oauth2" as const }; const h = await render(false, { ...cached, name: "Current Example" }, cached); try { expect(button(h.container, "Connect Current Example").textContent).toBe( "Connect Current Example", ); await act(async () => button(h.container, "Connect Current Example").click()); expect(h.container.querySelector("h3")?.textContent).toBe("Current Example"); await act(async () => h.container .querySelector('[aria-label="Close connection setup"]')! .click(), ); expect(h.container.querySelector('[data-state="suggested"]')).not.toBeNull(); expect(h.container.querySelector("h3")?.textContent).toBe("Current Example"); expect(button(h.container, "Connect Current Example").textContent).toBe( "Connect Current Example", ); expect(createConnection).not.toHaveBeenCalled(); expect(updateConnection).not.toHaveBeenCalled(); expect(enableCapability).not.toHaveBeenCalled(); } finally { await h.close(); } }); test("expanded header reflects the live provider identity rather than stale recommendation copy", async () => { const h = await render(false, { ...catalogItem, name: "Current Example", providerDomain: "current.example.com", }); await act(async () => button(h.container, "Add API key").click()); expect(h.container.querySelector("h3")?.textContent).toBe("Current Example"); expect(h.container.textContent).toContain("current.example.com"); expect(h.container.textContent).not.toContain("api.example.com"); expect(h.container.querySelectorAll("h3")).toHaveLength(1); await h.close(); }); test("first Connect opens the shared centered dialog and cancellation writes nothing", async () => { const h = await render(); expect(h.container.textContent).toContain("You choose what to authorize"); expect(h.container.querySelector('[data-state="suggested"]')).not.toBeNull(); await act(async () => button(h.container, "Add API key").click()); expect(h.container.querySelector('input[type="password"]')).not.toBeNull(); expect(h.container.querySelectorAll("h3")).toHaveLength(1); expect(h.container.textContent).toContain("Connect for workspace"); const dialog = document.querySelector('[role="dialog"]'); expect(dialog).not.toBeNull(); expect(dialog?.className).toContain("og-session-capability-dialog"); expect(h.host.querySelector("form")).toBeNull(); await act(async () => button(h.container, "Cancel").click()); expect(h.container.querySelector("form")).toBeNull(); expect(createConnection).not.toHaveBeenCalled(); expect(enableCapability).not.toHaveBeenCalled(); await h.close(); }); test("an open connection card masks its connected account when read access is revoked", async () => { const h = await render(true); try { await act(async () => button(h.container, "Add API key").click()); expect(button(h.container, "Add tools").disabled).toBe(false); context.accessContext.workspaceGrants = []; await h.rerender("workspace"); expect( [...h.container.querySelectorAll("button")].some((node) => node.textContent?.includes("Add tools"), ), ).toBe(false); context.accessContext.workspaceGrants = [ { workspaceId: "workspace", permissions: ["connections:read"] }, ]; await h.rerender("workspace"); expect(button(h.container, "Add tools").disabled).toBe(false); } finally { context.accessContext.workspaceGrants = [ { workspaceId: "workspace", permissions: ["connections:read"] }, ]; await h.close(); } }); test("switching workspace discards the open dialog and its credential draft", async () => { const h = await render(); try { await act(async () => button(h.container, "Add API key").click()); expect(document.querySelector('[role="dialog"]')).not.toBeNull(); await h.rerender("other-workspace"); expect(document.querySelector('[role="dialog"]')).toBeNull(); expect(h.container.querySelector('input[type="password"]')).toBeNull(); expect(createConnection).not.toHaveBeenCalled(); } finally { await h.close(); } }); test("credentials go only to the connection API and success follows enable", async () => { const h = await render(); await act(async () => button(h.container, "Add API key").click()); const input = h.container.querySelector('input[type="password"]') as HTMLInputElement; await act(async () => { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!; setter.call(input, "secret-for-provider-only"); input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); }); await act(async () => h.container .querySelector("form")! .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })), ); expect(createConnection).toHaveBeenCalledTimes(1); expect(enableCapability).toHaveBeenCalledTimes(1); expect(h.container.textContent).toContain("Connected · Available in this conversation"); expect(h.container.querySelector('[data-state="complete"]')).not.toBeNull(); expect(h.container.querySelector("button")).toBeNull(); expect(h.container.textContent).not.toContain("secret-for-provider-only"); await h.close(); }); test.each(["workspace", "private"] as const)( "a personal account attaches tools without a conversation grant (%s)", async (visibility) => { const h = await render(true, catalogItem, catalogItem, true, visibility); try { await act(async () => button(h.container, "Add API key").click()); const dialog = document.querySelector("[role=dialog]"); expect(dialog?.textContent).toContain( "Your messages and personal schedules can use this account.", ); expect(dialog?.querySelector("input[type=checkbox]")).toBeNull(); const add = button(h.container, "Add tools"); expect(add.disabled).toBe(false); await act(async () => add.click()); expect(createConnection).not.toHaveBeenCalled(); expect(h.container.textContent).toContain("Connected · Available in this conversation"); } finally { await h.close(); } }, ); test("retry after a partial save reuses the persisted Connection", async () => { const h = await render(); enableCapability.mockImplementationOnce(async () => { throw new Error("Enable failed"); }); await act(async () => button(h.container, "Add API key").click()); const input = h.container.querySelector('input[type="password"]') as HTMLInputElement; await act(async () => { Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call( input, "fixture-credential", ); input.dispatchEvent(new Event("input", { bubbles: true })); }); const submit = async () => { await act(async () => { h.container .querySelector("form")! .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); }); }; await submit(); expect(h.container.textContent).toContain("Enable failed"); expect(h.container.querySelector('[data-state="complete"]')).toBeNull(); await submit(); expect(createConnection).toHaveBeenCalledTimes(1); expect(updateConnection).toHaveBeenCalledTimes(1); expect(h.container.textContent).toContain("Connected · Available in this conversation"); await h.close(); }); });