mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 13:20:54 +01:00
78 lines
4.5 KiB
TypeScript
78 lines
4.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act, useState } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
import { DotRunnerConnection } from "./DotRunnerConnection";
|
|
|
|
const api = vi.hoisted(() => ({ get: vi.fn(), post: vi.fn(), delete: vi.fn() }));
|
|
vi.mock("../api/client", () => ({ api }));
|
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
const binding = { id: "binding", status: "connected", connected: true, subscriptionVerified: true, hasPendingChallenge: false };
|
|
let root: Root;
|
|
let container: HTMLDivElement;
|
|
let client: QueryClient;
|
|
const onBinding = vi.fn();
|
|
function Form() {
|
|
const [bindingId, setBindingId] = useState("");
|
|
return <><DotRunnerConnection companyId="company" agentId="agent" bindingId={bindingId}
|
|
onBinding={id => { onBinding(id); setBindingId(id); }} /><output>{bindingId}</output></>;
|
|
}
|
|
async function flush() { await act(async () => { await new Promise(resolve => setTimeout(resolve, 20)); }); }
|
|
async function render() {
|
|
await act(async () => root.render(<QueryClientProvider client={client}><Form /></QueryClientProvider>));
|
|
await flush();
|
|
}
|
|
beforeEach(() => {
|
|
client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
|
|
container = document.createElement("div"); document.body.append(container); root = createRoot(container);
|
|
api.get.mockResolvedValue({ enabled: true, resourceUrl: "https://paperclip.example/mcp/runner", binding: null });
|
|
});
|
|
afterEach(async () => { await act(async () => root.unmount()); client.clear(); container.remove(); vi.resetAllMocks(); });
|
|
|
|
it("restores the server binding when the form is reopened after pairing without saving", async () => {
|
|
api.post.mockImplementation(async () => {
|
|
api.get.mockResolvedValue({ enabled: true, resourceUrl: "https://paperclip.example/mcp/runner", binding });
|
|
return { bindingId: binding.id, pairingCode: "one-use-code", expiresAt: "2026-10-07T01:00:00Z" };
|
|
});
|
|
await render();
|
|
await act(async () => Array.from(container.querySelectorAll("button")).find(button => button.textContent === "Pair Dot")!.click());
|
|
await flush();
|
|
expect(container.querySelector("output")?.textContent).toBe(binding.id);
|
|
// Closing the form discards its local adapter edits, while pairing is durable.
|
|
await act(async () => root.render(null));
|
|
client.clear(); onBinding.mockClear();
|
|
await render();
|
|
expect(container.querySelector("output")?.textContent).toBe(binding.id);
|
|
expect(onBinding).toHaveBeenCalledTimes(1);
|
|
expect(Array.from(container.querySelectorAll("button")).some(button => button.textContent === "Pair Dot")).toBe(false);
|
|
});
|
|
|
|
it("does not restore a stale binding while revocation refreshes the connection", async () => {
|
|
api.get.mockResolvedValue({ enabled: true, resourceUrl: "https://paperclip.example/mcp/runner", binding });
|
|
api.delete.mockImplementation(async () => {
|
|
api.get.mockResolvedValue({ enabled: true, resourceUrl: "https://paperclip.example/mcp/runner", binding: null });
|
|
});
|
|
await render();
|
|
expect(container.querySelector("output")?.textContent).toBe(binding.id);
|
|
await act(async () => Array.from(container.querySelectorAll("button")).find(button => button.textContent === "Revoke connection")!.click());
|
|
await flush();
|
|
expect(container.querySelector("output")?.textContent).toBe("");
|
|
expect(onBinding).toHaveBeenLastCalledWith("");
|
|
expect(Array.from(container.querySelectorAll("button")).some(button => button.textContent === "Pair Dot")).toBe(true);
|
|
});
|
|
|
|
it("keeps a revoked binding cleared when the follow-up connection read fails", async () => {
|
|
api.get.mockResolvedValue({ enabled: true, resourceUrl: "https://paperclip.example/mcp/runner", binding });
|
|
api.delete.mockImplementation(async () => { api.get.mockRejectedValue(new Error("Connection refresh failed")); });
|
|
await render();
|
|
expect(container.querySelector("output")?.textContent).toBe(binding.id);
|
|
await act(async () => Array.from(container.querySelectorAll("button")).find(button => button.textContent === "Revoke connection")!.click());
|
|
await flush();
|
|
expect(container.querySelector("output")?.textContent).toBe("");
|
|
expect(onBinding).toHaveBeenLastCalledWith("");
|
|
expect(container.querySelector("[role=alert]")?.textContent).toBe("Connection refresh failed");
|
|
expect(client.getQueryData(["dot-binding", "company", "agent"])).toMatchObject({ binding: null });
|
|
expect(Array.from(container.querySelectorAll("button")).some(button => button.textContent === "Revoke connection")).toBe(false);
|
|
});
|