0.35.0 2026-10-06
Performance, correctness and CI-parity work on the test gate and three production shortcuts it exposed. No user-facing feature changes; the one new knob is DAGNATS_GREET_PACING.
Changed
- The test suite is ~44% faster (75.5s → ~42s wall, measured 40.6–45.2s across runs) without weakening what it checks. The cost was not the tests but latency the production code made them wait out: four demonstration/backoff delays (~20s), a 6s negative-assertion window, and a bcrypt cost of 10 that made every fixture login cost ~960ms under
-race. Fixed by making pacing injectable (Queue.SetRetry,(*TodoHandler).SetRetryDemoDelay,llm.NewSimulatedWithDelay) with production defaults untouched, plus event-driven waits.t.Parallel()onfeatures/todo,features/whiteboard,internal/queue,internal/natsandfeatures/store/crdtstore(per-test fixtures were already isolated) added the rest. make ci-localnow runscheck-generated,install-sh-guardand the binary boot smoke test, so the local gate matches the remote one step-for-step.docs/local-ci.mddocuments the parity table so the next drift is visible instead of silent.
Added
forbidigo: no baretime.Sleepin production (tests exempt). An uninterruptible wait holds its goroutine — and anywg.Wait()draining it at shutdown — for its full duration. Three real sites shipped this way; the lint now prevents a fourth.DAGNATS_GREET_PACING(default1500ms) — the onboarding-greet pause was product latency, so it is tunable rather than deletable. Non-positive or unparseable values fall back to the default.bin/check-install-sh.shandTestRegistryCoversDagnatsImports— guards for the two silent-breakage classes found while doing the above.
Fixed
Stop()blocked for up to a second per worker just because the queue was failing. The worker's receive-error path usedtime.Sleep(time.Second), which is uncancellable:Stop()cancels the pool context and then waits onwg.Wait(), so a shutdown sat through the full second. It is now aselecton the pool context. Measured:Stop()with a failing queue 946ms → 190µs.internal/natshad three mutable package globals (NS,NC,JS). One design, two symptoms: two concurrent starts raced on the same variables, and a test's teardown cleared state (and shut the server down) under its neighbours — which is why neither that package norcrdtstorecould run in parallel.StartEmbedded/StartLeafNode/ConnectExistingnow return an owned*Handlewith aClose()that frees only what it started.install.shplanted agosymlink in$BIN_DIR, which is onPATHby design — so it shadowed a system Go in every new shell (and read as though the CLI were namedgo). The toolchain is now exposed via$GO_DIR/binonPATHinstead. The same block had a reachability check that ran after prepending, so it could never be false and its guidance never printed.TestTodoRecordsNotBroadcastViaHubcould never fail. Its predicate searched for"event":"created", a string the hub path translates away before it reaches the wire — so reintroducing the very regression it guards left it green at any window. It now asserts the wire symptom and goes red in 1.5s when the broadcast is restored.css-checkfailed on a clean tree with no source change. Not a stale committed bundle: the localnode_modulesheld tailwindcss 4.3.2 / daisyui 5.6.15 whilepackage-lock.jsonpins 4.3.3 / 5.7.42.make css-installnow compares the installed versions against the lockfile and reinstalls on mismatch, and the pre-commit hook builds throughmake cssso a version skew cannot masquerade as stale CSS.- Generated artifacts were never verified in either CI — only regenerated.
_templ.go(71 files) andapp.min.cssare committed and embedded in the binary, so editing a.templorsrc/css/input.csswithout committing the regeneration passed lint, the race suite and both smoke tests, and shipped a binary rendering stale markup. Both jobs now diff after regenerating. gogogo --trim dagnatsproduced a broken tree.onboarding_lifecycle_test.goimportsinternal/dagnatsbut was missing from the unit's file list, so the trim left a dangling import and the proof build failed; and the success message still advertisedhttp://localhost:8080/dagnats/on a checkout where that console had just been deleted. Both fixed, with a guard for the first.shutdownDagNatswas a no-op, so application shutdown was skipped entirely. It only nilled a pointer, claiming the engine's shutdown was "wired internally". DagNats installs its ownsignal.Notifyfor SIGINT/SIGTERM, exactly as PocketBase does, and the runtime delivers a signal to every registered channel — so the engine tore down first andpb.Start()never returned, skipping every deferred cleanup on the way (the queue workers and the SQLite handle were simply dropped). Verified with the real binary: the old code never reachedqueue workers stopped. It now callsServer.Stop()so ours is the deterministic shutdown path.WorkerPool.Stop()was unreachable from production code.internal/server/boot.goassigned the pool to a discarded local (workersLocal := q.StartWorkers(); _ = workersLocal), andq.Close()only nilled the queue handle and closed the database out from under the still-running workers. TheQueuenow owns the pool andClose()stops it before releasing the queue and DB, so shutdown is bounded and correctly ordered.TestCollab_LeafNodeE2Ewas a real flake, not infrastructure noise. It waited for the leaf node to attach and then published once; attachment does not imply the worker'sapp.sync.>interest has propagated to the leaf, so under load the first (and only) message was dropped and the test failed after its full 15s deadline. It now re-publishes while polling for the persisted snapshot — a Loro update is an idempotent CRDT delta, so this removes the timing assumption rather than widening it.gogogo --trim dagnatsproduced gofmt-invalid Go. The strip removed the dagnats block but left its orphaned explanatory comments, the engine's boot preamble, two consecutive blank lines, and no final newline — so a freshly scaffolded project failed its ownmake fmt. The strip engine now collapses blank runs and re-terminates the file with a single newline (kept generic, so every unit benefits), and the dagnats rule drops the comments describing the call it removes.
CreateTodoForOnboardingwrote to the store through a nil context, and a DagNats handler closure depended on a context that is trace-only (the engine derives it from the message headers, so it is never cancelled on shutdown) — both found by enabling the cancellation lint rather than suppressed.
Added
rules/rules_test.go— a guard for the ruleguard rules. A rule that matches nothing is indistinguishable from a broken rule, and a broken rule fails open, so CI stays green while the footgun ships. Seven fixtures run through the realgolangci-lintagainst a minimal module, asserting a hit on each bad shape and silence on the corrected ones. Writing it showed that ONE match arm covers both ticker-receiver shapes, so the second arm (and its rule) was removed — it was not merely redundant: with two arms the linter reports only one diagnostic for a file holding a call-chain loop followed by an identifier loop. Red-proofed both ways.make check-sizesruns inci-localand in CI. The 500-line file budget was enforced only by the pre-commit hook, whichgit commit --no-verifyskips: a 1871-line file passed every other check (verified).funlennow applies to test files, with three individually-listed and commented exceptions for linear setup (127/130/138 lines) instead of a blanket exemption that hid four over-long functions. Function length is deliberately NOT enforced by a shell script —funlenalready does it, and it understands AST, so a second implementation would be a second source of truth.make lint-safe(merged from #72) — host-aware lint sizing, plusscripts/changed-packages.sh.
Verification
make ci-localgreen end-to-end, including the binary boot smoke test and the Playwright browser smoke across all three skins- Red-proofs: the
forbidigorule fires on an injectedtime.Sleep;check-generatedfails on a.templedit with no regeneration; the new CSS diff fails on a stale bundle;TestHandleConcurrentStartIsIndependentreproduces the old globals race when the handles are shared;TestWorkerStopIsPromptWhenQueueFailsfails (946ms) against the reintroduced sleep - 387 tests, 0 failures, 0 new skips;
golangci-lint0 issues with 33 linters