Files
PaperClipAI/ui/src/api/issue-summaries.test.ts
T
DottaandPaperclip 3e5a9f5afe perf(ui): load recent tasks with bounded summaries (#16851)
Replace passive recent-task detail reads with bounded, authorized summaries. Keep account isolation, stored history, task actions, and live recovery behavior.

Overlap access checks with shell settings and avoid duplicate session and health reads on the first healthy socket opening. Retain domain reconciliation.

Add focused coverage and local performance evidence. Isolate the existing cache capacity test from entry expiry on slow CI runners.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-19 12:13:28 -05:00

52 lines
2.5 KiB
TypeScript

import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
import { issuesApi } from "./issues";
const first = "11111111-1111-4111-8111-111111111111";
const second = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
describe("issue summary HTTP requests", () => {
const fetchMock = vi.fn();
beforeEach(() => { fetchMock.mockReset(); vi.stubGlobal("fetch", fetchMock); });
afterEach(() => vi.unstubAllGlobals());
it("normalizes IDs, forwards cancellation, and disables the HTTP cache", async () => {
fetchMock.mockResolvedValue(Response.json([]));
const controller = new AbortController();
await issuesApi.summaries("company", [second.toUpperCase(), ` ${first} `, first], { signal: controller.signal });
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, options] = fetchMock.mock.calls[0];
expect(url).toBe(`/api/companies/company/issues/summaries?ids=${first}%2C${second}`);
expect(options.cache).toBe("no-store");
expect(options.signal).toBeInstanceOf(AbortSignal);
});
it("does not send empty or oversized batches", async () => {
expect(await issuesApi.summaries("company", [])).toEqual([]);
await expect(issuesApi.summaries("company", Array(6).fill(first))).rejects.toThrow("Too many");
expect(fetchMock).not.toHaveBeenCalled();
});
it("does not reuse an in-flight response after an account change", async () => {
let resolveOld!: (value: Response) => void;
fetchMock.mockReturnValueOnce(new Promise(resolve => { resolveOld = resolve; }))
.mockResolvedValueOnce(Response.json([{ id: first, title: "New account" }]));
const oldAccount = issuesApi.summaries("company", [first]);
const newAccount = issuesApi.summaries("company", [first]);
expect((await newAccount)[0].title).toBe("New account");
resolveOld(Response.json([{ id: first, title: "Old account" }]));
expect((await oldAccount)[0].title).toBe("Old account");
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it("aborts a summary request when its observer leaves", async () => {
fetchMock.mockImplementation((_url, options) => new Promise((_resolve, reject) => {
options.signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")));
}));
const controller = new AbortController();
const pending = issuesApi.summaries("company", [first], { signal: controller.signal });
controller.abort();
await expect(pending).rejects.toMatchObject({ name: "AbortError" });
expect(fetchMock.mock.calls[0][1].signal.aborted).toBe(true);
});
});