// @vitest-environment jsdom
// Tests for the NS-656 memory-pressure banner: trigger selection,
// severity precedence, dismissal semantics, and escalation re-opening.
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import type { ReactNode } from "react";
import { I18nProvider } from "@/i18n";
import { MemoryPressureBanner } from "./MemoryPressureBanner";
import type {
StatusResponse,
MemoryPressureStatus,
DiskPressureStatus,
} from "@/lib/api";
let container: HTMLDivElement;
let root: Root;
async function render(ui: ReactNode) {
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
await act(async () => root.render({ui}));
}
async function rerender(ui: ReactNode) {
await act(async () => root.render({ui}));
}
beforeEach(() => {
sessionStorage.clear();
});
afterEach(async () => {
await act(async () => root?.unmount());
container?.remove();
});
function statusWith(memory: MemoryPressureStatus | undefined): StatusResponse {
return { memory } as StatusResponse;
}
function statusWithDisk(
disk: DiskPressureStatus | undefined,
memory?: MemoryPressureStatus,
): StatusResponse {
return { memory, disk } as StatusResponse;
}
function banner(): HTMLElement | null {
return container.querySelector('[data-testid="memory-pressure-banner"]');
}
describe("MemoryPressureBanner", () => {
it("renders nothing for null status or healthy memory", async () => {
await render();
expect(banner()).toBeNull();
await rerender(
,
);
expect(banner()).toBeNull();
// Older gateways: no memory block at all.
await rerender();
expect(banner()).toBeNull();
});
it("renders nothing for unknown pressure (absence of evidence)", async () => {
await render(
,
);
expect(banner()).toBeNull();
});
it("shows the elevated warning", async () => {
await render(
,
);
expect(banner()?.textContent).toContain("running low on memory");
});
it("shows the OOM-restart notice even when current pressure is ok", async () => {
await render(
,
);
expect(banner()?.textContent).toContain(
"restarted unexpectedly, most likely because it ran out of memory",
);
});
it("critical pressure outranks the OOM-restart notice", async () => {
await render(
,
);
expect(banner()?.textContent).toContain("almost out of memory");
});
it("dismissal hides the banner and persists across re-renders", async () => {
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
expect(banner()).toBeNull();
await rerender(
,
);
expect(banner()).toBeNull();
});
it("escalation to critical re-opens a dismissed banner", async () => {
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
expect(banner()).toBeNull();
await rerender(
,
);
expect(banner()?.textContent).toContain("almost out of memory");
});
it("a NEW OOM restart (different boot_id) re-opens a dismissed OOM notice", async () => {
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
expect(banner()).toBeNull();
// Same boot: stays dismissed.
await rerender(
,
);
expect(banner()).toBeNull();
// The gateway OOM-loops and boots again: new incident, banner returns.
await rerender(
,
);
expect(banner()?.textContent).toContain("restarted unexpectedly");
});
it("a gateway reboot (boot_id change) re-opens a dismissed live-pressure banner", async () => {
// Edge case: dismiss critical, gateway restarts, next poll is STILL
// critical with no observed "ok" in between. The new boot is a new
// incident — without boot-scoped keys it would stay hidden (and mask
// the OOM notice too, since critical takes precedence).
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
expect(banner()).toBeNull();
// Same boot, still critical: stays dismissed.
await rerender(
,
);
expect(banner()).toBeNull();
// Rebooted, still critical: new incident, banner returns.
await rerender(
,
);
expect(banner()?.textContent).toContain("almost out of memory");
});
it("recovery to ok resets a dismissed live-pressure banner for the next episode", async () => {
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
expect(banner()).toBeNull();
// Confirmed recovery clears live dismissals...
await rerender(
,
);
expect(banner()).toBeNull();
// ...so the NEXT critical episode surfaces again.
await rerender(
,
);
expect(banner()?.textContent).toContain("almost out of memory");
});
it("unknown pressure (stale heartbeat) does NOT reset live dismissals", async () => {
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
await rerender(
,
);
expect(banner()).toBeNull();
await rerender(
,
);
expect(banner()).toBeNull();
});
it("renders nothing for healthy or unknown disk", async () => {
await render(
,
);
expect(banner()).toBeNull();
await rerender(
,
);
expect(banner()).toBeNull();
});
it("shows the disk-critical warning with free-space detail", async () => {
await render(
,
);
expect(banner()?.textContent).toContain("disk is almost full");
expect(banner()?.textContent).toContain("(120 MB free)");
});
it("shows the disk-elevated warning", async () => {
await render(
,
);
expect(banner()?.textContent).toContain("disk is filling up");
});
it("disk critical outranks memory critical", async () => {
// Imminent data loss beats imminent restart.
await render(
,
);
expect(banner()?.textContent).toContain("disk is almost full");
});
it("memory OOM notice outranks disk elevated", async () => {
await render(
,
);
expect(banner()?.textContent).toContain("restarted unexpectedly");
});
it("dismissing a disk warning does not mask a later memory warning", async () => {
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
expect(banner()).toBeNull();
await rerender(
,
);
expect(banner()?.textContent).toContain("running low on memory");
});
it("disk escalation to critical re-opens a dismissed disk banner", async () => {
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
expect(banner()).toBeNull();
await rerender(
,
);
expect(banner()?.textContent).toContain("disk is almost full");
});
it("disk recovery to ok resets disk dismissals for the next episode", async () => {
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
// User frees space (or grows the disk)...
await rerender(
,
);
expect(banner()).toBeNull();
// ...then the disk fills again: new episode surfaces.
await rerender(
,
);
expect(banner()?.textContent).toContain("disk is almost full");
});
it("disk recovery does NOT reset memory dismissals (and vice versa)", async () => {
// Dismiss a memory warning while disk is also elevated.
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
// Trigger shown is memory critical (outranks disk elevated) — dismiss it.
await act(async () => dismiss.click());
// Disk warning is next in line and has its own key, so it surfaces...
expect(banner()?.textContent).toContain("disk is filling up");
const dismissDisk = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismissDisk.click());
expect(banner()).toBeNull();
// Disk recovers; memory still critical — its dismissal must survive.
await rerender(
,
);
expect(banner()).toBeNull();
});
it("a gateway reboot (boot_id change) re-opens a dismissed disk banner", async () => {
await render(
,
);
const dismiss = container.querySelector(
'[data-testid="memory-pressure-banner"] button',
) as HTMLButtonElement;
await act(async () => dismiss.click());
expect(banner()).toBeNull();
// Restart with the disk still full: new boot, warning returns.
await rerender(
,
);
expect(banner()?.textContent).toContain("disk is almost full");
});
});