Checks tab still visible on zero-check repo after #505 (summary 401 fails open / possible deploy lag) #513
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#513
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's requested
The #505 fix (merged in
694ff51, PR #510) does not hide the Checks tab on a zero-check repo on the live instance — the tab is still visible, and the page's summary request is 401. Diagnose and fix the deployed gap; static diagnosis from code reading + a live rendered-DOM probe (Playwright). No local repro of the app, no code changes on the filing side.Evidence (live probe, 2026-09-14 ~12:15 UTC)
Rendered page
https://hub.packden.us/crueber/walhub(anonymous session) shows the tab strip:Checksis visible while the same page'sGET /crueber/walhub/api(the summary endpoint) returns 401 authentication required for the anonymous caller, along withresolve,watch,social,tasks(all 401).Root-cause analysis (static, current tree)
The #505 mechanism is complete in the tree end-to-end:
internal/api/summary.go—has_checksrides the summary body (summaryBody.HasChecks, line ~51, populated at line ~176 from theEnv.ChecksSummaryhook); the ETag covers the index version (~ksuffix, line ~231) so the first report busts the cache.cmd/walhub/collab.go~202 setsapiEnv.ChecksSummaryfrom the checks service'sHasChecksprobe (internal/checks/service.go~539) — absent index →ok=false→has_checks:false.web/src/lib/tabs.jsshowChecksTab(summary)(line ~80) fails OPEN:if (!summary) return true; return summary.has_checks !== false;The failure is at the boundary between the two: when the summary request fails (401 here),
getSummary()is null/undefined,showChecksTabfails open and the tab renders. So every anonymous render of an auth-gated repo (hub.packden.us runs OIDC with anonymous read off at times) shows the Checks tab regardless ofhas_checks— the #505 gate never gets a summary to gate on. This matches the fail-open design comment intabs.js(loading/unknown keeps the tab), but on this instance "unknown" is the steady state for anonymous visitors, so the tab is permanently visible there even with zero checks.Secondary possibilities to rule in/out at fix time:
694ff51, an older SPA bundle serves the unconditionalTABSrender. Verify the deployed binary/SPA actually contains the #505 build before touching anything else.private, no-cache) with the~kETag suffix covering the checks index version — no stale-serve window that would keephas_checks:falseafter a report, so cache is NOT the mechanism for "tab visible on zero-check repo."Acceptance criteria
tabs.jscomment — if kept, note it as designed and close)./api#checks-cireporting-API pointer in the repo-meta line (Repo.jsx~731) so external CI authors can still discover the reporting API./:owner/:name/checksstill render the empty state when the tab is hidden (no regression on #505's route guarantee).Diagnosed + fixed in PR #515 (fix/issue-513, unmerged). Verdict: deploy lag RULED OUT — the live bundle (index-NJ0Ao3tk.js) already carries the #505 has_checks gate + reporting-API pointer; the anonymous summary 401 on hub.packden.us is correct server behavior (gated repo), so 401-fail-open was the whole story: getSummary() never settles → showChecksTab(undefined)=true forever. Fix per decided policy: shell records explicit 401 in summaryDenied, tab hides on denial, loading stays fail-open. Tests 1154/1153/1 (1 pre-existing smoke fail, head-to-head verified); vite+esbuild green.
Review of PR #515 (fix/issue-513) — verified in scratch worktree /tmp/pr515 (removed afterward). No browser (per task rules; node tests + reasoning only — noted explicitly).
VERDICT: ready to merge (no fixes pushed — none needed).
(1) Diagnosis: SOUND. The 401-fail-open mechanism is confirmed straight from the tree: pre-fix tabs.js
if (!summary) return true+ a 401-denied shared summary that never settles = tab pinned visible forever. Deploy-lag ruled out per author's live-bundle evidence (bundle carries the #505 gate + pointer, container recreated post-merge); the code mechanism alone fully explains the symptom, so the fix is correct either way. 401 is correct server behavior (gated repo, OIDC anonymous-read off) — client-side policy fix is the right layer.(2) Denied-signal wiring (Repo.jsx:613-623,851): CORRECT. Explicit 401 recorded via SDK
err.unauthorized(errors.js:30, status===401 — the documented marker); 404→null via tolerateMissing untouched (data.js:77-82 — 404 resolves before the outer catch ever sees it); all other errors rethrow into the tray path. Resolves undefined (still "loading", never "not found" — 401≠deletion) and spares tray spam. Flag resets at fetch start (line 615), so repo switches (newrepo:{full}key → new entry → refetch) never inherit stale denial; sign-in is an OIDC redirect/reload (fresh signals), worst case bounded by next fetch.(3) Helper contract (tabs.js:84-88): CORRECT.
opts.deniedhides; non-denied keeps the exact #505 rule;opts={}default keeps every single-arg call backward compatible (verified: checksHidden memo :661, meta pointer :750, all old callers behave byte-identically). Loading fail-open preserved (no strip flicker, old servers not stranded).(4) Untouched: CONFIRMED via diff. TABS model (:169),
activeTabsegments,tabBadgewiring (:851tabBadge(getSummary(), t.id)),<For each={TABS}>render, checks route/empty state,/api#checks-cimeta pointer (:750-752 — still gated on the loaded hidden state, exactly per criterion 4's #505 scope). Note: denied viewers see the "loading…" header (getSummary() stays undefined, :684) with no pointer — expected, there is no summary to anchor it to; doc states "renders only on a loaded hidden state". checksHidden (:661) stays false on denial → no extra["check"]stream (nothing readable to invalidate) — correct.(5) Authenticated flows: UNAFFECTED. Flag defaults false; 200/404 paths byte-identical; only the 401 path changes (previously: tray spam + stuck "loading…" + wrongly visible tab; now: silent + stuck "loading…" + hidden tab).
(6) Scope/docs: zero .go files, no SDK/API change, no new deps (law 1 holds), 12_web_ui.md decision appended in-commit (law 12 holds).
TESTS (scratch worktree, node_modules symlinked from main): full
node --test web/test/unit/*.test.js→ 1153 pass, 1 fail = smoke.test.js/: /setup status— ENVIRONMENTAL, not PR-caused: a live server is up on :8080 here (healthz 200) with setup locked (/setup → 403); the smokeup-gate passes on healthz then asserts /setup 200. A frontend-only diff (0 .go files) cannot change an HTTP status. Focused: checks-tab-513 (4 new) + checks-tab-505 files → 9/9 pass incl. the 12-state denial×summary matrix. vite build + esbuild green (unauthorizedgate present in bundle;summaryDeniedminified away as expected). pnpm/mise shim unavailable in this shell so vite/esbuild were run directly from web/node_modules/.bin — same scripts asmake web.Acceptance criteria mapping: mechanism confirmed before code change ✓ / 401-fail-open policy implemented (hide on explicit denial, loading fail-open) ✓ / pointer kept on loaded-hidden state ✓ / deep-link empty state untouched ✓.
Fixed by PR #515 (review clean — diagnosis confirmed, denied-signal correct, loading fail-open preserved, authenticated unaffected), merged. Closing.
Reopening: still reproduces. Root cause found in-code (no fix yet): the ~k ETag suffix is conditional on checksOK, so a zero-check repo's ETag is byte-identical to its pre-#505 ETag — any client holding a pre-#505 cached summary 304-matches forever and never receives has_checks, and the helper fail-opens on the missing field. Fix: always-present ~k suffix. Repro: prime cache with a field-less body + old ETag, revalidate after #505 → 304 (wrong), must become 200.
Root cause found in-code + fix PR: see above (unconditional ~k suffix).
Review of PR #516 (fix/issue-513-etag) — verified in scratch worktree /tmp/pr516 (removed afterward). No browser per task rules (ETag-only backend change; tests + reasoning only — noted explicitly). No fixes pushed — none needed.
VERDICT: ready to merge.
(1) Root-cause claim: SOUND. Old summary.go appended ~k only when checksOK, so a zero-check repo's ETag was byte-identical to its pre-#505 ETag; writeCached 304s on match, so a pre-#505 cached field-less body revalidated forever and never received has_checks → missing-field fail-open kept the Checks tab visible. Corroborated by the pre-PR handlers_test.go, which asserted bare-sha If-None-Match → 304 (that assertion is exactly the bug, and the PR correctly rewrites it to read the live etag).
(2) ~k0 collision analysis: SAFE, verified in code. updateIndex (service.go:566-574) builds a zero-value IndexDoc then ix.Version++ before the first write, so a present index is always Version >= 1; CompactIndex only ++s existing docs; HasChecks returns the stored version verbatim. Hook-nil path falls into the same else branch (nil hook → checksOK=false → ~k0; covered by the 'nil hook' pin). Theoretical edge only: hand-written index bytes with version:0 would read ok=true/Version=0 and emit a colliding ~k0 — unreachable via any writer, not worth hardening.
(3) No behavior change besides ETag: summary.go diff is a pure else-branch string append; body construction untouched (has_checks rides exactly as #505 left it); Cache-Control untouched (mutable-collab no-cache); zero new store trips — the ChecksSummary probe pre-exists from #505, law 6 holds (summary off the push/sync/checkpoint budgets, sim unaffected).
(4) Regression test: TestSummaryChecksRevalidate (summary505_test.go:95-112) sends a bare-sha pre-#505 token, requires 200 + has_checks present-and-false. Proves the fix. Verified it FAILS on base code (overlaid origin/main summary.go into scratch: etag pin fails bare-vs-~k0; and on old code the bare token IS the current etag so the 200-requirement fails too — the old 304 test proved that path). Restore byte-verified via diff afterward.
(5) Pin updates: every changed pin bare → ~k0 exact-match across handlers/summary235/summary240/summary319/summary424/visibility345/health209/gaps — all justified, none weakened. health209 wantNoETag removal is correct (unborn now emits exact '"~k0"'; equally strong exact assertions). handlers_test 304 path now reads the live etag (stronger). Remaining bare-sha If-None-Match sends are intentional: tree-endpoint immutability (handlers_test.go:564), stale-healthy-must-200 (health209_test.go:157), the new regression probe (summary505_test.go:100). No gaps.
(6) Wording: collab.go checks comment, service.go HasChecks comment, and both 07_api.md passages updated to unconditional-~k0; the new 'Unconditional ~k suffix' decision appended (law 12). Leftover 'byte-identical ETag' strings (collab.go:145,160 re #319/#424, 07_api.md:1223 re ~c, counts_test.go:16) each scope to their own projection's conditional suffix (no ~c/~f added when declined) — still accurate, not stale.
(7) Tests: api -race clean, coverage 95.3% (gate holds); checks -race clean (32s); cmd/walhub clean; gofmt/vet clean; go build ./... green. internal/server shows 10 failures, all 'ui shell missing — run make build' (scratch web/dist unbuilt; PR touches no server/UI-serving code — environmental, not PR-caused). e2e: unaffected by reasoning + grep (no ETag pins outside internal/api; change is header-only on one endpoint).
No small problems found; nothing to fix. Merge recommended as-is. Main worktree left clean (read-only throughout).
Fixed by PR #516 (review clean — root cause sound, ~k0 collision-safe, regression fails on base; api 95.3%, -race clean), merged. Closing for the second time.