Refactor live.js into pure-function bridge + goja test harness #19

Open
opened 2026-05-16 22:23:05 +02:00 by arne · 1 comment
Owner

What to build

Implement ADR 0001. Restructure internal/web/static/live.js so its decision logic lives in pure functions that return data verdicts, with a thin DOM-touching shell around them. Add github.com/dop251/goja as a Go test dependency. Add a live_test.go (location TBD: internal/web/ or a dedicated internal/web/static_test/) that loads live.js into goja and exercises the pure functions.

Refactor shape:

  • Pure: peerFromPath(pathname) → string, decideThreadUpdated(eventData, currentPath) → {kind, ...}, plus whatever decision points the active-peer migration left behind.
  • Shell: addEventListener calls, dispatchEvent, EventSource lifecycle.
  • Expose the pure functions on window.__liveUpdates (extending the existing pattern at live.js:228) so the goja test can call them directly.

Initial test coverage (matching CONTEXT.md § Live updates protocol):

  • peerFromPath: /c/<encoded> round-trips correctly; non-thread paths return ""; bad-encoding paths return "".
  • decideThreadUpdated: peer-match → {kind: "dispatch"}; peer-mismatch → {kind: "skip-peer-mismatch"}; no thread open → {kind: "skip-no-thread"}; malformed JSON → {kind: "skip-malformed"}.

Acceptance criteria

  • go test ./internal/web/... runs the goja-based JS tests
  • All pure decision functions in live.js are exposed on window.__liveUpdates
  • DOM-touching code (addEventListener, dispatchEvent, EventSource lifecycle) is grouped in a single named shell function so the boundary is explicit
  • Existing manual behavior unchanged (peer-filter still works in a real browser)
  • Test cases for at least: peerFromPath round-trip, decideThreadUpdated peer-match, decideThreadUpdated peer-mismatch

Blocked by

  • #16 (active-peer migration) — so the refactor target is the post-migration live.js, not the soon-to-be-deleted tab-ID code
## What to build Implement [ADR 0001](docs/adr/0001-live-update-test-seam.md). Restructure `internal/web/static/live.js` so its decision logic lives in pure functions that return data verdicts, with a thin DOM-touching shell around them. Add `github.com/dop251/goja` as a Go test dependency. Add a `live_test.go` (location TBD: `internal/web/` or a dedicated `internal/web/static_test/`) that loads `live.js` into goja and exercises the pure functions. **Refactor shape:** - **Pure**: `peerFromPath(pathname) → string`, `decideThreadUpdated(eventData, currentPath) → {kind, ...}`, plus whatever decision points the active-peer migration left behind. - **Shell**: `addEventListener` calls, `dispatchEvent`, `EventSource` lifecycle. - Expose the pure functions on `window.__liveUpdates` (extending the existing pattern at `live.js:228`) so the goja test can call them directly. **Initial test coverage** (matching CONTEXT.md § Live updates protocol): - `peerFromPath`: `/c/<encoded>` round-trips correctly; non-thread paths return `""`; bad-encoding paths return `""`. - `decideThreadUpdated`: peer-match → `{kind: "dispatch"}`; peer-mismatch → `{kind: "skip-peer-mismatch"}`; no thread open → `{kind: "skip-no-thread"}`; malformed JSON → `{kind: "skip-malformed"}`. ## Acceptance criteria - [ ] `go test ./internal/web/...` runs the goja-based JS tests - [ ] All pure decision functions in `live.js` are exposed on `window.__liveUpdates` - [ ] DOM-touching code (`addEventListener`, `dispatchEvent`, `EventSource` lifecycle) is grouped in a single named shell function so the boundary is explicit - [ ] Existing manual behavior unchanged (peer-filter still works in a real browser) - [ ] Test cases for at least: `peerFromPath` round-trip, `decideThreadUpdated` peer-match, `decideThreadUpdated` peer-mismatch ## Blocked by - #16 (active-peer migration) — so the refactor target is the post-migration live.js, not the soon-to-be-deleted tab-ID code
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Restructure the live-updates bridge script so its decision logic lives in pure functions returning data verdicts, separate from a thin DOM-mutating shell. Add a goja-based Go test that exercises the pure functions. Establishes the regression-test seam captured in ADR 0001.

Current behavior:
The bridge script (internal/web/static/live.js) intermixes pure decisions (e.g. parsing the peer URL from location.pathname, deciding whether to react to a thread-updated SSE frame given the open peer) with DOM side effects (addEventListener on the EventSource, dispatchEvent on #main, EventSource lifecycle, htmx sseOpen interop). There is no automated regression test for the bridge — only the pre-existing window.__liveUpdates = { peerFromPath } hook, which proves the pattern is viable but is currently used only by a small manual assertion.

Desired behavior:

Decision logic in the bridge becomes pure functions. Each function takes plain values (the event payload, the current path, etc.) and returns a plain-data verdict object describing what the side-effect shell should do (e.g. {kind: "dispatch"}, {kind: "skip-peer-mismatch"}, {kind: "skip-no-thread"}, {kind: "skip-malformed"}). The shell — the DOM-touching code that applies verdicts — is a single named function. Pure functions are exposed on window.__liveUpdates so a Go test using goja can call them with controlled inputs and assert returned verdicts.

A new Go test (in the web package, in a file colocated with the other web tests) loads the bridge script into a goja runtime and exercises the pure decision points. The test does not polyfill the DOM, does not need a browser, does not need a fake posta-server. It runs in go test.

Initial test coverage (matching the protocol table in CONTEXT.md § Live updates protocol):

  • peerFromPath:
    • thread paths like /c/<URL-encoded peer> round-trip to the decoded peer
    • non-thread paths return the empty string
    • bad-encoding paths return the empty string without throwing
  • The thread-updated decision function:
    • well-formed payload + matching current peer → {kind: "dispatch"}
    • well-formed payload + non-matching peer → {kind: "skip-peer-mismatch"}
    • well-formed payload + no thread open → {kind: "skip-no-thread"}
    • malformed JSON → {kind: "skip-malformed"}

Future bridge tests follow the same pattern: extend window.__liveUpdates with the new pure function, add Go test cases.

Key principles for the agent:

  • The exposure surface on window.__liveUpdates is a deliberate contract, not a leak. It exists to make the bridge testable from outside a browser.
  • Pure functions must not touch the DOM, the network, the EventSource, or any other global side-effect target. Take inputs, return verdicts.
  • The shell function is the only place addEventListener, dispatchEvent, fetch, and EventSource lifecycle live.
  • Behavior must not change. This is a restructure, not a redesign. The browser-visible outcome of any sequence of events should be identical before and after the refactor.

Acceptance criteria:

  • go test ./internal/web/... runs the new bridge tests via goja
  • All bridge decision logic is callable as pure functions from window.__liveUpdates
  • Side-effect code (DOM event listeners, dispatches, EventSource lifecycle) is grouped in a single named function so the boundary is obvious to a reader
  • Test cases cover at minimum: peerFromPath round-trip plus both non-thread and bad-encoding paths; the thread-updated decision in all four outcomes listed above
  • Manual verification: a real browser session still receives inbound peer messages with no regression (assuming #15 has landed; otherwise this refactor lands on top of the same bug, doesn't make it worse, and doesn't fix it)
  • goja is added as a go.mod dependency only; no npm artifact appears in the repo

Out of scope:

  • Adding new bridge behavior (verdicts beyond the contract above)
  • A browser-driven test harness (deferred to a later ADR)
  • Touching the scroll preservation contract (separate, follow-on issue)
  • Restructuring the htmx-sse extension or the way htmx is wired
  • Any change to the wire protocol
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Restructure the live-updates bridge script so its decision logic lives in pure functions returning data verdicts, separate from a thin DOM-mutating shell. Add a `goja`-based Go test that exercises the pure functions. Establishes the regression-test seam captured in ADR 0001. **Current behavior:** The bridge script (`internal/web/static/live.js`) intermixes pure decisions (e.g. parsing the peer URL from `location.pathname`, deciding whether to react to a `thread-updated` SSE frame given the open peer) with DOM side effects (`addEventListener` on the `EventSource`, `dispatchEvent` on `#main`, `EventSource` lifecycle, htmx `sseOpen` interop). There is no automated regression test for the bridge — only the pre-existing `window.__liveUpdates = { peerFromPath }` hook, which proves the pattern is viable but is currently used only by a small manual assertion. **Desired behavior:** Decision logic in the bridge becomes pure functions. Each function takes plain values (the event payload, the current path, etc.) and returns a plain-data verdict object describing what the side-effect shell should do (e.g. `{kind: "dispatch"}`, `{kind: "skip-peer-mismatch"}`, `{kind: "skip-no-thread"}`, `{kind: "skip-malformed"}`). The shell — the DOM-touching code that applies verdicts — is a single named function. Pure functions are exposed on `window.__liveUpdates` so a Go test using `goja` can call them with controlled inputs and assert returned verdicts. A new Go test (in the `web` package, in a file colocated with the other web tests) loads the bridge script into a `goja` runtime and exercises the pure decision points. The test does not polyfill the DOM, does not need a browser, does not need a fake posta-server. It runs in `go test`. **Initial test coverage** (matching the protocol table in `CONTEXT.md § Live updates protocol`): - `peerFromPath`: - thread paths like `/c/<URL-encoded peer>` round-trip to the decoded peer - non-thread paths return the empty string - bad-encoding paths return the empty string without throwing - The `thread-updated` decision function: - well-formed payload + matching current peer → `{kind: "dispatch"}` - well-formed payload + non-matching peer → `{kind: "skip-peer-mismatch"}` - well-formed payload + no thread open → `{kind: "skip-no-thread"}` - malformed JSON → `{kind: "skip-malformed"}` Future bridge tests follow the same pattern: extend `window.__liveUpdates` with the new pure function, add Go test cases. **Key principles for the agent:** - The exposure surface on `window.__liveUpdates` is a deliberate contract, not a leak. It exists to make the bridge testable from outside a browser. - Pure functions must not touch the DOM, the network, the EventSource, or any other global side-effect target. Take inputs, return verdicts. - The shell function is the only place `addEventListener`, `dispatchEvent`, `fetch`, and `EventSource` lifecycle live. - Behavior must not change. This is a restructure, not a redesign. The browser-visible outcome of any sequence of events should be identical before and after the refactor. **Acceptance criteria:** - [ ] `go test ./internal/web/...` runs the new bridge tests via `goja` - [ ] All bridge decision logic is callable as pure functions from `window.__liveUpdates` - [ ] Side-effect code (DOM event listeners, dispatches, EventSource lifecycle) is grouped in a single named function so the boundary is obvious to a reader - [ ] Test cases cover at minimum: `peerFromPath` round-trip plus both non-thread and bad-encoding paths; the `thread-updated` decision in all four outcomes listed above - [ ] Manual verification: a real browser session still receives inbound peer messages with no regression (assuming #15 has landed; otherwise this refactor lands on top of the same bug, doesn't make it worse, and doesn't fix it) - [ ] `goja` is added as a `go.mod` dependency only; no npm artifact appears in the repo **Out of scope:** - Adding new bridge behavior (verdicts beyond the contract above) - A browser-driven test harness (deferred to a later ADR) - Touching the scroll preservation contract (separate, follow-on issue) - Restructuring the htmx-sse extension or the way htmx is wired - Any change to the wire protocol
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
posta/chat#19
No description provided.