store.test.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import assert from "node:assert/strict";
  2. import test from "node:test";
  3. import { MonitorStore } from "../src/store.js";
  4. import { normalizeThread } from "../src/domain.js";
  5. import { rawThread } from "./fake-app-server.js";
  6. function summary(id: string, status = "notLoaded") {
  7. return normalizeThread(rawThread(id, { status: { type: status } }));
  8. }
  9. test("snapshot replacement is atomic and advances revision only for changed inventory", () => {
  10. const store = new MonitorStore();
  11. store.replaceSnapshot([summary("a")], new Set(["a"]));
  12. const firstRevision = store.revision;
  13. assert.deepEqual(store.snapshot().threads.map((thread) => thread.id), ["a"]);
  14. store.replaceSnapshot([summary("b")], new Set(["b"]));
  15. assert.deepEqual(store.snapshot().threads.map((thread) => thread.id), ["b"]);
  16. assert.equal(store.revision, firstRevision + 1);
  17. store.replaceSnapshot([summary("b")], new Set(["b"]));
  18. assert.equal(store.revision, firstRevision + 1);
  19. });
  20. test("status events update known and newly observed threads", () => {
  21. const store = new MonitorStore();
  22. store.replaceSnapshot([summary("known")], new Set());
  23. store.updateStatus("known", { type: "active", activeFlags: ["waitingOnApproval"] });
  24. store.updateStatus("new", { type: "idle", activeFlags: [] });
  25. const byId = new Map(store.snapshot().threads.map((thread) => [thread.id, thread]));
  26. assert.equal(byId.get("known")?.status, "active");
  27. assert.deepEqual(byId.get("known")?.activeFlags, ["waitingOnApproval"]);
  28. assert.equal(byId.get("new")?.status, "idle");
  29. });
  30. test("change ring stays bounded and reports lost history", () => {
  31. const store = new MonitorStore({ changeRingSize: 2 });
  32. store.setConnection("connecting");
  33. store.setConnection("disconnected", "one");
  34. store.setConnection("connecting");
  35. const result = store.changesSince(0, 10);
  36. assert.equal(result.changes.length, 2);
  37. assert.equal(result.truncated, true);
  38. });
  39. test("connection loss retains cached snapshot and marks it stale", () => {
  40. const store = new MonitorStore();
  41. store.replaceSnapshot([summary("a")], new Set());
  42. store.setConnection("disconnected", "socket closed");
  43. const snapshot = store.snapshot();
  44. assert.equal(snapshot.freshness, "stale");
  45. assert.equal(snapshot.threads.length, 1);
  46. assert.equal(snapshot.connectionError, "socket closed");
  47. });
  48. test("waiting observes changes and normal timeouts", async () => {
  49. const store = new MonitorStore();
  50. const wait = store.waitForChange(store.revision, 100);
  51. store.setConnection("connecting");
  52. const changed = await wait;
  53. assert.equal(changed.timedOut, false);
  54. assert.equal(changed.changes[0]?.kind, "connection");
  55. const timedOut = await store.waitForChange(store.revision, 5);
  56. assert.equal(timedOut.timedOut, true);
  57. });
  58. test("concurrent waiters are all released and shutdown cancels remaining waits", async () => {
  59. const store = new MonitorStore();
  60. const first = store.waitForChange(0, 100);
  61. const second = store.waitForChange(0, 100);
  62. store.setConnection("connecting");
  63. assert.deepEqual((await Promise.all([first, second])).map((result) => result.revision), [1, 1]);
  64. const pending = store.waitForChange(store.revision, 10_000);
  65. store.shutdown();
  66. assert.equal((await pending).cancelled, true);
  67. });