diff --git a/sidecar/attune_gui/static_cw/living-docs-poller.js b/sidecar/attune_gui/static_cw/living-docs-poller.js new file mode 100644 index 0000000..c76dcae --- /dev/null +++ b/sidecar/attune_gui/static_cw/living-docs-poller.js @@ -0,0 +1,26 @@ +// Pure logic for the Living Docs row poller. +// +// DOM-free and timer-free on purpose: the recursive-setTimeout loop, +// visibilitychange handling, fetch, and DOM rendering stay in the inline +// shim in living_docs.html. This module holds only the decision the +// poller branches on — extracted so it can be unit-tested (Vitest, Node). +// +// See specs/living-docs-poller-testing. + +/** Poll cadence (ms) the shim reschedules on while rows are regenerating. */ +export const POLL_INTERVAL_MS = 1500; + +/** + * Keep polling iff at least one row is still regenerating. + * + * Input is the `rows` array from GET /api/living-docs/rows (objects with + * a `computed_state`). Defensive against a non-array payload or rows + * missing `computed_state` so a malformed response can't throw inside the + * poll loop. + */ +export function shouldKeepPolling(rows) { + return ( + Array.isArray(rows) && + rows.some((r) => r && r.computed_state === "regenerating") + ); +} diff --git a/sidecar/attune_gui/static_cw/living-docs-poller.test.js b/sidecar/attune_gui/static_cw/living-docs-poller.test.js new file mode 100644 index 0000000..70181f2 --- /dev/null +++ b/sidecar/attune_gui/static_cw/living-docs-poller.test.js @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; + +import { POLL_INTERVAL_MS, shouldKeepPolling } from "./living-docs-poller.js"; + +const row = (computed_state) => ({ id: "f/concept", computed_state }); + +describe("shouldKeepPolling", () => { + it("is true when any row is regenerating", () => { + expect(shouldKeepPolling([row("current"), row("regenerating"), row("stale")])).toBe(true); + }); + + it("is false when no row is regenerating", () => { + const rows = ["current", "stale", "missing", "pending-review", "errored"].map(row); + expect(shouldKeepPolling(rows)).toBe(false); + }); + + it("is false for an empty list", () => { + expect(shouldKeepPolling([])).toBe(false); + }); + + it("does not throw and is false for non-array input", () => { + expect(shouldKeepPolling(undefined)).toBe(false); + expect(shouldKeepPolling(null)).toBe(false); + expect(shouldKeepPolling({})).toBe(false); + }); + + it("does not throw on rows missing computed_state", () => { + expect(shouldKeepPolling([{}, null, { computed_state: undefined }])).toBe(false); + }); +}); + +describe("POLL_INTERVAL_MS", () => { + it("pins the poll cadence the shim relies on", () => { + expect(POLL_INTERVAL_MS).toBe(1500); + }); +}); diff --git a/sidecar/attune_gui/templates/living_docs.html b/sidecar/attune_gui/templates/living_docs.html index abbfe89..75310fb 100644 --- a/sidecar/attune_gui/templates/living_docs.html +++ b/sidecar/attune_gui/templates/living_docs.html @@ -168,7 +168,12 @@