| 1 | import assert from "node:assert/strict"; |
| 2 | import type { ProcessMetric } from "electron"; |
| 3 | import { test } from "node:test"; |
| 4 | import { ProcessDiagnostics } from "./processDiagnostics.js"; |
| 5 | |
| 6 | test("samples are bounded, redact metadata and preserve unavailable fields", () => { |
| 7 | let now = 0; |
| 8 | let calls = 0; |
| 9 | const sampler = new ProcessDiagnostics(() => { |
| 10 | calls++; |
| 11 | return [{ pid: 42, creationTime: 1, type: "Tab", name: "private title", cpu: { percentCPUUsage: 2 }, memory: { workingSetSize: 2048 } }] as ProcessMetric[]; |
| 12 | }, () => now); |
| 13 | const first = sampler.snapshot(); |
| 14 | assert.deepEqual(first.samples[0].processes, [{ pid: 42, creationTime: 1, type: "Tab", cpuPercent: null, workingSetMb: 2, privateMb: null }]); |
| 15 | sampler.snapshot(); |
| 16 | assert.equal(calls, 1); |
| 17 | for (let i = 1; i <= 20; i++) { now = i * 30_000; sampler.sample(); } |
| 18 | const snapshot = sampler.snapshot(); |
| 19 | assert.ok(snapshot.samples.length <= 12); |
| 20 | assert.ok(snapshot.samples.every((sample) => sample.ageMs <= 300_000)); |
| 21 | assert.equal(snapshot.samples.at(-1)?.intervalMs, 30_000); |
| 22 | assert.equal(snapshot.samples.at(-1)?.processes[0].cpuPercent, 2); |
| 23 | assert.equal(JSON.stringify(snapshot).includes("private title"), false); |
| 24 | }); |
| 25 | |
| 26 | test("unavailable and expired samples do not become zero readings", () => { |
| 27 | let now = 0; |
| 28 | let fail = false; |
| 29 | const sampler = new ProcessDiagnostics(() => { if (fail) throw Error("gone"); return []; }, () => now); |
| 30 | sampler.sample(); |
| 31 | fail = true; |
| 32 | now = 301000; |
| 33 | assert.deepEqual(sampler.snapshot().samples, []); |
| 34 | }); |
| 35 | |
| 36 | test("foreground samples at most twice per minute and background once per minute", () => { |
| 37 | let now = 0; |
| 38 | let foreground = true; |
| 39 | let calls = 0; |
| 40 | const sampler = new ProcessDiagnostics(() => { calls++; return []; }, () => now, () => foreground); |
| 41 | sampler.sample(); |
| 42 | now = 29_999; sampler.snapshot(); |
| 43 | assert.equal(calls, 1); |
| 44 | now = 30_000; sampler.sample(); |
| 45 | assert.equal(calls, 2); |
| 46 | foreground = false; |
| 47 | now = 60_000; sampler.sample(); |
| 48 | assert.equal(calls, 2); |
| 49 | now = 90_000; sampler.sample(); |
| 50 | assert.equal(calls, 3); |
| 51 | }); |
| 52 | |
| 53 | test("growth requires sustained readings, uses private memory, and rejects PID reuse", () => { |
| 54 | let now = 0; |
| 55 | let memory = 100; |
| 56 | let creationTime = 1; |
| 57 | const sampler = new ProcessDiagnostics(() => [{ pid: 42, creationTime, type: "Tab", cpu: { percentCPUUsage: 0 }, memory: { workingSetSize: 900 * 1024, privateBytes: memory * 1024 } }] as ProcessMetric[], () => now); |
| 58 | sampler.sample(); |
| 59 | now = 30_000; sampler.sample(); |
| 60 | memory = 400; |
| 61 | now = 60_000; sampler.sample(); |
| 62 | assert.equal(sampler.snapshot().growth.length, 0); |
| 63 | now = 90_000; sampler.sample(); |
| 64 | now = 120_000; |
| 65 | assert.deepEqual(sampler.snapshot().growth, [{ pid: 42, type: "Tab", metric: "private", baselineMb: 100, currentMb: 400, durationMs: 120_000 }]); |
| 66 | creationTime = 2; |
| 67 | now = 150_000; |
| 68 | assert.equal(sampler.snapshot().growth.length, 0); |
| 69 | assert.equal(sampler.snapshot().samples.at(-1)?.processes[0].cpuPercent, null); |
| 70 | }); |
| 71 |