How I Used AI Agents to Migrate Reka UI's Tests to Vitest Browser Mode Alexander Opalic created a fork of Reka UI and used AI coding agents to migrate its 97-file test suite from jsdom to Vitest Browser Mode, keeping the original jsdom tests as a comparison baseline. The migration classified files by DOM dependency, assigned one implementer and one reviewer per file, and used targeted mutations to verify tests catch real breakage. The fork, available at github.com/alexanderop/reka-ui-bench-mark, is a research project, not an upstream change. I wanted to learn Vitest Browser Mode properly. Not with a counter component. Not with five tests written specifically for a demo. I wanted a real component library with years of testing history and enough sharp edges to punish bad assumptions. So I created a working fork of Reka UI https://github.com/unovue/reka-ui and used AI coding agents to migrate its test strategy away from jsdom. The original suite had 97 test files. Eighty-seven touched the DOM and received Browser Mode counterparts. The remaining ten contained 571 DOM-free tests, so they moved to a plain Node project instead. I kept every original jsdom file as a comparison corpus. This gave the agents a live baseline rather than a memory of what the old tests used to do. The AI didn’t only translate tests. It reviewed ports, introduced deliberate bugs, ran both environments, recorded findings, and improved its own prompts after each batch. That process is the interesting part. A research fork, not an upstream migration This work lives in my Reka UI Browser Mode fork https://github.com/alexanderop/reka-ui-bench-mark/tree/browserMode . It isn’t an upstream Reka UI change. The fork exists to study the migration and preserve the comparison. ✨TLDR - →Build the migration oracle before asking agents to port files - →Give one file to one implementer and a fresh reviewer - →Keep the old suite alive so every port has a comparison target - →Use targeted mutations to prove that tests can catch real breakage - →Store prompts in Git and fix the prompt when a batch goes wrong - →Run only a few browser agents concurrently because each one launches Chromium The Wrong Way to Use AI for a Migration The tempting prompt is: Port all tests from jsdom to Vitest Browser Mode. Make the suite green. That prompt optimizes for green tests. An agent can reach green by deleting an awkward assertion. It can replace an exact query with a broader one. It can add force: true to every click or quarantine failures without understanding them. All of those ports compile. All of them can pass. The problem gets worse with parallel agents. One weak port is reviewable. Eighty-seven weak ports become a new baseline before anyone notices. So the first job wasn’t porting tests. It was building a machine that could reject bad ports. The Core Idea: Agents Propose, Machines Decide The migration became a feedback-controlled loop: The AI handled judgment-heavy work inside each step. Deterministic scripts controlled the boundaries. That distinction mattered. The agents could propose translations and investigate differences. They couldn’t redefine what “complete” meant. Step 1: Keep Both Environments Running The first commit created three Vitest projects over the same source tree: | Project | Purpose | |---|---| unit | Original jsdom tests, kept unchanged | browser | New .browser.test.ts files in Chromium | node | Tests that never needed a DOM | Running the environments side by side changed the migration from a rewrite into an experiment. For every file, I could ask: - Did the Browser Mode port keep the same test structure? - Did it preserve the assertions? - Did it reach at least the same production code? - Which suite noticed when we broke the component? Without the retained jsdom suite, those questions would become opinions. Step 2: Build an Inventory Before Assigning Work I scanned all 97 files for DOM signals such as: - Vue Test Utils and Testing Library imports document , window , and DOM constructors- accessibility audits - browser API mocks - story fixture mounts The script then classified every file by migration risk: | Tier | Meaning | |---|---| | T0 | No DOM. Move it to Node | | T1 | DOM-dependent but expected to be boring | | T2 | Mostly mechanical component tests | | T3 | Mocks browser APIs or depends on geometry | | T4 | Special patterns such as fake timers, module mocks, snapshots, or virtualization | This inventory became the progress bar and the work queue. It also prevented a wasteful mistake. Ten files didn’t need Chromium or jsdom. Moving them to Node removed the fake browser environment from 28% of the original tests. Step 3: Calibrate the System on Three Files Before the fan-out, I chose three deliberately different files. Slider: the difficult port Slider mocked ResizeObserver , scrolling, and pointer capture. It forced the first agent to solve real geometry and input problems. useForwardExpose: the boring port This composable installed no compensating mocks. Its Browser Mode version was almost identical and bought little new information. That negative result was useful. The process needed to report “this port gained nothing” without inventing a victory. Label: the mechanical port Label was small and looked easy. It still exposed several traps around click semantics, zero-width elements, and missing positive assertions. These three files produced the first version of the implementer and reviewer prompts. They gave later agents concrete precedents for difficult, boring, and mechanical work. Step 4: Give Each Implementer One File Each agent received one original test file and one output path. The assignment looked roughly like this: Port packages/core/src/{FILE} to packages/core/src/{COMPONENT}.browser.test.ts. Keep every describe and it name verbatim. Never reduce the assertion count. Don't change production source or fixtures. Keep the original jsdom file. Run only focused tests for your file. Record at least one evidence-backed finding. The narrow ownership reduced conflicts. It also made failures attributable. If a batch failed, I knew which file, prompt, and agent decision produced it. Agents weren’t allowed to fix product bugs during the port. A faithful Browser Mode test failing was considered a successful finding. The agent had to quarantine that test with it.fails and link it to a findings key: // @finding Slider/Slider.test.ts axe it.fails "should pass axe accessibility tests", async = { // Keep the original assertion strong. } This kept the suite runnable without hiding the discovery. When the bug gets fixed, it.fails becomes red because the expected failure disappears. Step 5: Review with a Fresh Context The implementer never reviewed its own port. A second agent received the original and the Browser Mode version. It didn’t initially receive the implementer’s reasoning or findings. Its task was adversarial: Assume this port is weaker than the original. Find out how. The reviewer searched for: - exact assertions replaced by broad ones - lazy locators that were never resolved - retrying assertions that widened timing contracts - new sleeps hiding synchronization problems - role queries replacing tag assertions - forced interactions that bypassed the behavior under test - quarantines swallowing unrelated assertions This separation worked because implementation and review reward different behavior. The implementer wants completion. The reviewer wants a counterexample. The machine checked structure. The reviewer checked meaning. Step 6: Make Parity Machine-Enforced Every port had to pass five focused commands: pnpm --filter reka-ui exec vitest run \ --project=browser