What We Worked On
Plan 104 of the Not-Telerik component library (Bawn.Components.Blazor) is the render-mode and browser-farm hardening plan: every shipped component owes proof that it survives real browsers, real render modes, and real packaging. One agent had been grinding forward from component 001; Philip’s instruction to this session was to use workflows and work backwards towards that agent. So while the forward agent finished NtFieldChrome (014, 8e1b5b7) and paused, a five-agent workflow took the far end of the queue — the AI components 099–103 — each agent in its own isolated git worktree with its own farm port, three lanes running concurrently.
All five slices came back green, each as one atomic commit:
| Commit | Component | Plan |
|---|---|---|
| e174d21 | NtAiPrompt | 099 |
| 4a6536d | NtInlineAiPrompt | 100 |
| c0ce913 | NtPromptBox | 101 |
| fbdba27 | NtSmartPaste | 102 |
| 91109b8 | NtSpeechToText | 103 |
Integration was its own chapter: five cherry-picks onto main, mechanical-but-fiddly conflict resolution in the shared runner, one real bug that only the integrated tree could reveal (fed1f5f), and a closing evidence commit (847f37b). The wave pushed as seven commits, and the plan-104 checklist gained twenty-one checked boxes.
What the Farm Actually Tests
The farm is worth explaining carefully, because it tests something none of the other 3,367 tests in the repo can see.
The shipped package, not the source tree
tools/run-browser-farm.sh packs Bawn.Components.Blazor into a real NuGet package at a unique throwaway version, then restores a checked-in consumer Blazor Web App against that package only — an isolated package cache guarantees no other copy can satisfy the restore — publishes it, and runs it under ASPNETCORE_ENVIRONMENT=Production. It validates what a customer installs: packaging metadata, static web assets, the scoped CSS bundle, JS modules. Not what happens to work via project reference.
That distinction is exactly why this wave found real bugs: NtPromptBox’s collocated JS and CSS loaded fine from source but broke from the package, and the week’s best bug appeared only once the packaged CSS bundle genuinely loaded.
Per component, per render mode
Each of the 19 covered components has four routes:
Before any browser is involved, the runner fetches each route’s raw HTML and asserts on the prerendered string itself: exact instance counts, hostile-looking text present but HTML-encoded (<img src=x onerror=alert(1)>, never live markup), zero callbacks fired during prerender, correct initial state attributes. That proves static SSR emits useful, stable markup with no code running prematurely.
Then three real engines
Playwright drives Chromium, Firefox, and WebKit through every route and asserts: hydration adds interactivity exactly once, with the correct renderer per mode; real keyboard and pointer input works (NtSpeechToText’s journeys inject a deterministic recognizer through the public ISpeechRecognizer seam, since headless browsers have no Web Speech API); Arabic ar-EG culture with inherited RTL layout; reduced-motion emulation; forced-colors (high-contrast) rendering; viewport resize down to 480px. Server-mode journeys literally close the _blazor WebSocket and verify events still work after the circuit reconnects. Teardown/remount cycles prove observers, listeners, and JS modules are released. And every page probes #blazor-error-ui, so a silently terminated circuit fails the run.
The check that caught this wave’s bug is two lines: set document.documentElement.style.zoom to 200% then 400%, and assert scrollWidth <= clientWidth + 1. In plain terms: a low-vision user who zooms the page must never get a horizontal scrollbar.
The other two gates in each validation run are ordinary but necessary bookends: the Release build proves the tree compiles as it will ship, and the full dotnet test suite (unit + bUnit rendering tests, 3,367 of them by wave’s end) proves the slices’ library fixes didn’t regress anything at the component-logic level.
The One-Pixel Bug
Every slice passed its own full three-engine farm run before integration. The integrated tree failed one journey:
Two device pixels — one CSS pixel at 200% zoom. The culprit was the visually hidden screen-reader announcer, which used the classic hidden-content pattern: position: absolute; inline-size: 1px; margin: -1px; clip: rect(0,0,0,0). In an RTL document, that -1px margin pushes the box one pixel past the document’s right edge, and the zoom-fit assertion catches the resulting scrollable overflow.
The 101 agent had discovered that the farm consumer never actually loaded the library’s scoped CSS bundle (WebKit resolves @import in .styles.css against the document URL when a <base> tag is present) and fixed it with a ScopedCssLink component. So slice 103 verified NtSpeechToText honestly in its own worktree — with the component’s collocated CSS silently absent. Only when both slices merged did the styles load and the pixel poke out. A green slice is not a green union.
The fix (fed1f5f) drops the negative margin and adds clip-path: inset(50%) across all five wave components’ announcers. NtChat, NtGauge, NtTaskBoard, and the global stylesheet still carry the pattern — recorded in the evidence for their own future hardening slices.
What Went Well
Five for five. Every workflow agent finished green: full three-engine farm plus full test suite in its own worktree, one atomic commit, no pushes. The farm harness’s parallel design (per-run mktemp workdirs, PID-qualified package versions, NT_BROWSER_FARM_PORT) held up under three concurrent runs.
The farm earned its keep. Real defects fixed this wave, all invisible to bUnit: case-insensitive attribute filtering bypasses in four components (mixed-case CLASS/ROLE/ARIA-*/DATA-NT-* splats could shadow component-owned semantics), a hardcoded Cancel label bypassing localization in NtSmartPaste, a missing clipboard-read timeout that hung Firefox headless forever, the scoped-CSS-bundle packaging gap, and the RTL announcer overflow.
Append-only discipline made integration tractable. Each agent was instructed to append at the end of s_components, the prerender checks, the dispatch chain, and the file. The conflicts were all at known seams and resolved mechanically — plan order, keep both sides.
Meeting protocol with the forward agent worked. It finished 014, pushed, and paused; the backward lane integrated and pushed with exactly one writer on origin/main at a time.
What Didn’t Go Well
Three agents ended their dispatch chains with a bare else. Fine in isolation, a misrouting landmine in a merged chain. Integration normalized every branch to an explicit else if and added a final guard that throws on an unregistered component.
Git fused similar-shaped method tails. The journey methods all start with the same GotoAsync boilerplate, so cherry-picks interleaved two slices’ methods into Frankenstein conflict hunks. The reliable resolution was to rebuild the file tail verbatim from each slice’s own commit rather than resolve hunk-by-hunk — and one orphan conflict marker still slipped into an intermediate commit before being caught.
The validation kept getting killed at exactly 600 seconds. The repo’s run-bounded.sh hook wraps background commands in a 10-minute timeout, and it reads its limit from its own environment — setting NT_TEST_TIMEOUT_SECONDS inside the wrapped command does nothing. Two runs died mid-WebKit before the validation moved into a detached systemd-run --user unit with a monitor watching the log.
Debug-loop footguns. Republishing the consumer over the same output directory left two fingerprinted CSS bundles side by side (the fix looked ineffective because the stale bundle was still being picked up), and plain &-backgrounded servers kept getting reaped by the harness until they ran as proper background tasks.
Takeaways
-
1Test the artifact you ship
Project references lie. Packing, restoring from an isolated feed, and running under Production found packaging bugs that three thousand unit tests never could.
-
2A green slice is not a green union
Slice 103 was verified honestly and still hid a bug, because slice 101 changed what “loading the package” meant. The integrated re-run is not bureaucracy; it is the only run that tests the tree you actually push.
-
3Accessibility affordances need accessibility tests
The bug was in the screen-reader announcer and against the low-vision zoom guarantee — one assistive feature breaking another, caught only because zoom-fit is asserted in RTL.
-
4Design for the merge when running parallel agents
Append-only extension points, explicit dispatch branches, and a loud guard for the unregistered case turn five-way conflicts into mechanical concatenation.
-
5Know your own harness
A timeout wrapper that can’t be configured from inside the command it wraps will keep killing your longest, most important runs — the ones that only fail at minute eleven.
The Watercolor
If I painted this session, it would be a greenhouse at night — long rows of glass terrariums receding into the dark, each one glowing from within. Three pigments only for the light: a warm cadmium orange for Chromium, a cool phthalo blue for Firefox, a sea-glass teal for WebKit. The wash would be loose and wet where the five worktrees ran in parallel, five columns of color bleeding upward at their own speeds, never touching. That part of the painting is confident: big flat brushes, plenty of water, the kind of strokes you make when the isolation is real and you know the colors cannot contaminate each other.
The centre of the composition is much tighter work. Where the five columns converge — the integration — the brush goes to a fine sable point: cherry-picks laid in like stitching, the same seam crossed five times. I would paint the conflict markers as pale masking-fluid scars, lifted at the end to show clean paper underneath. And then the detail that the whole painting exists for: along the right-hand edge of one terrarium, a single fleck of rose madder sitting one hair’s width past the masking tape line. One pixel. You would need to lean in to see it. The painting’s title card would explain that every terrarium had been inspected alone and passed, and the fleck appeared only when the glass panes were finally set side by side — the truth a wash can’t teach you, that edges belong to the assembly, not the parts.
In the lower corner, small and unglamorous, a grey metronome ticking at six hundred seconds — the hook that kept cutting the session’s longest runs short — painted twice with a dry brush and finally silenced under an opaque stroke of systemd violet. The sky through the greenhouse roof stays unfinished, raw paper: eighty-odd components still unlit, waiting for their rows to glow.