import test from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import type { JobRecord } from "../jobs/registry"; import type { ObserveInputs } from "./inputs"; import { computePulse } from "./pulse"; // THE TOKEN'S BYTES ARE THE CONTRACT, so most of what follows asserts the // HASHED STRING rather than the hash: `digest` is injected, so a test can read // exactly what would be hashed. A reordered part or a changed separator makes // every open tab re-render once for nothing, and a hash literal would only say // "different" without saying how. const echo = (s: string) => s; const sha1 = (s: string) => createHash("sha1").update(s).digest("base64url"); const job = (over: Partial & { id: string }): JobRecord => ({ kind: "sync", queueKey: "q", status: "running", queuedAt: 0, logPath: "/dev/null", ...over, }) as JobRecord; function observe(over: Partial = {}): ObserveInputs { return { registry: null, scheduler: null, pool: null, snapshotGeneration: 0, settingsMtime: 0, changelogMtime: 0, digest: echo, ...over, }; } const registryOf = (jobs: JobRecord[]) => ({ list: () => jobs, get: (id: string) => jobs.find((j) => j.id === id), }); test("nothing constructed yet is a valid, idle answer", () => { // Every singleton is nullable because the observer reads the globals directly // and "not created yet" means "nothing to report". A pulse must be able to say // the system is idle without making a system to ask. const out = computePulse(observe()); assert.equal(out.activeJobs, 0); assert.equal(out.runningJobs, 0); assert.equal(out.busy, false); // Three parts, in this order, and nothing else. assert.equal(out.rev, "snap:0|s:0|c:0"); // Deterministic: the same nothing hashes to the same token. assert.equal(computePulse(observe()).rev, out.rev); }); test("queued work is active but not running", () => { const out = computePulse( observe({ registry: registryOf([ job({ id: "a", status: "running" }), job({ id: "b", status: "queued" }), job({ id: "c", status: "done" }), ]), }), ); assert.equal(out.activeJobs, 2); assert.equal(out.runningJobs, 1); assert.equal(out.busy, true); // A finished job is still in the token — its status moved, and that is a // change worth re-rendering for. assert.equal( out.rev, "a:running:::::0|b:queued:::::0|c:done:::::0|snap:0|s:0|c:0", ); }); test("the token moves with a job's progress", () => { const at = (current: number) => computePulse( observe({ digest: sha1, registry: registryOf([ job({ id: "a", progress: { metric: "downloads", initial: 0, current, target: 100 } }), ]), }), ).rev; assert.notEqual(at(1), at(2)); assert.equal(at(7), at(7)); // And the shape of that part is `initial/current/target`. assert.match( computePulse( observe({ registry: registryOf([ job({ id: "a", progress: { metric: "downloads", initial: 0, current: 7, target: 100 } }), ]), }), ).rev, /^a:running::::0\/7\/100:0\|/, ); }); test("the token moves with the snapshot generation", () => { // A report regenerated on the debounce lands after the job that triggered it // has already finished, so without this counter the pages whose counts come // from snapshots would stay stale until something unrelated moved. const rev = (snapshotGeneration: number) => computePulse(observe({ digest: sha1, snapshotGeneration })).rev; assert.notEqual(rev(4), rev(5)); assert.equal(rev(5), rev(5)); }); test("the token moves with either watched file's mtime", () => { const base = computePulse(observe({ digest: sha1 })).rev; assert.notEqual(base, computePulse(observe({ digest: sha1, settingsMtime: 1 })).rev); assert.notEqual(base, computePulse(observe({ digest: sha1, changelogMtime: 1 })).rev); }); test("the queue shape and the pool are in the token, in that order", () => { const out = computePulse( observe({ registry: registryOf([job({ id: "a", status: "running" })]), scheduler: { queues: () => [{ name: "sync", running: ["a"], queued: ["b", "c"] }], } as unknown as ObserveInputs["scheduler"], pool: { summary: () => [{ id: "w1", busy: true }, { id: "w2", busy: false }], isPaused: () => true, canStopPartial: () => false, } as unknown as ObserveInputs["pool"], }), ); assert.equal( out.rev, "a:running:::::0|q:sync:a:b,c|w:paused:2|w:w1:1|w:w2:0|snap:0|s:0|c:0", ); }); test("identical inputs hash to an identical token", () => { const make = () => observe({ digest: sha1, registry: registryOf([ job({ id: "a", startedAt: 5, tasks: [{}, {}] as JobRecord["tasks"] }), ]), snapshotGeneration: 3, settingsMtime: 111, changelogMtime: 222, }); assert.equal(computePulse(make()).rev, computePulse(make()).rev); });