Files
PaperClipAI/ui/src/pages/apps/chat/ChatExecutionDefaults.test.tsx
T
DottaandPaperclip 473c80d13f Automatically import and update project repository skills (#16066)
Automatically import skills from new GitHub project repositories and update enabled sources in the background. Preserve project-derived access, saved selections, immutable execution versions and explicit pins; add legacy adoption, refresh and connection repair in Sources.

Include durable scheduling and bounded work credits, production Storybook journeys, real GitHub-to-routine acceptance receipts and final-head CI/security/Greptile qualification.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-13 16:11:43 -05:00

73 lines
4.2 KiB
TypeScript

// @vitest-environment jsdom
import { act } 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 { ChatExecutionDefaults } from "./ChatExecutionDefaults";
import type { ChatExecutionDefaults as Defaults } from "@paperclipai/shared";
const mocks = vi.hoisted(() => ({ projects: vi.fn(), workspaces: vi.fn(), save: vi.fn() }));
vi.mock("@/api/projects", () => ({ projectsApi: { list: mocks.projects } }));
vi.mock("@/api/execution-workspaces", () => ({ executionWorkspacesApi: { listSummaries: mocks.workspaces } }));
let root: Root, container: HTMLDivElement, client: QueryClient;
beforeEach(() => {
vi.clearAllMocks(); mocks.projects.mockResolvedValue([]); mocks.workspaces.mockResolvedValue([]); mocks.save.mockResolvedValue(undefined);
container = document.createElement("div"); document.body.append(container); root = createRoot(container);
client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
});
afterEach(async () => { await act(async () => root.unmount()); client.clear(); container.remove(); });
async function render(value: Defaults | null = { projectId: null }) {
await act(async () => root.render(<QueryClientProvider client={client}><ChatExecutionDefaults companyId="company" value={value} resource onSave={mocks.save} /></QueryClientProvider>));
await vi.waitFor(() => expect(container.querySelectorAll("select")[1].disabled).toBe(false));
}
async function choose(value: string) {
await act(async () => { const select = container.querySelectorAll("select")[1]; select.value = value; select.dispatchEvent(new Event("change", { bubbles: true })); });
}
async function save() {
await act(async () => container.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })));
}
it("lets a destination clear inherited workspace independently of project", async () => {
await render(); await choose("clear"); await save();
expect(mocks.save).toHaveBeenCalledWith({ projectId: null, workspace: null });
expect(container.textContent).toContain("Saved for new tasks.");
});
it("keeps edits visible after a save failure and permits retry", async () => {
mocks.save.mockRejectedValueOnce(new Error("Workspace access was revoked"));
await render(); await choose("task"); await save();
expect(container.querySelector('[role="alert"]')?.textContent).toContain("Workspace access was revoked");
expect(container.querySelectorAll("select")[1].value).toBe("task");
await save();
expect(mocks.save).toHaveBeenLastCalledWith({ projectId: null, workspace: { kind: "task_directory" } });
expect(container.textContent).toContain("Saved for new tasks.");
});
it("retains accepted selections until the parent refreshes and then follows later defaults", async () => {
await render();
await choose("task");
await save();
expect(container.querySelectorAll("select")[1].value).toBe("task");
expect(container.querySelector<HTMLButtonElement>('button[type="submit"]')!.disabled).toBe(true);
expect(container.textContent).toContain("Saved for new tasks.");
await render({ projectId: null, workspace: { kind: "task_directory" } });
await render({ projectId: null, workspace: null });
expect(container.querySelectorAll("select")[1].value).toBe("clear");
expect(mocks.save).toHaveBeenCalledTimes(1);
});
it("offers and saves an idle workspace from the shared task selection filter", async () => {
mocks.workspaces.mockResolvedValue([
{ id: "active-workspace", name: "Active files", status: "active", closedAt: null },
{ id: "idle-workspace", name: "Idle files", status: "idle", closedAt: null },
]);
await render();
expect(mocks.workspaces).toHaveBeenCalledWith("company", { selectableForTask: true });
const options = [...container.querySelectorAll("select")[1].options].map((option) => option.value);
expect(options).toContain("existing:active-workspace");
expect(options).toContain("existing:idle-workspace");
await choose("existing:idle-workspace");
await save();
expect(mocks.save).toHaveBeenCalledWith({
projectId: null,
workspace: { kind: "existing", workspaceId: "idle-workspace" },
});
});