test(e2e): V14 Tier-2 chromedp — topic ACL error envelope + WS keep-open (livetemplate#415) — DRAFT, Phase-5-resolved#327
test(e2e): V14 Tier-2 chromedp — topic ACL error envelope + WS keep-open (livetemplate#415) — DRAFT, Phase-5-resolved#327adnaan wants to merge 5 commits into
Conversation
…open (livetemplate#415)
Phase 4 of livetemplate's broadcast-action-redesign (#415). Adds the
Tier-2 user-visible leg of V14 -- end-to-end in a real browser against a
real livetemplate handler -- and the cross-repo go.mod replace required
to build it against unreleased livetemplate Phase-0..4.
## V14 (Tier-2 user-visible leg)
e2e/topic_acl_error_envelope_v14_test.go (new, //go:build browser):
verifies that an ACL-denied ctx.Subscribe in the WS-connect Mount surfaces
in a real browser as an `lvt:error` CustomEvent { code, topic } on the
wrapper AND leaves the WebSocket OPEN AND FUNCTIONAL (the advisor
sharpening: stays *functional*, not merely un-closed -- the test sends a
click action over the same socket post-envelope and asserts the rendered
value updates).
Three load-bearing harness decisions, all commented in the file:
1. Full 4-artifact capture (CLAUDE.md global rule, surfaced on failure):
* browser console -- shared installConsoleLogger from
lifecycle_ergonomics_test.go (same package_e2e_test, streamed via
t.Logf live).
* server logs -- e2etest.NewServerLogger() teed into the global slog
default with save+restore in t.Cleanup (slog.SetDefault is
process-global; preserves the default for sibling tests).
* WS frames -- e2etest.RecordWSFrames (lvt#317), surfaced via
wsLog.PrintLast.
* rendered HTML -- chromedp.OuterHTML("html") in a `dump()` closure
called BEFORE t.Fatalf (chrome ctx is cancelled by defer cleanup()
before t.Cleanup runs; HTML must be captured at failure-time).
The first failed run during development dumped server logs + WS
frames + HTML and surfaced the missing IsInitialMount guard
(Deviation 3 below) -- proof the harness works as intended.
2. Capture-phase document listener for the non-bubbling event. The
client dispatches `lvt:error` on the wrapper with bubbles:false.
Installing the listener via `document.addEventListener('lvt:error',
..., true)` in a <head> script BEFORE client.js loads catches the
wrapper dispatch via DOM capture phase (capture traverses
window->target regardless of bubbles). Race-free without
instrumenting the client.
3. Local Phase-4 client-bundle handler, NOT e2etest.ServeClientLibrary.
The canonical helper fetches @livetemplate/client from the unpkg CDN
and caches on disk for 1h; that cache can shadow LVT_CLIENT_CDN_URL,
so an unreleased client cannot be reliably served via the canonical
path. Reverts to ServeClientLibrary in Phase 5/6 after npm publish.
## IsInitialMount guard (Deviation 3 -- the e2e exposed a Mount-path
dual-fire reality the Tier-1 test does not exercise)
Mount runs on every HTTP request AND WebSocket connect (livetemplate
CLAUDE.md). On the initial HTTP GET (page render), a denied
ctx.Subscribe surfaces as HTTP 500 (mount.go HTTP-GET path; phase-1.md
pre-existing behavior, unchanged by Phase 4). Without the guard, the
page would 500 before client.js ever loaded and there would be no WS
for V14 to exercise. The controller guards with `if ctx.IsInitialMount()
{ return s, nil }` to scope the denial to the WS-connect Mount --
exactly V14's spec scenario.
## Cross-repo go.mod replace (Deviation 2 -- the Phase-5-resolved
dependency artifact)
go.mod gains:
replace github.com/livetemplate/livetemplate => \
../../../livetemplate/.worktrees/broadcast-redesign-phase-4
With an inline comment naming Phase 5 as its resolution. The lvt
Phase-4 branch is intentionally NOT independently CI-runnable until
Phase 5 converts this replace into a real version pin -- which matches
the proposal's documented "lvt e2e gates last (consumes both)"
release order. This commit is correct AS A PHASE-4-INTEGRATION TRACKING
COMMIT; do not merge ahead of livetemplate's Phase-0..4 release.
## Gate (manual -- pre-commit hook not installed in this checkout)
* lvt non-browser regression green (Phases 0-4 livetemplate
backward-compatible with current lvt code):
GOWORK=off go test -short -count=1 -timeout=120s \
./internal/... ./commands/... ./testing/...
* V14 Tier-2 browser e2e green (1.57s end-to-end with full 4-artifact
capture):
GOWORK=off go test -tags=browser -v -timeout=5m \
-run 'TestE2E_V14_TopicACLDeniedEmitsLvtErrorAndKeepsWSOpen' \
./e2e/
* My added file is lint-clean (verified with v2 flags
--default=none --enable=errcheck,unused,staticcheck,ineffassign;
the 84+ pre-existing lvt-wide lint issues are pre-existing on lvt
main, NOT introduced here -- the lvt scripts/pre-commit.sh has a
pre-existing --disable-all flag-removed-in-v2 bug at line 36 that
silently disabled the lint gate; flagged in proposal phase-4.md
Ledger item 4 for a separate Phase-5/6 fix).
## V14 status across the three coordinated PRs
| Tier | Repo | Status |
| ------------------------------------- | --------------- | ------ |
| Tier 1 -- Go integration (server) | livetemplate | GREEN |
| Client logic leg (jest) | client | GREEN |
| Tier 2 -- chromedp user-visible (this)| lvt | GREEN |
Server-emitted envelope contract (pinned byte-for-byte, asserted by
all three tiers):
{"type":"error","code":"topic_forbidden","topic":"<denied topic>"}
## Cross-repo coordination
Companion PRs (release order: livetemplate + client are wire-independent;
this one gates last):
- companion: livetemplate/livetemplate broadcast-redesign-phase-4 --
Option B keep-open server change (mount.go) + V14 Tier-1 regression
+ the canonical phase-4.md learnings.
- companion: livetemplate/client broadcast-redesign-phase-4 -- TS
type==='error' branch in handleWebSocketPayload -> lvt:error
CustomEvent {code, topic}; jest V14 logic leg.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #327: V14 Tier-2 chromedp e2e (topic ACL error envelope + WS keep-open)
OverviewThis PR adds a browser-tagged e2e test (
It also pins a The overall design is sound and well-thought-out. The four-artifact failure capture harness (console, server logs, WS frames, rendered HTML) is excellent practice and directly aligns with CLAUDE.md's global failure-surfacing rule. Issues🔴 High — Hardcoded author-local absolute path committed to the repo// serveLocalPhase4ClientBundle
path = "/home/adnaan/code/livetemplate/client/.worktrees/broadcast-redesign-phase-4/dist/livetemplate-client.browser.js"The env var fallback ( Suggestion: Either remove the hardcoded default entirely, or replace it with a repo-relative path convention (e.g. // Proposed alternative
if path == "" {
t.Skip("V14 e2e requires LVT_CLIENT_BUNDLE_PATH — see docs/learnings/phase-4.md for setup")
}🟡 Medium —
|
- e2e/topic_acl_error_envelope_v14_test.go:
* Replace hardcoded /home/adnaan/... default with a repo-relative
convention path (../../../../client/.worktrees/...) that honors the
standing worktree layout rule; LVT_CLIENT_BUNDLE_PATH env var still
overrides. (Claude PR #327 High.)
* Switch the missing-bundle failure from t.Fatalf to t.Skip with a
clear gating message pointing at phase-4.md -- this test is gated
on the cross-repo Phase-4 setup and is intentionally not CI-runnable
until Phase 5 (proposal release order), so failing the whole
-tags=browser run for contributors not set up for V14 would be a
hostile default.
* Add the load-bearing assertion that the server-side keep-open WARN
log fired (serverLogger.HasLog) -- proof the server took mount.go's
Option B fall-through, not just that the client happened to stay
connected. Guards against silent regressions where the WARN message
changes or the keep-open behavior is removed. (Claude PR #327 Medium.)
- go.mod:
* Add an inline 'DO NOT RUN go mod tidy' warning to the Phase-4
replace block so contributors who run go mod tidy unawares are
warned via the file itself, not just the PR description. The
replace path only resolves on the documented worktree layout;
other contributors / CI see 'module not found' and tidy would
corrupt go.sum. (Claude PR #327 Medium.)
Verified: GOWORK=off go test -tags=browser -v -timeout=5m \
-run TestE2E_V14_TopicACLDeniedEmitsLvtErrorAndKeepsWSOpen ./e2e/
PASS (1.98s) -- repo-relative path resolves, new WARN assertion fires,
post-envelope WS round-trip still works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #327: V14 Tier-2 chromedp e2e (Topic ACL error envelope + WS keep-open)
OverviewAdds the browser-facing leg of V14: a chromedp e2e that verifies an ACL-denied What works well
Issues and suggestions1.
|
…go.mod CONTRIBUTING reference) PR #327 round 2 Claude review (all minor/comment-level): - go.mod: extend the Phase-4 replace comment with the convention note. Claude flagged that CONTRIBUTING.md lists Go Workspace as the *Recommended* cross-repo dev path and Manual Replace Directives as the *Alternative*. This PR uses the documented Alternative because (a) it scopes the cross-repo dependency to this branch's git history (a workspace modification doesn't), and (b) it makes Phase 5's conversion to a real version pin a single-line diff at one greppable anchor. go.sum is unchanged from main (filesystem replaces add no sum entries — verified: `git diff main -- go.sum` returns 0 lines). Claude's #2 (missing go.sum update) is confirmed moot. - e2e/topic_acl_error_envelope_v14_test.go: * slog ordering: add explicit comment on the load-bearing `defer serverLogger.Stop()` (LIFO) vs `t.Cleanup` (after-defers) ordering — slog.SetDefault is restored *after* the logger pipe closes, so no late writes from a shut-down logger into the global slog. (Claude PR #327 round-2 #5.) * errCount sanity-check failure message: include the contents of `window.__lvtErrors` in the failure message so spurious-extra-event debugging doesn't require re-running. dump() was already called before this fatal — Claude's #6 (missing dump) was based on stale code, but the message-enrichment captures the intent. (Claude PR #327 round-2 #6.) Other items declined / acknowledged: - #3 (extract poll() to shared helper): low-priority future per Claude; out of Phase-4 scope. Tracked for a future PR if the pattern recurs. - #4 (log.Printf in goroutine): Claude confirmed correct — t.Logf in a goroutine that may outlive the test would be a data race / panic. No action. Verified: GOWORK=off go test -tags=browser -v -timeout=5m \ -run TestE2E_V14_TopicACLDeniedEmitsLvtErrorAndKeepsWSOpen ./e2e/ PASS (1.63s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #327: V14 Tier-2 chromedp e2e (Draft, Phase 4)
OverviewAdds a single
The Strengths
Issues / Suggestions1. The // current — inline in the test
poll := func(jsExpr string, timeout time.Duration, why string) { … }
// preferred — shared helper
e2etest.PollJS(t, ctx, jsExpr, timeout, why)2. go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("v14 server: %v", err) // goes to the standard logger, not the captured slog
}
}()
3. chromeCtx, cleanup := e2etest.SetupDockerChrome(t, 60*time.Second)
ctx := chromeCtx.Context // shadows `context` import if anyone adds context.WithTimeout laterConsider 4. The replace is: The path is relative to the 5. Server shutdown timeout swallows errors silently _ = server.Shutdown(ctx)For a test helper this is fine, but Test Coverage Assessment
Coverage is thorough for a Tier-2 e2e gate. The only scenario not explicitly covered is WS reconnect loop suppression — if the keep-open behavior had a bug and the socket actually closed, the client's auto-reconnect could mask it (the Pre-Merge Checklist (Phase 5)
Bottom LineThe design is sound, the harness decisions are well-motivated, and the inline documentation is unusually thorough. The code is correct for its Phase-4 tracking purpose. The only blocking item before Phase-5 merge is verifying the 🤖 Generated with Claude Code |
PR #327 round 3 Claude review — minor improvements + targeted declines: - e2e/topic_acl_error_envelope_v14_test.go: * Replace `log.Printf` with `slog.Error` in the server ListenAndServe goroutine so a startup error lands in the serverLogger-captured stream the test surfaces on failure (was escaping the slog tee to default process stderr). slog.SetDefault is already set BEFORE startV14ACLServer runs, so this is safe. Removed the now-unused `log` import. (Claude PR #327 round-3 #2.) * Log non-nil `server.Shutdown` errors via slog.Error so flaky teardown on slow CI runners is diagnosable once Phase 5 makes the e2e CI-runnable. (Claude PR #327 round-3 #5.) Items declined (with PR reply): - #1 (extract poll() to shared e2etest helper, medium pre-merge): YAGNI for Phase 4 — no other browser test currently uses this pattern. Tracked for Phase 5/6 if the pattern recurs. - #3 (`ctx` shadows `context` pkg): follow existing pattern — `lifecycle_ergonomics_test.go` does the same shadowing; no harm here (no `context.WithTimeout` after this point). - #4 (go.mod replace path depth): empirically correct. The path `../../../livetemplate/.worktrees/broadcast-redesign-phase-4` correctly resolves from the worktree-nested go.mod (`<umbrella>/lvt/.worktrees/broadcast-redesign-phase-4/go.mod` → 3 `..` → `<umbrella>/` → target). Claude assumed a flat `<umbrella>/lvt/go.mod` layout (1 `..`), which doesn't match the standing worktree convention. The V14 e2e PASSING end-to-end is empirical proof — if the path were wrong, the test wouldn't compile. Verified: GOWORK=off go test -tags=browser -v -timeout=5m \ -run TestE2E_V14_TopicACLDeniedEmitsLvtErrorAndKeepsWSOpen ./e2e/ PASS (1.69s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round-3 review responses: Addressed (in a9e2a54):
Declined with rationale:
Per the AI Code Review Workflow convergence rule, the only new functional items in round 3 (#2 and #5) are addressed; the rest are style/cosmetic or already-correct items. Expecting round 4 to converge. |
Code Review — PR #327: V14 Tier-2 chromedp e2e (DRAFT / Phase-4 tracking artifact)
OverviewThis PR adds:
What's Working Well
Issues & Suggestions1.
|
| Area | Assessment |
|---|---|
| Test correctness | ✅ Thorough — 3+1 assertions cover the spec tightly |
| Harness design | ✅ 4-artifact capture, capture-phase listener, skip-not-fatal are all correct |
go.mod replace |
|
| Log assertion fragility | |
| Code clarity | ✅ Inline comments explain the non-obvious decisions well |
| CI impact | ✅ Intentionally broken per documented release order — not a bug |
The code is well-crafted and the harness decisions are sound. The go.mod replace and log-message coupling are the two things worth hardening before Phase 5 merges this.
🤖 Generated with Claude Code
PR #327 round 4 — two one-line documentation hardenings + targeted declines: - e2e/topic_acl_error_envelope_v14_test.go: * Add grep-anchor comment pointing at livetemplate's mount.go WARN string the keep-open assertion couples to (substring match would silently no-op if the WARN is reworded; the anchor makes the coupling explicit for the Phase-5 owner). Note structured-attribute hardening as a future Phase 5/6 option. (Claude PR #327 round-4 #2.) * Add explicit 'Not parallel: this test mutates the process-global slog.Default' comment at the top of the test function. (Claude PR #327 round-4 #5.) Items declined / acknowledged (see PR reply): - #1 (Makefile guard / pre-commit sentinel for go.mod replace): defensive add-on; the existing inline warning + PR body + DRAFT label are already 3 layers of warning. Phase 5 resolves the replace entirely, removing the failure mode permanently. - #3 (poll() time.Sleep busy-wait): Claude framed as 'worth noting for Phase 5/6, not a blocker'; same as round 3 #1 — defer to Phase 5/6. - #4 (io.MultiWriter to stderr is noisy in CI): intentional pattern, matches lifecycle_ergonomics_test.go; dev-time live tailing visibility outweighs CI noise on a passing test, and dump() still surfaces the captured stream on failure. - #6 (Ping uses blank identifier for ctx): Claude confirms intentional for test fixture — no action. Verified: GOWORK=off go test -tags=browser -v -timeout=5m \ -run TestE2E_V14_TopicACLDeniedEmitsLvtErrorAndKeepsWSOpen ./e2e/ PASS (1.54s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round-4 review responses: Addressed (in 4ca0773):
Declined / deferred with rationale:
Round-4 functional delta: zero blockers. The two addressed items are documentation hardenings. Per the AI Code Review Workflow convergence rule, expecting round 5 to converge fully — the only items remaining are accepted-defers / repeats. |
Code Review — PR #327: V14 Tier-2 chromedp e2e (topic ACL error envelope + WS keep-open)
OverviewAdds Strengths
Issues and Suggestions1. Log-string coupling is the biggest correctness risk (acknowledged, but worth prioritizing)if !serverLogger.HasLog("connection kept open") {This asserts on a prose substring of a // livetemplate/mount.go
slog.Warn("Mount Subscribe denied by topic ACL",
slog.String("event", "topic_acl_ws_denied_kept_open"), // ← stable key
slog.String("topic", topic),
)
// test
if !serverLogger.HasLogKey("event", "topic_acl_ws_denied_kept_open") {2.
|
|
Round-5 review — convergence reply, no code push. Per the user-documented AI Code Review Workflow convergence rule ("The convergence signal is 'successive rounds aren't identifying any new functional issue' — only style, phrasing, or wording alternatives"), round 5 is the convergence signal for this PR. Triage: Repeats of items addressed or declined in earlier rounds:
New cosmetic items, all rated "not a blocker" by the review itself:
Loop closure: round 5 raised zero new functional issues — only repeated previously-resolved items + 3 cosmetic non-blockers. Standing convergence per the user's documented rule. Phase 5 picks up the structured-key + replace-resolution + bundle-handler-revert + poll-helper-extraction as its deliverable bundle, all already tracked in phase-4.md Phase 5 adjustments. No code push for round 5; closing the loop here. |
|
Closing as superseded by the upcoming Phase-5 lvt PR. Per the Phase-5 plan, the Phase-5 lvt PR will:
The three load-bearing harness decisions documented here (full 4-artifact capture, capture-phase document listener for the non-bubbling event, No code is lost; the Phase-5 lvt PR supersedes this one cleanly. Refs livetemplate#415 livetemplate#429 |
Warning
DRAFT — do not merge ahead of livetemplate's Phase-0..4 release. This PR carries a committed
go.modreplacepointing at../../../livetemplate/.worktrees/broadcast-redesign-phase-4(a path that exists only on the author's machine). CI is guaranteed to fail with amodule not foundbuild error until Phase 5'sgo.modpin bump resolves thereplaceto a real version pin. This is NOT a CI bug — it is the proposal's documented "lvt e2e gates last (consumes both)" release order. The PR is open as a tracking artifact.Phase 4 of livetemplate's broadcast-action-redesign (#415). Adds the Tier-2 user-visible leg of V14 — end-to-end in a real browser against a real livetemplate handler — and the cross-repo
go.modreplacerequired to build it against the unreleased livetemplate Phase-0..4.Companion PRs (release order):
livetemplate#427(server keep-open + Tier-1) andclient#121(TSlvt:errorbranch + jest) are wire-independent and ship first; this PR consumes both.V14 (Tier-2 user-visible leg)
e2e/topic_acl_error_envelope_v14_test.go(new,//go:build browser): verifies that an ACL-deniedctx.Subscribein the WS-connect Mount surfaces in a real browser as anlvt:errorCustomEvent{ code, topic }on the wrapper AND leaves the WebSocket open and functional (advisor sharpening: stays functional, not merely un-closed — the test sends a click action over the same socket post-envelope and asserts the rendered value updates).Three load-bearing harness decisions, all inline-commented
installConsoleLoggerfromlifecycle_ergonomics_test.go(samepackage e2e_test, streamed viat.Logflive);e2etest.NewServerLogger()teed into the globalslogdefault with save+restore int.Cleanup(advisor parallel-safety:slog.SetDefaultis process-global);e2etest.RecordWSFrames(lvt#317), surfaced viawsLog.PrintLast;chromedp.OuterHTML("html")in adump()closure called beforet.Fatalf(chrome ctx is cancelled bydefer cleanup()beforet.Cleanupruns; HTML must be captured at failure-time).The first failed run during development dumped server logs + WS frames + HTML and surfaced the missing
IsInitialMountguard (Deviation 3 below) — proof the harness works as intended.lvt:erroron the wrapper withbubbles:false. Installing the listener viadocument.addEventListener('lvt:error', …, true)in a<head>script beforeclient.jsloads catches the wrapper dispatch via DOM capture phase (capture traverses window→target regardless ofbubbles). Race-free without instrumenting the client.e2etest.ServeClientLibrary. The canonical helper fetches@livetemplate/clientfrom the unpkg CDN and caches on disk for 1h; that cache can shadowLVT_CLIENT_CDN_URL, so an unreleased client cannot be reliably served via the canonical path. Reverts toServeClientLibraryin Phase 5/6 after npm publish.IsInitialMountguard — the e2e exposed a Mount-path dual-fire reality the Tier-1 test does not exerciseMount runs on every HTTP request AND WebSocket connect (livetemplate CLAUDE.md). On the initial HTTP GET (page render), a denied
ctx.Subscribesurfaces as HTTP 500 (mount.goHTTP-GET path; phase-1.md pre-existing behavior, unchanged by Phase 4). Without the guard, the page would 500 beforeclient.jsever loaded and there would be no WS for V14 to exercise. The controller guards withif ctx.IsInitialMount() { return s, nil }to scope the denial to the WS-connect Mount — exactly V14's spec scenario.Cross-repo
go.modreplace— the Phase-5-resolved dependency artifactgo.modgains:…with an inline comment naming Phase 5 as its resolution. Phase 5's
go.mod pin bumpdeliverable is literally "convert thisreplaceinto a real version pin." Until then, this PR is correct as a Phase-4-integration tracking commit; do not merge ahead of livetemplate's Phase-0..4 release.Gate (manual — pre-commit hook not installed in this checkout)
GOWORK=off go test -short -count=1 -timeout=120s ./internal/... ./commands/... ./testing/...GOWORK=off go test -tags=browser -v -timeout=5m -run 'TestE2E_V14_TopicACLDeniedEmitsLvtErrorAndKeepsWSOpen' ./e2e/--default=none --enable=errcheck,unused,staticcheck,ineffassign). The 84+ pre-existing lvt-wide lint issues are pre-existing on lvtmain, not introduced here — the lvtscripts/pre-commit.shhas a pre-existing--disable-allflag-removed-in-v2 bug at line 36 that silently disabled the lint gate. Flagged in livetemplate phase-4.md Ledger item 4 for a separate Phase-5/6 fix.V14 status across the 3 coordinated PRs
replacecaveat above)Server-emitted envelope contract (pinned byte-for-byte, asserted by all three tiers):
{"type":"error","code":"topic_forbidden","topic":"<denied topic>"}🤖 Generated with Claude Code