fix(ci): prevent OOM in web coverage by splitting tests and raising heap limit

useSessionEvents tests OOM the vitest worker when coverage instrumentation
is enabled (4GB default isn't enough). Split the test file into SSE and mux
suites so the worker can GC between files, and raise the heap to 6GB for
the coverage job.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gaurav Bhola 2026-04-06 07:04:46 -07:00
parent 6f3aec703e
commit 047880435c
2 changed files with 162 additions and 1 deletions

View File

@ -73,7 +73,7 @@ jobs:
- name: Web tests with coverage
if: needs.detect-changes.outputs.web == 'true'
run: pnpm --filter @composio/ao-web exec vitest run --coverage
run: NODE_OPTIONS="--max-old-space-size=6144" pnpm --filter @composio/ao-web exec vitest run --coverage
- name: Prepare and merge lcov reports
run: |

View File

@ -0,0 +1,161 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useSessionEvents } from "../useSessionEvents";
import { makeSession } from "../../__tests__/helpers";
describe("useSessionEvents mux session updates", () => {
beforeEach(() => {
const eventSourceConstructor = vi.fn(() => ({
onmessage: null,
onopen: null,
onerror: null,
readyState: 0,
close: vi.fn(),
}));
global.EventSource = Object.assign(eventSourceConstructor, {
CONNECTING: 0,
OPEN: 1,
CLOSED: 2,
}) as unknown as typeof EventSource;
global.fetch = vi.fn();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
const makePatch = (id: string) => ({
id,
status: "working",
activity: "active" as string | null,
attentionLevel: "none",
lastActivityAt: new Date().toISOString(),
});
const makeSessions = (ids: string[]) => ids.map((id) => makeSession({ id }));
it("sets connectionStatus to connected when muxSessions is provided", () => {
const patches = [makePatch("s1")];
const { result } = renderHook(() =>
useSessionEvents(makeSessions(["s1"]), undefined, patches),
);
expect(result.current.connectionStatus).toBe("connected");
});
it("dispatches snapshot from muxSessions", () => {
const sessions = makeSessions(["s1", "s2"]);
const patches = [
{ ...makePatch("s1"), status: "working", activity: "active" },
{ ...makePatch("s2"), status: "pr_open", activity: "idle" },
];
const { result } = renderHook(() =>
useSessionEvents(sessions, undefined, patches),
);
expect(result.current.sessions[0].status).toBe("working");
expect(result.current.sessions[1].status).toBe("pr_open");
});
it("triggers scheduleRefresh when mux session membership changes", async () => {
vi.useFakeTimers();
const sessions = makeSessions(["s1", "s2"]);
const initialPatches = [makePatch("s1"), makePatch("s2")];
const newPatches = [makePatch("s1")];
let currentPatches = initialPatches;
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({
sessions: [makeSession({ id: "s1" })],
}),
} as unknown as Response);
const { rerender } = renderHook(() =>
useSessionEvents(sessions, undefined, currentPatches),
);
currentPatches = newPatches;
rerender();
act(() => vi.advanceTimersByTime(200));
await waitFor(() => vi.mocked(fetch).mock.calls.length > 0);
vi.useRealTimers();
});
it("aborts in-flight request when muxSessions changes rapidly", async () => {
vi.useFakeTimers();
const controller = new AbortController();
const sessions = makeSessions(["s1", "s2", "s3"]);
let resolveFirst!: () => void;
const firstFetch = new Promise<Response>((resolve) => {
resolveFirst = () =>
resolve({
ok: true,
json: async () => ({ sessions: [] }),
} as unknown as Response);
});
vi.mocked(fetch)
.mockReturnValueOnce(firstFetch)
.mockResolvedValue({
ok: true,
json: async () => ({ sessions: [] }),
} as unknown as Response);
let patches = [makePatch("s1")];
const { rerender } = renderHook(() =>
useSessionEvents(sessions, undefined, patches),
);
patches = [makePatch("s1"), makePatch("s2")];
rerender();
act(() => vi.advanceTimersByTime(200));
patches = [makePatch("s1"), makePatch("s2"), makePatch("s3")];
rerender();
resolveFirst();
vi.useRealTimers();
expect(controller.signal).toBeDefined();
});
it("skips SSE setup when muxActive is true", () => {
const patches = [makePatch("s1")];
const EventSourceSpy = vi.fn(() => ({
close: vi.fn(),
onmessage: null,
onerror: null,
onopen: null,
readyState: 1,
}));
global.EventSource = Object.assign(EventSourceSpy, {
CONNECTING: 0, OPEN: 1, CLOSED: 2,
}) as unknown as typeof EventSource;
renderHook(() => useSessionEvents([], undefined, patches));
expect(EventSourceSpy).not.toHaveBeenCalled();
});
it("mux-active cleanup aborts in-flight fetch and clears timer", async () => {
vi.useFakeTimers();
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ sessions: [] }),
} as unknown as Response);
const sessions = makeSessions(["s1", "s2"]);
const patches = [makePatch("s1")];
let mux: typeof patches | undefined = patches;
const { rerender, unmount } = renderHook(() =>
useSessionEvents(sessions, undefined, mux),
);
mux = [makePatch("s1"), makePatch("s2")];
rerender();
unmount();
act(() => vi.advanceTimersByTime(500));
vi.useRealTimers();
});
});