Files
PaperClipAI/ui/src/pages/AgentRunnerUpgrade.test.tsx
T
TonioandPaperclip 9dfdeaad86 Refine agent workspaces and organization charts (#20973)
Refine agent details, identity editing, responsive controls, and organization charts with three card tiers, clearer save feedback, and stable filtering.

Guard instruction-file renames and root configuration updates in one transaction through the agent lifecycle integration, preserving conflict checks, rollback, and configuration history.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-12 01:23:59 +00:00

93 lines
4.8 KiB
TypeScript

// @vitest-environment jsdom
import { act } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, expect, it, vi } from "vitest";
import type { AgentDetail } from "@paperclipai/shared";
import { ConfigurationTab } from "./AgentDetail";
const mocks = vi.hoisted(() => ({ update: vi.fn(), navigate: vi.fn(), toast: vi.fn() }));
vi.mock("../api/agents", async importOriginal => {
const actual = await importOriginal<typeof import("../api/agents")>();
return { ...actual, agentsApi: { ...actual.agentsApi, update: mocks.update, adapterModels: async () => [] } };
});
vi.mock("@/lib/router", async importOriginal => {
const actual = await importOriginal<typeof import("@/lib/router")>();
return { ...actual, useNavigate: () => mocks.navigate, useParams: () => ({ tab: "runtime" }) };
});
vi.mock("../context/ToastContext", async importOriginal => {
const actual = await importOriginal<typeof import("../context/ToastContext")>();
return { ...actual, useToastActions: () => ({ pushToast: mocks.toast }) };
});
vi.mock("../components/AgentConfigForm", () => ({
AgentConfigForm: ({ onSave }: { onSave: (patch: Record<string, unknown>) => Promise<unknown> }) =>
<button onClick={() => { void onSave({ runner: "paperclip" }); }}>Upgrade runner</button>,
}));
function agent(id: string): AgentDetail {
return { id, companyId: "company-1", name: id, urlKey: id, role: "engineer", permissions: {},
adapterType: "codex_local", adapterConfig: {}, runtimeConfig: {}, chainOfCommand: [] } as unknown as AgentDetail;
}
afterEach(() => vi.clearAllMocks());
it("keeps an upgrade receipt on its original agent when navigation changes the configuration page", async () => {
const original = agent("original-agent");
let finish!: (updated: AgentDetail) => void;
mocks.update.mockReturnValue(new Promise<AgentDetail>(resolve => { finish = resolve; }));
const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } });
const invalidate = vi.spyOn(client, "invalidateQueries");
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
const noop = () => {};
const saved = vi.fn();
const render = (value: AgentDetail) => root.render(<QueryClientProvider client={client}>
<ConfigurationTab agent={value} companyId={value.companyId} onDirtyChange={noop} onSaveActionChange={noop}
onCancelActionChange={noop} onSavingChange={noop} onSaved={saved} updatePermissions={{ mutate: noop, isPending: false }} />
</QueryClientProvider>);
try {
await act(async () => render(original));
await act(async () => container.querySelector("button")!.click());
await act(async () => render(agent("other-agent")));
await act(async () => finish({ ...original, adapterType: "paperclip_runner", adapterConfig: { provider: "codex" } }));
await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); });
expect(mocks.update).toHaveBeenCalledWith(original.id, { runner: "paperclip" }, original.companyId);
expect(mocks.navigate).not.toHaveBeenCalled();
expect(saved).not.toHaveBeenCalled();
expect(invalidate).toHaveBeenCalledWith({ queryKey: ["agents", "detail", original.id] });
expect(invalidate).not.toHaveBeenCalledWith({ queryKey: ["agents", "detail", "other-agent"] });
} finally {
await act(async () => root.unmount());
client.clear();
container.remove();
}
});
it.each(["runtime", "secrets"] as const)("notifies the save tray after a successful %s save", async content => {
const original = agent("saved-agent");
const updated = { ...original, adapterType: "paperclip_runner", adapterConfig: { provider: "codex" } };
mocks.update.mockResolvedValue(updated);
const saved = vi.fn(), noop = () => {};
const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } });
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
try {
await act(() => root.render(<QueryClientProvider client={client}>
<ConfigurationTab agent={original} companyId={original.companyId} content={content}
onDirtyChange={noop} onSaveActionChange={noop} onCancelActionChange={noop} onSavingChange={noop}
onSaved={saved} updatePermissions={{ mutate: noop, isPending: false }} />
</QueryClientProvider>));
expect(saved).not.toHaveBeenCalled();
await act(async () => container.querySelector("button")!.click());
await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); });
expect(saved).toHaveBeenCalledOnce();
} finally {
await act(() => root.unmount());
client.clear();
container.remove();
}
});