// @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"); }); });