AI-Powered Hybrid Approach for Large-Scale Test Migration from Enzyme to React Testing Library

slack · Tech · 2024 · source Agentic: yes · promotional bias: low · source-backed · extracted with prompt 0.5.4 (schema v2)

Generated from extraction.json — do not edit by hand. Every claim cites verbatim source evidence; stated means the quote directly asserts the claim, implied means it supports it indirectly.

Problem

Domain: Large-scale frontend test migration

Slack needed to migrate thousands of Enzyme React tests to React Testing Library so the company could upgrade to React 18 without losing test coverage or spending more than 10,000 engineering hours on manual migration.

Constraints:

Architecture

Slack built a hybrid AST-plus-LLM conversion pipeline for Enzyme-to-React-Testing-Library migration. The system collects the source file, rendered DOM trees for test cases, and a partial deterministic AST conversion with annotations. It packages that context for Claude 2.1, parses the response, runs linters and autofix, executes the converted tests, and performs one feedback iteration with runtime logs when the first attempt fails. Successful or partially successful outputs are bucketed by pass rate for developers to review and merge manually.

Diagram confidence: explicit (topology described by the source).

flowchart LR
  migration-work-allocator["migration-work-allocator"] --> developer-review["developer-review"]
  source-test-file["source-test-file"] --> context-collector["context-collector"]
  source-test-file --> ast-codemod["ast-codemod"]
  ast-codemod --> context-collector
  dom-tree-adapter["dom-tree-adapter"] --> context-collector
  context-collector --> claude-2-1-api["claude-2-1-api"]
  claude-2-1-api --> response-parser["response-parser"]
  response-parser --> lint-and-autofix["lint-and-autofix"]
  lint-and-autofix --> test-runner["test-runner"]
  test-runner --> feedback-loop["feedback-loop"]
  feedback-loop --> claude-2-1-api
  test-runner --> success-bucketing["success-bucketing"]
  success-bucketing --> developer-review
Drag to pan · scroll or use +/− to zoom · reconstructed from the source's described component topology

Components

Component Role Interacts with
migration-work-allocator Distributes test conversion work across frontend developers and tracks progress by person, team, department, and sub-department. developer-review
source-test-file Input Enzyme test file selected for conversion to React Testing Library. context-collector, ast-codemod
context-collector Collects original file code, rendered DOM trees, partial codemod output, prompts, and annotations for the LLM request. source-test-file, dom-tree-adapter, ast-codemod, claude-2-1-api
dom-tree-adapter Renders each test case's DOM so the conversion can follow React Testing Library's user-perspective testing model. context-collector
ast-codemod Performs deterministic AST conversions for known-safe patterns and annotates unresolved conversion sites for the model. source-test-file, context-collector
claude-2-1-api LLM conversion step that consumes code, DOM, prompts, and annotated partial conversion context. context-collector, response-parser
response-parser Parses the LLM output into candidate converted test code. claude-2-1-api, lint-and-autofix
lint-and-autofix Runs linting and automatic fixes before test execution. response-parser, test-runner
test-runner Executes converted tests and produces pass/fail results and runtime logs. lint-and-autofix, feedback-loop, success-bucketing
feedback-loop Adds logs to a second dynamic prompt when the first conversion fails, constrained to one iteration for cost. test-runner, claude-2-1-api
success-bucketing Groups outputs by conversion pass rate so developers can prioritize higher-confidence conversions. test-runner, developer-review
developer-review Human sign-off and merge gate for generated conversion results. success-bucketing, migration-work-allocator

Data flow

A test file enters the migration pipeline. Context collection gathers the file code, DOM trees for each test case, and codemod output for patterns Slack can convert deterministically. The codemod performs 100%-accurate AST conversions where possible and annotates unknown conversion sites. The packaged context is sent to Claude 2.1; the response is parsed, linted, autofixed, and executed. If execution fails, logs are added to a second prompt for one feedback loop. The resulting file is bucketed by pass rate, then a developer reviews and signs off before any merge.

4 supporting quotes
“steps 1 to 3 is context collection where we get the file code. We look at the DOM tree for all of those test cases. We get the partially converted code with our codemod”article, implied
“we send the AI API request, we parse the response, we run the linters and auto-fix stuff. We actually run the code and see if it's passing”article, stated
“If it fails, we have one more feedback step where we also add the logs and dynamically create prompts based on what happened.”article, stated
“none of this code was merged to our main branch without people actually looking at the code and signing off on it.”article, stated

Why this architecture

Slack used a hybrid architecture because neither deterministic codemods nor the LLM alone captured enough context. AST rules were reliable for known transformations, DOM trees supplied user-visible context required by React Testing Library, annotations constrained hallucination, runtime execution verified generated code, and human review protected the main branch. The LLM became valuable only as one tool inside a larger preprocessing, validation, feedback, and review pipeline.

5 supporting quotes
“In the end, we have 45% success rate of code in a file. Let's say we have 10 files, 5 of them would be converted, 45% correctly”article, stated
“We got access to Anthropic model Claude 2.1, and we tried to use it to convert our tests.”article, stated
“We had 40% to 60% success rate converted in a file. There was so much variability”article, implied
“AI really requires pre-processing and post-processing. It just cannot work really well as a standalone process.”article, implied
“If we integrate this tool into the larger pipeline, that's where we see real benefits. That's what worked for us.”article, stated

Models used: Anthropic Claude 2.1

Scale

Alternatives considered (roads not taken)

Manual migration across developers

Rejected because: It was necessary initially but too slow for the full 15,500-test workload and carried a more-than-10,000-hour estimate.

“At that point we converted things manually and just investigated what to do next. Zero percent was automated.”article, stated
“overall, we estimated it would take more than 10,000 engineering hours. All those tests were written in more than 5 years by hundreds of frontend developers.”article, stated

Pure AST codemod

Rejected because: JavaScript/TypeScript variation, 65 Enzyme methods, and RTL's different testing philosophy made rule coverage insufficient.

“For each of those methods we had to create different AST rules. That's the traditional, conventional way of performing migrations.”article, stated
“This example shows that we cannot really use Enzyme code and create a certain rule that would lead us to the RTL version.”article, stated

Pure LLM conversion from Enzyme code and prompt

Rejected because: Claude 2.1 alone had high variability and only 40-60% success, so Slack did not release that tool to developers.

“We had 40% to 60% success rate converted in a file. There was so much variability”article, stated
“we didn't really even release this tool to the developers. They were using the AST one.”article, stated

Multiple feedback loops on failures

Rejected because: An extra loop improved conversion about 10-20%, but further steps were not cost-effective under API/resource constraints.

“At that time, it was quite expensive to make a lot of API calls so we only had one feedback step.”article, stated
“I believe it was about 10% to 20% better results on the conversion, but the next extra step was already not very useful.”article, stated

Parallelize conversions for speed

Rejected because: The team avoided overloading shared API endpoints and ran sequential conversions in CI, often at night.

“We did not really work on parallelization. It was one after another. Let's say you pass it a folder with 50 test files, it's going to go one by one by one.”article, stated
“for most of the conversions, we ran it at night when people don't use our endpoints, so we don't overload that one API endpoint that is used for other projects.”article, stated

Patterns

tool-use-orchestration (architecture)

Slack chained deterministic AST conversion, DOM rendering, LLM conversion, parsing, linting, test execution, feedback prompts, bucketing, and human review into one migration tool.

“we send the AI API request, we parse the response, we run the linters and auto-fix stuff. We actually run the code and see if it's passing”article, stated
“you can generate the code, you can run, you can verify, you can add any other step or any other tool to make sure that your code actually works”article, stated

prompt-chaining (workflow)

The pipeline uses an initial conversion prompt and one failure-feedback prompt augmented with runtime logs when the generated tests fail.

“If it fails, we have one more feedback step where we also add the logs and dynamically create prompts based on what happened.”article, stated
“we only had one feedback step.”article, stated

structured-output-contract (control)

Slack annotated every conversion instance and gave the model constrained suggestions so it would edit only intended regions and avoid hallucinated transformations.

“we gave the range of things that LLM should convert to, so it wouldn't be something completely random.”article, stated
“we labeled every conversion instance, and LLM knew that it should not, for example, look at the import section.”article, stated

eval-driven-development (evaluation)

The team evaluated conversions against human-converted references, ran linters and tests, and counted only executed passing tests toward impact claims.

“We compared human converted test cases for a certain number of tests, and then we generated our conversions with our tool, and we compared those two versions.”article, stated
“we only calculated cases where we knew that code was executed and it passed. We didn't really count anything else.”article, stated

human-in-the-loop-gate (control)

Generated migration output was never merged without developer inspection and sign-off.

“none of this code was merged to our main branch without people actually looking at the code and signing off on it.”article, stated

fallback-degradation (operations)

When token limits or oversized DOM context broke the first request, the retry dropped DOM-tree information to fit the model call.

“if we hit that in the first call, then in the second one, we did not include the DOM tree.”article, stated
“if this is the condition, just drop some of this information.”article, stated

cost-aware-routing (operations)

API costs and endpoint capacity constrained the system to one feedback step, sequential processing, and off-peak CI runs.

“At that time, it was quite expensive to make a lot of API calls so we only had one feedback step.”article, stated
“for most of the conversions, we ran it at night when people don't use our endpoints, so we don't overload that one API endpoint”article, stated

Tradeoffs

Use AI only where deterministic tooling was insufficient

Chose hybrid AST plus LLM conversion over LLM-first automation.

Rationale: The speaker argues that deterministic tools are preferable when available, and LLMs shine where high variability and dissimilar context exceed rule-based systems.

“avoid using AI — counterintuitive a little bit — if there is any other deterministic way to do it.”article, stated
“For our case, where we had test case code, we had HTML code, we had prompts, we had partially converted code, no other tool can consume that information.”article, stated

Limit failure feedback to one loop

Chose one log-augmented retry over longer iterative self-repair loops.

Rationale: Additional loops increased API cost and gave diminishing returns after the first retry.

“we only had one feedback step.”article, stated
“the next extra step was already not very useful. We didn't see any benefits because if the first didn't really yield any results”article, stated

Run sequentially and off-peak

Chose one-by-one CI conversions at night over parallelizing all file conversions.

Rationale: Slack prioritized endpoint stability and cost/resource limits over maximum conversion throughput.

“We did not really work on parallelization. It was one after another.”article, stated
“we ran it at night when people don't use our endpoints, so we don't overload that one API endpoint that is used for other projects.”article, stated

Distribute migration by individual developer responsibility

Chose flat direct assignments to frontend developers over strict organizational hierarchy.

Rationale: The code ownership data was imperfect, and even workload distribution reduced bottlenecks while still keeping developers close to code they had touched.

“We didn't really strictly follow the usual division by departments, by teams, by organizations. Instead, we focused more on the direct interactions with the developers.”article, stated
“We also offered very good progress tracking by person, by team, by multiple teams, by department, by sub-departments”article, stated

Gotchas (what bit them)

Rule-based AST codemods hit a combinatorial wall.

Root cause: JavaScript and TypeScript allow many ways to express the same testing logic, and Enzyme had dozens of methods with many callback and abstraction variants.

Resolution: Slack kept deterministic AST conversions for patterns it could handle perfectly and used annotations plus LLM reasoning for the unresolved cases.

3 supporting quotes
“There are so many different ways to abstract this logic.”article, stated
“We looked at how many methods were used, and there were 65 methods.”article, stated
“all of those AST conversions that we knew would result in 100% accurate conversions, we performed them, but for everything else we just added annotations.”article, stated

Pure LLM conversion lacked the same context human developers use.

Root cause: The model initially saw Enzyme code and a prompt, while humans rely on rendered DOM, React code, partial AST transformations, and frontend experience.

Resolution: The team modeled context collection after human migration behavior and supplied DOM trees, prompts, partial conversion output, and annotations.

“They got access to rendered DOM, pretty much HTML code that you see in the browser. React code, they also authored. AST conversions that we provided with our codemod”article, stated
“One thing that came up to our minds is to model this approach after humans and see if it's going to work or not.”article, stated

Code-generation claims needed conservative measurement to overcome skepticism.

Root cause: A syntactically broken file may still contain mostly converted code, but counting it would overstate actual developer time saved.

Resolution: Slack counted only test cases whose generated code executed and passed, even when that understated broader partial conversion value.

“if you have, for example, 80% converted test files, but there is a syntax error, nothing ran, we didn't really calculate that towards this goal”article, stated
“a year ago there were a lot more AI skeptics. We had to have proof for every single thing that we're doing.”article, stated

Large legacy components exceeded practical token and API limits.

Root cause: Some files generated huge requests, including combinations of source code and per-test DOM trees.

Resolution: The second attempt dropped DOM context when the first call failed due to very long requests.

“some of the components, they yield a query with 8,000 lines of code, and we were just rate limited or did not get a proper response.”article, stated
“the second loop, we just did not include that piece of information. That's how we tried to limit the number of tokens in the second call.”article, stated

Evaluation

Slack evaluated conversion quality by bucketed test sets, comparison against human-converted references, linting, autofix, executing generated tests, tracking file/test pass rates, measuring developer time saved, and adoption of the codemod versus manual conversion.

Metrics: AST-only success rate · LLM-only success rate · automatically converted code share · fully converted file rate · partially converted file rate · executed test-case pass rate · developer minutes saved per passing test · tool adoption rate · feedback-loop improvement · runtime per file

4 supporting quotes
“we took a test set of a certain amount of files, and we broke that down by easy, medium, and difficult buckets just to better represent our code base.”article, stated
“We compared human converted test cases for a certain number of tests, and then we generated our conversions with our tool, and we compared those two versions.”article, implied
“we looked at all of the test cases whose code was executed, and they either passed or failed.”article, implied
“Adoption for us was number of files that people converted with the codemod and without, and the total number of converted files.”article, stated

Outcomes

The source reports 15,500 initial tests, more than 10,000 estimated engineering hours, more than 150 developers across 50 teams, 338 files and more than 2,000 test cases in the measured run, 16% fully converted files, 24% partially converted files, 22% executed test cases passing, and two-to-four-minute per-file runtime.

4 supporting quotes
“May, we had zero. We finally got to the end of this process. We converted all the files.”article, stated
“In the end, we got to this amazing result, 80% of the code in the file was converted successfully.”article, stated
“In the end, we can say that AI codemod saved us 22% of developer time, and we have data that supports it.”article, stated
“We also open-sourced our tool on npm, since there was plenty of requests from other companies where they wanted to know how we did it”article, stated