0.34.0 2026-10-05
Covers everything since v0.33.0. An earlier draft of this entry was numbered 0.31.1 — a version that was never tagged and sorts below the released v0.33.0, so it would have been skipped by any reader walking the file top-down.
Added
- In-repo
gogogo-coding-standardsskill (skills/gogogo-coding-standards/,SKILL.md+ 5 references): the Go concurrency/perf/testing deltas for 1.25-1.27, the Datastar gate, and a Zig gate summary. Universal principles are delegated tostelow-workflow-coding-standards(linked, not duplicated).AGENTS.mdshrank 576 → 75 lines with the load-bearing rules kept as one-liners pointing atdocs/*.md, andadvise_stacknow cites the skill as its source. make ci-local-fast— a layered gate scoped to the changed packages: the cheap decisive checks (templ, datastar-lint, css-check, check-scope) plus scoped lint and race tests, narrowed byscripts/changed-packages.sh. Measured ~2s for a CSS-only change and ~10s for one changed package, against ~240s for the full gate. It falls back to all packages when a shared file moves (go.mod,config/,db/,internal/capabilities/).make ci-localis unchanged and stays the authoritative pre-push gate.rules/rules.go— project footguns enforced by ruleguard, loaded throughgocriticso they run inside the ordinarygolangci-lintpass and block CI. The first rule flags<-time.After(...)used directly as a select case: a timer allocated per evaluation that leaks inside a loop. Also enablesperfsprint,prealloc,fatcontextandusestdlibvars.credits.Settler.Start(ctx)— the settlement worker is no longer started byNew; the caller owns its lifetime and the router binds it toApp.OnTerminate.
Fixed
- Five concurrency and resource pitfalls, each with a red-proof test.
credits.Newno longer starts workers (and its settlement ticker selects onctx.Done());onboarding.pollRunis bound to the handler's shutdown via a done channel +sync.Once— not a stored context, whichcontainedctxrejects — while the durable workflow still survives the client navigating away, since the poll context derives fromBackgroundrather thanRequest.Context(). Installer writes are confined to their target directory. - The
features/todologin flake, at its root."POST /login: context deadline exceeded"was not timing noise:requestTimeoutwas 5s while SQLite'sbusy_timeoutis 10s, so under a held write lock the request context cancelled while the database was still legitimately waiting. Reproduced deterministically — a 5s context under a 6s lock hold returnsdatabase is locked, a 20s context succeeds.requestTimeoutis now 20s with the relation tobusy_timeoutdocumented so the next reader does not shorten it again. Separately, the goqite queue DB was opened without anybusy_timeout, so a concurrent queue writer failed withSQLITE_BUSYinstead of waiting; it now carriesbusy_timeout(10000)+ WAL in the DSN. Goroutine leaks are now asserted withgoleak. - Two tests sharing a fixed port.
internal/natsandfeatures/todo/handlersboth bound18099, which is why the suite ran as a single-p 1sweep "for DagNats engine stability" — the serialization was a misdiagnosis ofbind: address already in use. Both now bind ephemeral ports; see the parallelization note below. - A browser tab opened on every run that spawns the real binary. PocketBase's first-run installer mints a
pbinstalltoken and callsosutils.LaunchURL; a test booting the binary with a throwawayDATA_DIRhas no superuser, so it fired every time — usually pointing at a server that had not bound, hence the "127.0.0.1 refused to connect" tab.router.Initsuppressed it only wheninteractive()was false, but a child inherits the parent's TTY:make testfrom a real terminal made the child look interactive too.isCharDevicewas also the wrong predicate —/dev/nullis a character device — so< /dev/null > /dev/nullpassed as a terminal. NowisTerminalrejects/dev/nullexplicitly, and every test that spawns the binary setsGOGOGO_NO_BROWSER=1in an env built from scratch (an inherited=0no longer wins). - The SSE test pump did not honour its timeout.
pumpSSEUntilchecked its deadline only betweenBody.Readcalls, andReadblocks until the next event — so a caller asking for a 6s window waited one full heartbeat (15s).TestTodoRecordsNotBroadcastViaHubtook 15.5s for a 6s negative assertion. The read now runs in a goroutine with a timer select that closes the body on expiry. - An SSE predicate that could never fire burned its whole timeout.
TestIntegration_SuggestSimulatedEnqueuesAndStreamsResultunmarshalled the raw SSE transcript as JSON, which isevent:/data:lines and carries asignalsprefix on each payload, sojson.Unmarshalalways failed: the test waited the full 14s and passed on a later assertion. Now parses viasseSignalPayloads. 15.5s → 5.0s. - A data race in the todo fixture. Four test fixtures assigned
auth.CookieSecure = false— its zero value, so a no-op write — which is a genuine race as soon as two tests in a package overlap (surfaced on the first attempt to addt.Parallel()). The assignments are deleted and the constraint documented on the variable: set shared state once inTestMain, never in a fixture. - Two more fixed ports in tests.
bootLiveServerhardcoded8291andcmd/web/smoke_test.gohardcoded18199— the same collision class as above. Both now reserve an ephemeral port. - The skill's YAML frontmatter failed to parse. The
gogogo-coding-standardsdescription was an unquoted scalar containing"Triggers when: ", and": "starts a mapping in YAML, so every host reported anError in user YAML. The description is quoted and a frontmatter gate now runs in CI so it cannot recur. - The Datastar gate was checking almost nothing.
bin/datastar-lintdid not forward"$@", so every argument was discarded:make datastar-lintlinted the whole repo (includingnode_modules) and printed 264 warnings where 0 were expected — the filters looked applied and were not. CI now pins the linter version instead of@latest, because a release can raise a rule to ERROR and fail the build with no code change here.
Changed
features/todorace suite: 148s → ~70s, andmake ci-localto ~110s, with no change in coverage and zero races.owner_require_test.go's per-Read goroutine loop (one leaked goroutine per 500ms tick) now uses the shared pump.- The test suite runs the four engine packages serialized and everything else in parallel.
scripts/test-web.shdetects the engine packages (cmd/web,features/todo,features/todo/handlers,internal/nats), runs those-p 1in the foreground and the remaining 35 at default parallelism alongside, so the parallel group hides under the engine's wall-clock. Measured ~2m10 vs ~4m15, verified across 3 runs atGOMAXPROCS=4(the CI runner's size) and 2 atGOMAXPROCS=2. - The queue worker no longer allocates a timer per idle second. The worker loop used
time.After(200ms)inside itsfor {}, the exact pattern the standards skill warns about; it resets a single timer instead. The per-tickInfolog becameDebug— it was one line per worker per idle second (4/s, ~345k lines/day) with nothing actionable. golangci-lint2.13.2 → 2.14.0 (CI pin updated). The Homebrew bottle is built with go1.26 and refuses this repo's go1.27.1; the official release binary (go1.27.0) is what works.- New ruleguard rule
BlockingReadBehindDeadlineflags aBody.Readinside afor time.Now().Before(deadline)loop in tests — the construct above, made mechanical instead of prose. docs/local-ci.mdrecords why the browser smoke test stays on Playwright, with the lighter candidates measured rather than assumed. Lightpanda hasnavigator.serviceWorkerandcachesbothundefined, so the offline path the smoke test exists to prove — Service Worker + IndexedDB outbox + replay — cannot run there.
Verification
make ci-localgreen (templ + datastar-lint + css-check + check-scope + golangci-lint + race tests + build + Playwright smoke)- Red-proofs: reverting
isTerminalfailsTestIsTerminal_NonTerminalsAreRejected; revertingpumpSSEUntilmakesTestPumpSSEUntil_HonorsDeadlineWhileReadParkedhang; the ruleguard rules fire on the bad patterns and stay silent on the corrected helpers; reverting the concurrency bounds leaks goroutines undergoleak - A
PATHshim interceptingopenrecords zero browser launches across the fullci-local