File size: 14,372 Bytes
674d56a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
// @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(<I18nProvider>{ui}</I18nProvider>));
}

async function rerender(ui: ReactNode) {
  await act(async () => root.render(<I18nProvider>{ui}</I18nProvider>));
}

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(<MemoryPressureBanner status={null} />);
    expect(banner()).toBeNull();
    await rerender(
      <MemoryPressureBanner status={statusWith({ pressure: "ok" })} />,
    );
    expect(banner()).toBeNull();
    // Older gateways: no memory block at all.
    await rerender(<MemoryPressureBanner status={statusWith(undefined)} />);
    expect(banner()).toBeNull();
  });

  it("renders nothing for unknown pressure (absence of evidence)", async () => {
    await render(
      <MemoryPressureBanner status={statusWith({ pressure: "unknown" })} />,
    );
    expect(banner()).toBeNull();
  });

  it("shows the elevated warning", async () => {
    await render(
      <MemoryPressureBanner status={statusWith({ pressure: "elevated" })} />,
    );
    expect(banner()?.textContent).toContain("running low on memory");
  });

  it("shows the OOM-restart notice even when current pressure is ok", async () => {
    await render(
      <MemoryPressureBanner
        status={statusWith({ pressure: "ok", last_boot_suspected_oom: true })}
      />,
    );
    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(
      <MemoryPressureBanner
        status={statusWith({
          pressure: "critical",
          last_boot_suspected_oom: true,
        })}
      />,
    );
    expect(banner()?.textContent).toContain("almost out of memory");
  });

  it("dismissal hides the banner and persists across re-renders", async () => {
    await render(
      <MemoryPressureBanner status={statusWith({ pressure: "elevated" })} />,
    );
    const dismiss = container.querySelector(
      '[data-testid="memory-pressure-banner"] button',
    ) as HTMLButtonElement;
    await act(async () => dismiss.click());
    expect(banner()).toBeNull();
    await rerender(
      <MemoryPressureBanner status={statusWith({ pressure: "elevated" })} />,
    );
    expect(banner()).toBeNull();
  });

  it("escalation to critical re-opens a dismissed banner", async () => {
    await render(
      <MemoryPressureBanner status={statusWith({ pressure: "elevated" })} />,
    );
    const dismiss = container.querySelector(
      '[data-testid="memory-pressure-banner"] button',
    ) as HTMLButtonElement;
    await act(async () => dismiss.click());
    expect(banner()).toBeNull();
    await rerender(
      <MemoryPressureBanner status={statusWith({ pressure: "critical" })} />,
    );
    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(
      <MemoryPressureBanner
        status={statusWith({
          pressure: "ok",
          last_boot_suspected_oom: true,
          boot_id: "2026-08-13T01:00:00+00:00",
        })}
      />,
    );
    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(
      <MemoryPressureBanner
        status={statusWith({
          pressure: "ok",
          last_boot_suspected_oom: true,
          boot_id: "2026-08-13T01:00:00+00:00",
        })}
      />,
    );
    expect(banner()).toBeNull();
    // The gateway OOM-loops and boots again: new incident, banner returns.
    await rerender(
      <MemoryPressureBanner
        status={statusWith({
          pressure: "ok",
          last_boot_suspected_oom: true,
          boot_id: "2026-08-13T02:00:00+00:00",
        })}
      />,
    );
    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(
      <MemoryPressureBanner
        status={statusWith({
          pressure: "critical",
          boot_id: "2026-08-13T01:00:00+00:00",
        })}
      />,
    );
    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(
      <MemoryPressureBanner
        status={statusWith({
          pressure: "critical",
          boot_id: "2026-08-13T01:00:00+00:00",
        })}
      />,
    );
    expect(banner()).toBeNull();
    // Rebooted, still critical: new incident, banner returns.
    await rerender(
      <MemoryPressureBanner
        status={statusWith({
          pressure: "critical",
          last_boot_suspected_oom: true,
          boot_id: "2026-08-13T02:00:00+00:00",
        })}
      />,
    );
    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(
      <MemoryPressureBanner status={statusWith({ pressure: "critical" })} />,
    );
    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(
      <MemoryPressureBanner status={statusWith({ pressure: "ok" })} />,
    );
    expect(banner()).toBeNull();
    // ...so the NEXT critical episode surfaces again.
    await rerender(
      <MemoryPressureBanner status={statusWith({ pressure: "critical" })} />,
    );
    expect(banner()?.textContent).toContain("almost out of memory");
  });

  it("unknown pressure (stale heartbeat) does NOT reset live dismissals", async () => {
    await render(
      <MemoryPressureBanner status={statusWith({ pressure: "elevated" })} />,
    );
    const dismiss = container.querySelector(
      '[data-testid="memory-pressure-banner"] button',
    ) as HTMLButtonElement;
    await act(async () => dismiss.click());
    await rerender(
      <MemoryPressureBanner status={statusWith({ pressure: "unknown" })} />,
    );
    expect(banner()).toBeNull();
    await rerender(
      <MemoryPressureBanner status={statusWith({ pressure: "elevated" })} />,
    );
    expect(banner()).toBeNull();
  });

  it("renders nothing for healthy or unknown disk", async () => {
    await render(
      <MemoryPressureBanner
        status={statusWithDisk({ pressure: "ok", free_mb: 5000 })}
      />,
    );
    expect(banner()).toBeNull();
    await rerender(
      <MemoryPressureBanner status={statusWithDisk({ pressure: "unknown" })} />,
    );
    expect(banner()).toBeNull();
  });

  it("shows the disk-critical warning with free-space detail", async () => {
    await render(
      <MemoryPressureBanner
        status={statusWithDisk({ pressure: "critical", free_mb: 120 })}
      />,
    );
    expect(banner()?.textContent).toContain("disk is almost full");
    expect(banner()?.textContent).toContain("(120 MB free)");
  });

  it("shows the disk-elevated warning", async () => {
    await render(
      <MemoryPressureBanner
        status={statusWithDisk({ pressure: "elevated", free_mb: 900 })}
      />,
    );
    expect(banner()?.textContent).toContain("disk is filling up");
  });

  it("disk critical outranks memory critical", async () => {
    // Imminent data loss beats imminent restart.
    await render(
      <MemoryPressureBanner
        status={statusWithDisk(
          { pressure: "critical", free_mb: 100 },
          { pressure: "critical" },
        )}
      />,
    );
    expect(banner()?.textContent).toContain("disk is almost full");
  });

  it("memory OOM notice outranks disk elevated", async () => {
    await render(
      <MemoryPressureBanner
        status={statusWithDisk(
          { pressure: "elevated", free_mb: 900 },
          { pressure: "ok", last_boot_suspected_oom: true },
        )}
      />,
    );
    expect(banner()?.textContent).toContain("restarted unexpectedly");
  });

  it("dismissing a disk warning does not mask a later memory warning", async () => {
    await render(
      <MemoryPressureBanner
        status={statusWithDisk({ pressure: "elevated", free_mb: 900 })}
      />,
    );
    const dismiss = container.querySelector(
      '[data-testid="memory-pressure-banner"] button',
    ) as HTMLButtonElement;
    await act(async () => dismiss.click());
    expect(banner()).toBeNull();
    await rerender(
      <MemoryPressureBanner
        status={statusWithDisk(
          { pressure: "elevated", free_mb: 900 },
          { pressure: "elevated" },
        )}
      />,
    );
    expect(banner()?.textContent).toContain("running low on memory");
  });

  it("disk escalation to critical re-opens a dismissed disk banner", async () => {
    await render(
      <MemoryPressureBanner
        status={statusWithDisk({ pressure: "elevated", free_mb: 900 })}
      />,
    );
    const dismiss = container.querySelector(
      '[data-testid="memory-pressure-banner"] button',
    ) as HTMLButtonElement;
    await act(async () => dismiss.click());
    expect(banner()).toBeNull();
    await rerender(
      <MemoryPressureBanner
        status={statusWithDisk({ pressure: "critical", free_mb: 150 })}
      />,
    );
    expect(banner()?.textContent).toContain("disk is almost full");
  });

  it("disk recovery to ok resets disk dismissals for the next episode", async () => {
    await render(
      <MemoryPressureBanner
        status={statusWithDisk({ pressure: "critical", free_mb: 150 })}
      />,
    );
    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(
      <MemoryPressureBanner
        status={statusWithDisk({ pressure: "ok", free_mb: 8000 })}
      />,
    );
    expect(banner()).toBeNull();
    // ...then the disk fills again: new episode surfaces.
    await rerender(
      <MemoryPressureBanner
        status={statusWithDisk({ pressure: "critical", free_mb: 150 })}
      />,
    );
    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(
      <MemoryPressureBanner
        status={statusWithDisk(
          { pressure: "elevated", free_mb: 900 },
          { pressure: "critical" },
        )}
      />,
    );
    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(
      <MemoryPressureBanner
        status={statusWithDisk(
          { pressure: "ok", free_mb: 8000 },
          { pressure: "critical" },
        )}
      />,
    );
    expect(banner()).toBeNull();
  });

  it("a gateway reboot (boot_id change) re-opens a dismissed disk banner", async () => {
    await render(
      <MemoryPressureBanner
        status={statusWithDisk(
          { pressure: "critical", free_mb: 150 },
          { pressure: "ok", boot_id: "2026-08-13T01:00:00+00:00" },
        )}
      />,
    );
    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(
      <MemoryPressureBanner
        status={statusWithDisk(
          { pressure: "critical", free_mb: 150 },
          { pressure: "ok", boot_id: "2026-08-13T02:00:00+00:00" },
        )}
      />,
    );
    expect(banner()?.textContent).toContain("disk is almost full");
  });
});