Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions platform/configurator-mcp/test/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ describe("Streamable HTTP configurator", () => {
});

it("aborts an in-flight upstream request when the MCP client disconnects", async () => {
let upstreamStarted!: () => void;
const started = new Promise<void>((resolve) => {
upstreamStarted = resolve;
});
let observedAbort!: () => void;
const aborted = new Promise<void>((resolve) => {
observedAbort = resolve;
Expand All @@ -194,6 +198,7 @@ describe("Streamable HTTP configurator", () => {
},
{ once: true },
);
upstreamStarted();
});
});
const controller = new AbortController();
Expand All @@ -203,9 +208,10 @@ describe("Streamable HTTP configurator", () => {
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
signal: controller.signal,
});
await new Promise((resolve) => setTimeout(resolve, 20));
const rejected = expect(request).rejects.toThrow();
await started;
controller.abort();
await expect(request).rejects.toThrow();
await rejected;
await expect(aborted).resolves.toBeUndefined();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { act, type ComponentProps } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { waitForUi } from "@/test/wait-for-ui";
import { SessionComposer } from "./composer";

const mocks = vi.hoisted(() => ({ capture: vi.fn(), transcribe: vi.fn(), cancel: vi.fn(), api: vi.fn() }));
Expand Down Expand Up @@ -230,8 +231,7 @@ it.each(["transcript", "upload"])("waits for both the transcript and attachments
const input = container.querySelector<HTMLInputElement>('input[type="file"]')!;
Object.defineProperty(input, "files", { value: [new File(["pdf"], "notes.pdf", { type: "application/pdf" })] });
await act(async () => input.dispatchEvent(new Event("change", { bubbles: true })));
await act(async () => { await new Promise(resolve => setTimeout(resolve, 20)); });
expect(mocks.api).toHaveBeenCalled();
await waitForUi(() => expect(mocks.api).toHaveBeenCalled());
await record();
await click("Send message");
const completeTranscript = async () => { await act(async () => finish("Spoken text")); };
Expand All @@ -250,8 +250,7 @@ it("keeps the completed transcript for review when an attachment upload fails",
const input = container.querySelector<HTMLInputElement>('input[type="file"]')!;
Object.defineProperty(input, "files", { value: [new File(["pdf"], "notes.pdf", { type: "application/pdf" })] });
await act(async () => input.dispatchEvent(new Event("change", { bubbles: true })));
await act(async () => { await new Promise(resolve => setTimeout(resolve, 20)); });
expect(container.textContent).toContain("Upload failed");
await waitForUi(() => expect(container.textContent).toContain("Upload failed"));
await record();
await click("Send message");
await act(async () => finish("Spoken text"));
Expand Down
7 changes: 4 additions & 3 deletions platform/web/src/components/session/composer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import type { ModelOption } from "@/api";
import { waitForUi } from "@/test/wait-for-ui";
import { SessionComposer } from "./composer";

const mocks = vi.hoisted(() => ({ api: vi.fn() }));
Expand Down Expand Up @@ -57,8 +58,6 @@ async function attach(...files: File[]) {
const input = container.querySelector<HTMLInputElement>('input[type="file"]')!;
Object.defineProperty(input, "files", { value: files, configurable: true });
await act(async () => input.dispatchEvent(new Event("change", { bubbles: true })));
// The base64 read is asynchronous; let it reach the upload call.
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 20)); });
}
const png = (name = "screen.png", size = 3) => new File([new Uint8Array(size)], name, { type: "image/png" });

Expand Down Expand Up @@ -91,6 +90,7 @@ it("keeps the draft and explains when nothing can be steered", async () => {
it("uploads attachments on pick and sends them with the message", async () => {
const { onSend } = await setup({ runActive: false });
await attach(png());
await waitForUi(() => expect(uploads).toHaveLength(1));
expect(mocks.api).toHaveBeenCalledWith("POST", "/api/v1/universes/u1/attachments", { bytesBase64: btoa("\0\0\0") }, expect.any(AbortSignal));
expect(container.textContent).toContain("Uploading…");
await act(async () => uploads[0]!(`sha256:${"a".repeat(64)}`));
Expand All @@ -108,6 +108,7 @@ it("waits for uploads when Enter is pressed early, then sends", async () => {
await attach(png());
await press("Enter");
expect(onSend).not.toHaveBeenCalled();
await waitForUi(() => expect(uploads).toHaveLength(1));
await act(async () => uploads[0]!(`sha256:${"b".repeat(64)}`));
expect(onSend).toHaveBeenCalledWith(expect.objectContaining({ text: "", attachments: [expect.objectContaining({ name: "screen.png" })] }), null);
});
Expand All @@ -128,7 +129,7 @@ it("blocks sending while an upload has failed until it is removed", async () =>
mocks.api.mockRejectedValueOnce(new Error("Gateway unavailable"));
const { onSend } = await setup({ runActive: false });
await attach(png());
expect(container.textContent).toContain("Upload failed");
await waitForUi(() => expect(container.textContent).toContain("Upload failed"));
await act(async () => button("Send message").click());
expect(onSend).not.toHaveBeenCalled();
expect(container.textContent).toContain("Remove or retry the attachments that failed to upload.");
Expand Down
19 changes: 12 additions & 7 deletions platform/web/src/components/session/file-links.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { waitForUi } from "@/test/wait-for-ui";
import { appHref, blobHref } from "@/lib/blob-view";
import { ToolGroupTrace } from "./tool-trace";
import { MarkdownContent } from "./markdown-content";
Expand All @@ -19,14 +20,17 @@ const file: FileReference = {
};
let root: Root;
let container: HTMLDivElement;
let client: QueryClient;
beforeEach(() => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } } });
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
});
afterEach(async () => {
await act(async () => root.unmount());
client.clear();
container.remove();
vi.unstubAllGlobals();
});
Expand Down Expand Up @@ -137,9 +141,6 @@ it("continues to sanitize unsafe links", async () => {
});

it.each([false, true])("resolves a reference outside the loaded history and scopes the lookup to its session (image: %s)", async (showImage) => {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const resolved = showImage ? { ...file, type: "image/jpeg" } : file;
const load = vi.fn(async () => resolved);
vi.stubGlobal("URL", class extends URL {
Expand All @@ -162,8 +163,9 @@ it.each([false, true])("resolves a reference outside the loaded history and scop
</QueryClientProvider>,
),
);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 20));
await waitForUi(() => {
expect(container.querySelector("a")?.href).toContain("session=s1");
expect(container.querySelector("img")?.getAttribute("src")).toBe(showImage ? "blob:http://localhost/historical-image" : undefined);
});
expect(load).toHaveBeenCalledWith(file.handle, expect.any(AbortSignal));
expect(container.querySelector("a")!.href).toContain("session=s1");
Expand All @@ -178,7 +180,6 @@ it.each([false, true])("resolves a reference outside the loaded history and scop


it("checks history even for a visible reference and refreshes when new metadata arrives", async () => {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const load = vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(file);
const show = async (known: boolean) => {
await act(async () => root.render(
Expand All @@ -192,7 +193,11 @@ it("checks history even for a visible reference and refreshes when new metadata
</TranscriptLinksContext.Provider>
</QueryClientProvider>,
));
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); });
await waitForUi(() => {
expect(load).toHaveBeenCalledTimes(known ? 2 : 1);
expect(client.isFetching()).toBe(0);
expect(client.getQueryCache().getAll().at(-1)?.state.status).toBe("success");
});
};
await show(false);
expect(container.querySelector("a")).toBeNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Button } from "./button";
let root: Root;
let container: HTMLDivElement;
beforeEach(() => {
vi.useFakeTimers();
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("PointerEvent", MouseEvent);
vi.stubGlobal(
Expand Down Expand Up @@ -48,17 +49,18 @@ beforeEach(() => {
afterEach(async () => {
await act(async () => root.unmount());
container.remove();
vi.clearAllTimers();
vi.useRealTimers();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
const settle = () => new Promise((resolve) => setTimeout(resolve, 30));
async function key(target: Element, key: string) {
await act(async () => {
target.dispatchEvent(
new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }),
);
await settle();
});
await act(async () => { await vi.runAllTimersAsync(); });
}
it("opens with the keyboard, moves between actions and restores focus on Escape", async () => {
const download = vi.fn();
Expand Down Expand Up @@ -89,8 +91,9 @@ it("opens with the keyboard, moves between actions and restores focus on Escape"
trigger.dispatchEvent(
new MouseEvent("click", { bubbles: true, detail: 1 }),
);
await settle();
});
await act(async () => { await vi.runAllTimersAsync(); });
expect(document.activeElement?.getAttribute("role")).toBe("menu");
await key(document.activeElement!, "ArrowDown");
expect(document.activeElement?.textContent).toBe("Upload replacement");
await key(document.activeElement!, "ArrowDown");
Expand Down
7 changes: 4 additions & 3 deletions platform/web/src/components/universe-appearance-card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { UNIVERSE_ICONS } from "@lightspeed/platform-shared";
import { PermissionIdentityProvider } from "@/lib/permissions";
import { GeneralSettingsPage } from "@/pages/GeneralSettingsPage";
import { UniverseAppearanceCard } from "./universe-appearance-card";
import { waitForUi } from "@/test/wait-for-ui";
import { UniverseIcon } from "./universe-icon";

const mocks = vi.hoisted(() => ({ api: vi.fn(), universe: {} as Universe }));
Expand Down Expand Up @@ -64,7 +65,7 @@ it.each(UNIVERSE_ICONS)("previews the %s icon and saves it to the shared univers
expect(preview.querySelector("span[style]")?.getAttribute("style")).toContain("oklch(0.58 0.11 255)");
expect(mocks.api).not.toHaveBeenCalled();
await click("Save appearance");
await vi.waitFor(() => expect(client.getQueryData<Universe[]>(["universes"])![0]).toMatchObject({ icon, iconColor: "blue" }));
await waitForUi(() => expect(client.getQueryData<Universe[]>(["universes"])![0]).toMatchObject({ icon, iconColor: "blue" }));
expect(mocks.api).toHaveBeenCalledWith("PATCH", "/api/v1/universes/universe", { icon, iconColor: "blue" });
await render(<CachedIcon />);
expect(container.querySelector(`.lucide-${icon}`)).not.toBeNull();
Expand All @@ -75,11 +76,11 @@ it("keeps the saved appearance when a save fails and allows retry", async () =>
mocks.api.mockRejectedValueOnce(new Error("Unable to save"));
await render(<UniverseAppearanceCard universe={mocks.universe} />);
await click("Star icon"); await click("Save appearance");
await vi.waitFor(() => expect(container.querySelector('[role="alert"]')?.textContent).toBe("Unable to save"));
await waitForUi(() => expect(container.querySelector('[role="alert"]')?.textContent).toBe("Unable to save"));
expect(client.getQueryData<Universe[]>(["universes"])![0]).toMatchObject({ icon: "orbit", iconColor: "default" });
expect(button("Star icon").getAttribute("aria-pressed")).toBe("true");
await click("Save appearance");
await vi.waitFor(() => expect(client.getQueryData<Universe[]>(["universes"])![0]).toMatchObject({ icon: "star" }));
await waitForUi(() => expect(client.getQueryData<Universe[]>(["universes"])![0]).toMatchObject({ icon: "star" }));
});

it("restores defaults only after saving", async () => {
Expand Down
Loading
Loading