Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

AI System Design

Design a CI Test-Generation Agent

Step 1 / 9

Learn AI system design by building an agent that automatically generates tests for new and changed code in a CI pipeline.

The numbers to beatmutant introduceda deliberate small bugtest must FAILagainst the mutant to prove valuefails closedunproven tests never merge

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

A test that always passes is worse than no test

Generating a test is trivial — an LLM can produce SOMETHING that looks like a test in seconds. Generating a test that actually catches real bugs, doesn't flake, and doesn't just lock in whatever the code currently does (bugs included) is the genuinely hard problem.

Developerpushes code
New in this step: Developer.

We'll build the whole validation pipeline a generated test needs to earn trust: coverage targeting, mutation testing to prove it's meaningful, flakiness checking, and a human review gate that catches what even mutation testing can't.

What the new pieces do

Developerclient
Ships code under real deadline pressure, where comprehensive manual test-writing is often the first thing to get shortchanged.

Step 1 · The baseline

Manual tests only

Simplest version: developers write all tests themselves, as part of normal development. What happens under real deadline pressure?

Developerpushes codeCI Pipelineruns on every push
New in this step: CI Pipeline.

Tests are written entirely manually, alongside normal feature work under deadline pressure. What tends to happen?

  1. In practice, thorough test-writing is one of the first things to get shortchanged under real deadline pressure — not because developers don't value it, but because it competes directly with shipping the feature itself, and shipping usually wins under a deadline.

  2. Manual testing effort is finite and competes with feature work — under pressure, coverage gaps are a predictable, common outcome, and those gaps are exactly where undetected bugs tend to live.

  3. Coverage gaps from time pressure occur at any codebase size — the underlying dynamic (testing effort competing with feature work under deadline pressure) doesn't depend on scale.

Manual testing effort competes directly with feature work, and under real deadline pressure, coverage gaps on new and changed code are a predictable, common outcome — exactly where undetected bugs tend to hide.

Why this piece earns its place

The baseline step is also the deployment decision, and it is easy to skim past: everything downstream lives inside CI rather than in an editor plugin or a nightly cron. A generated test is worth nothing until it has been executed, and executing it needs a checked-out tree, installed dependencies and a working test command pinned to one commit. CI is the only place that already has all three, reproducibly, for the exact code under discussion — rebuilding that environment anywhere else is most of the work of building CI again. What that binds you to, though, is a toolchain rather than a codebase, and that is the second-order cost nobody prices in. Coverage instrumentation, the test runner, the assertion conventions and later the mutation operators are every one of them language-specific. A repo with a Python service behind a TypeScript front end is two builds of this agent, not one with a flag, and the second language costs close to what the first did. Work that out before you start, because it usually decides that the honest scope is the one stack carrying most of the risk rather than the whole repo. Where you would skip the design entirely: a repo with no CI. There the first deliverable is CI, and this conversation is a quarter away.

What the new pieces do

CI Pipelinegateway
Runs on every push — the fast feedback loop developers wait on, which nothing in this design should be allowed to slow down significantly.

Step 2 · Find the gaps precisely

Target generation, don't duplicate coverage

Before generating anything, figure out exactly what's NOT already covered.

CI Pipelineruns on every pushCoverage-Gap Analyzertargets generation
New in this step: Coverage-Gap Analyzer.

Before generating tests, what should determine where generation effort goes?

  1. Generating redundant tests for already well-covered code wastes generation effort and adds maintenance burden for no real coverage benefit — the goal is filling GAPS, not maximizing raw test count.

  2. Precise coverage-gap analysis means every generated test adds genuinely NEW coverage rather than duplicating what already exists — the most efficient use of generation effort, focused exactly where the real risk (untested code paths) actually lives.

  3. Relying on developers to manually flag which functions need generated tests reintroduces the same time-pressure problem from Step 1 — remembering to flag things under deadline pressure is exactly the kind of extra step that gets skipped.

Add a Coverage-Gap Analyzer that identifies exactly which lines and branches of new or changed code lack test coverage, and targets generation precisely at those gaps — rather than generating redundant tests for already-covered code.

Why this piece earns its place

Gap analysis is the component whose degradation is invisible from the outside. If the coverage report comes back empty — an instrumentation flag dropped during a build refactor, a runner upgraded, a language the tool cannot parse — the analyzer finds no gaps, the generator proposes nothing, and every dashboard stays green. Nothing fails. The agent just quietly stops working, and someone notices weeks later that no proposals have arrived. So the alarm that matters is on throughput, not errors: gaps found per merged change, proposals produced per day. Two details worth naming out loud. Branch coverage and line coverage disagree, and the disagreement is where the interesting gaps hide — a line inside a conditional can be fully covered while one side of that condition never runs once. And the definition of changed code is a per-request knob rather than a config constant: the diff against the merge base is what a reviewer cares about, the diff against the previous push is what a developer iterating on a branch cares about, and fixing one globally serves the other badly.

What the new pieces do

Coverage-Gap Analyzerservice
Identifies exactly which lines and branches of new/changed code have no test coverage — so generation effort is spent precisely, not redundantly.

Step 3 · Generate against the real signature

A specific test, not generic boilerplate

Generate a test for an identified gap. Should it be a generic template, or something tailored to the actual code?

Coverage-Gap Analyzertargets generationTest Generatortargets the gap
New in this step: Test Generator.

Generating a test for a specific coverage gap — generic boilerplate, or something more targeted?

  1. Generic boilerplate rarely exercises the SPECIFIC logic of the actual uncovered code path — it might technically execute the function without meaningfully testing its real behavior or edge cases.

  2. Generating directly against the real function — its actual parameters, return behavior, and the specific gap identified — produces a test that genuinely exercises the logic in question, not a generic template that happens to call the function without testing anything meaningful about it.

  3. Merely checking for the absence of an exception is a very weak test — it wouldn't catch a function that runs without crashing but produces the WRONG result, which is a common and important class of bug.

The Test Generator produces a test targeting the specific identified gap, built against the function's actual signature and behavior — exercising the real uncovered path, not a generic template that merely calls the function without meaningfully testing it.

Why this piece earns its place

The signature is the smallest part of what the generator needs. It also needs the module's imports, the fixtures the repo already has, the framework and assertion style in use, and where tests for this module live. Skip that and you get tests that are individually reasonable and collectively foreign — a second set of helpers, a second mocking style, a second way to build the same object. Nobody rejects those at review — each looks fine alone — and the suite grows a dialect only the agent speaks. Two failure counters are worth keeping apart, because they point at different fixes and only one is legible here. A candidate that does not import or compile fails immediately and locally: the generator was not shown enough of the repo, and context selection changes on the next run. A candidate that compiles, runs and asserts nothing looks exactly like a good one from here — nothing at generation time tells them apart, which is the job Step 4 exists to do. So attribute that verdict backwards: a mutant left alive is a fact about the generator, not only about the test it came back on, and filing it purely as a validation-stage rejection loses the one number saying the prompt or the model needs changing. Collapsing both into one bad-generation rate hides which lever to pull, and the levers cost wildly different amounts.

What the new pieces do

Test Generatorservice
Generates a test exercising the specific uncovered path, using the actual function's real signature and behavior.

Step 4 · Prove the test is meaningful

Mutation testing: does it actually catch bugs?

A generated test passes. That alone doesn't prove much — a test that asserts nothing meaningful, or asserts something trivially true, also "passes." How do you verify a generated test actually catches real bugs?

Test Generatortargets the gapMutation Testerfails closed
New in this step: Mutation Tester.

A generated test passes. How do you verify it's actually meaningful, not just trivially passing?

  1. A test can pass for the wrong reason entirely — asserting something that's always true regardless of the code's actual behavior, or not really exercising the logic at all — "passes" alone says nothing about whether the test would actually catch a real bug.

  2. Mutation testing directly answers the question that matters: "would this test actually catch a real bug?" By deliberately breaking the code in a small way and checking the test fails, you get concrete proof the test is sensitive to the actual logic — not just passing by coincidence or triviality.

  3. A second model's subjective assessment of whether a test "looks right" doesn't provide the same concrete, verifiable proof that mutation testing does — mutation testing gives an objective, falsifiable answer (does the test actually fail on broken code), not a judgment call.

Validate every generated test with mutation testing: introduce a small, deliberate bug into the code and confirm the test fails against that mutant. A test that still passes against clearly-broken code isn't testing anything meaningful and is discarded or sent back for regeneration — this fails closed, so nothing unproven reaches the test suite.

Why this piece earns its place

One ordering mistake makes the whole check worthless. Run the candidate against the unmutated code first and require it to pass. A test that fails to import, or that was simply broken on arrival, also fails against the mutant, and a naive check reads that as proof of sensitivity. The two runs are halves of one check, not alternatives. The operator set is the other thing to choose deliberately, because it is the closest this design comes to writing down what meaningful means. Deleting a statement is caught by nearly any test that touches the line; flipping a comparison or moving a boundary is caught only by one that checks the result. Tighten the operators later and you have silently raised the bar on everything admitted before, so store the standard a test passed beside the test. Two more things you only learn by running it. Equivalent mutants — edits that change no behavior — can never be killed, so counting them as rejections discards good tests. And a kill is a single execution, taken before Step 5 has established the candidate is even stable, so record which mutant died: that is what re-proves the verdict if the test later turns out to be unreliable.

  • mutant introduceda deliberate small bug
  • test must FAILagainst the mutant to prove value
  • fails closedunproven tests never merge

What the new pieces do

Mutation Testerservice
Introduces a small deliberate bug into the code and confirms the generated test actually fails against it — proof the test is meaningful, not trivially passing.

Step 5 · Catch flakiness before it poisons CI

Inconsistent results are worse than no test

A test passed mutation testing — it genuinely exercises real logic. But does it produce the SAME result every time it runs?

Test Generatortargets the gapMutation Testerfails closedFlakiness Checkerrepeated runs
New in this step: Flakiness Checker.

A generated test passed mutation testing. Is that sufficient to trust it in the CI suite?

  1. Mutation testing proves the test is sensitive to real bugs, but says nothing about whether it produces CONSISTENT results — a test could be meaningful and still be flaky (timing-dependent, order-dependent), which is a separate failure mode entirely.

  2. A flaky test — one that passes and fails inconsistently for reasons unrelated to actual code correctness — is arguably worse than no test at all, because it trains developers to distrust and ignore CI failures ("just rerun it"), which erodes confidence in the entire test suite, not just that one test.

  3. Generated tests can be just as prone to flakiness (timing assumptions, shared state, ordering dependencies) as manually-written ones — there's no reason to assume generation exempts a test from this risk.

Run every newly mutation-validated test through a Flakiness Checker — repeated executions to confirm consistent results — before it's trusted in the suite. A test that produces inconsistent results, even one that's otherwise meaningful, is discarded or flagged rather than allowed to poison CI signal.

Why this piece earns its place

Repeated runs are a filter, not a proof. Anything that misbehaves rarely enough walks through whatever batch you can afford and surfaces much later, in an unrelated pull request, as a red build nobody can explain. That is the consequence worth planning for: this check needs a partner inside the suite — a quarantine path that can demote a merged test which starts misbehaving, and attribute it back to the run that produced it. Without that edge the checker never learns what it let past, and its run budget stays a guess. How you spend that budget matters more than how large it is. Most instability comes from shared state, contention, and the order the suite happens to execute in, none of which shows up when one test runs alone, over and over, on an idle machine. Varying the conditions finds more than repeating them: shuffle the order, run it alongside the rest of the suite, run it under parallelism. And handle one case carefully — sometimes the test is fine and the code is racy. Then the inconsistency is a real defect this agent just found, and a silent discard throws away the most valuable thing it has produced. Discard by default, but never into a counter nobody reads.

What the new pieces do

Flakiness Checkerservice
Runs a newly-validated test multiple times before trusting it — a test with inconsistent results is worse than no test at all.

Step 6 · Human review — for a different reason than mutation testing

Meaningful isn't the same as correct

A test passed mutation testing (it's sensitive to real bugs) AND the flakiness check (it's consistent). Is it now safe to auto-merge without any human involvement?

Human Review Gatechecks intentTest Suiteaccepted tests
New in this step: Human Review Gate, Test Suite.

Route mutation-tested, non-flaky generated tests through a Human Review Gate before they join the suite — because mutation testing proves a test is non-trivial, not that its assertion reflects correct intended behavior. A test generated against code that's subtly buggy could pass mutation testing (it genuinely fails on further mutation) while still asserting the WRONG expected outcome, effectively locking in a bug as if it were correct behavior. A developer's review catches exactly this gap that automated validation structurally can't.

Why this piece earns its place

A review gate is a queue, and queues that grow get rubber-stamped, which converts the gate into a formality that still charges full price in delay. So the design question is not whether to review, it is what one review costs. Route each proposal to the author of the code it covers, while the intent is still in their head, and keep the unit small — one test, one decision, not a batch handed over at the end of a sprint. Show them the mutant that test killed beside the diff: that is evidence they cannot cheaply reconstruct, and it moves the question from is this a good test, which invites a slow reread of the whole function, to is this the behavior I meant, which is the only thing a human is here for. What teams miss is that this gate is also the admission policy for the suite. An accepted test is a permanent cost — it runs on every future push, and from then on every change touching that code has to satisfy it or argue with it. So the reviewer is really answering two questions, and the second one, is this worth paying for indefinitely, is what keeps a generated suite a signal rather than a tax. Which means the reject has to stay cheap and unembarrassing to use, or the first question quietly becomes the only one.

What the new pieces do

Human Review Gateservice
A developer sanity-checks that a generated, mutation-proven test actually reflects CORRECT intended behavior, not just that it isn't trivial.
Test Suitedata
The durable, trusted set of tests that actually gates future changes — every entry has survived generation, mutation testing, flakiness checking, and human review.

Step 7 · Keep the fast lane fast

Expensive validation shouldn't block every push

Generation, mutation testing, and flakiness checking are all relatively expensive and slow. Should they run in the same fast lane developers wait on for every push?

CI PipelineTest GeneratorMutation TesterHuman Review GateTest Suite
New in this step: CI Pipeline → Test Suite. · swipe to pan the diagram

Run test generation and its validation pipeline in a separate, asynchronous lane — the fast primary CI feedback loop developers wait on for every push stays fast and unaffected, while generation and mutation/flakiness validation happen in the background, with results (a proposed new test, pending review) surfacing once ready rather than blocking anything in the interactive path.

Why this piece earns its place

Moving work off the critical path buys latency and costs freshness. By the time a proposal is ready the branch has usually moved, and a test written against code that has since been rewritten spends a reviewer on a decision that no longer applies — which is how a team learns to skim every proposal that follows. So a proposal carries the commit it was generated against, and on arrival the lane checks whether the targeted lines still exist in that form: revalidate, or discard silently. Never surface a stale one. Two consequences are easy to miss. This lane has no verdict to give the push that triggered it, so its output cannot be a CI status check at all — a proposal is a change, and it needs somewhere to land as one, which is also what gives Step 6's reviewer something to approve or close. And separate lane is a scheduling claim, not a topology one. Where both lanes draw executors from the same runner pool, a burst of pushes puts generation work in front of the loop you promised not to slow, so give it its own capacity or a strictly lower priority class, and bound the backlog rather than let it grow.

Step 8 · The sharp edges

Don't lock in a bug, and prefer testing behavior over implementation

Two subtler risks: generating a test against code that's CURRENTLY buggy would validate the bug as expected behavior, and tests tightly coupled to internal implementation details break on every refactor even when the actual behavior is unchanged.

DeveloperCI PipelineCoverage-Gap AnalyzerTest GeneratorMutation TesterFlakiness CheckerHuman Review GateTest Suite
The system as it stands at this step. · swipe to pan the diagram

Anchor generation to some notion of intent — a docstring, a spec, a PR description — rather than purely "whatever the current code happens to do," so a generated test doesn't simply codify an existing bug as if it were correct. And prefer generating tests against a function or module's public behavior and interface rather than internal implementation details, so tests remain valid across refactors that preserve behavior but change internals — reducing long-term maintenance burden on the generated suite.

Why this piece earns its place

Intent sources are not interchangeable, and ranking them is worth doing out loud. A ticket or a spec is written before the code and usually by someone other than its author, so it can genuinely contradict the implementation — which is exactly what makes it useful. A docstring sits next to the code and rots with it. A pull request description is written by the same person who just wrote the bug, describing what they believe they built: closest to the change, weakest as an independent check. The useful behavior when intent and code disagree is not to generate a test at all. It is to say so. An agent that flags the contradiction is worth more than one that quietly picks a side, and picking the code is how a bug gets frozen into the suite as a requirement. The public-versus-internal rule needs something computable behind it too — exported symbols, a package boundary, whatever the language actually enforces — because where nothing enforces a boundary, every function looks public. And mark generated tests as generated. It is one annotation, and it is what makes them attributable later and cheap to regenerate in bulk once the generator improves.

You did it

You just designed a CI test-generation agent.

DeveloperCI PipelineCoverage-Gap AnalyzerTest GeneratorMutation TesterFlakiness CheckerHuman Review GateTest Suite
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Manual testing effort competes with feature delivery, and coverage gaps are a predictable outcome under real deadline pressure.
  • Coverage-gap analysis targets generation precisely at what's genuinely uncovered, not redundantly at what's already tested.
  • Tests are generated against the real function signature and behavior, exercising the specific identified gap.
  • Mutation testing proves a generated test is meaningful — actually sensitive to real bugs — not just trivially passing.
  • A flakiness check catches inconsistent test results before they poison CI trust, which a flaky test does disproportionately.
  • A human review gate catches what mutation testing structurally can't: whether a test's assertion reflects genuinely correct intended behavior, not just non-trivial behavior.
  • Generation and its expensive validation run in an async lane, keeping the fast primary CI feedback loop unaffected.
  • Generation anchored to intent (not just current behavior) avoids locking in existing bugs, and testing public behavior over internals reduces refactor-driven maintenance burden.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why is mutation testing specifically the right validation technique here, rather than just checking code coverage percentage?

    Coverage percentage measures whether a line of code was EXECUTED during a test run — it says nothing about whether the test actually verified anything meaningful about what that code did. A test can achieve 100% line coverage while asserting nothing (or asserting something trivially true) about the code's actual behavior, technically satisfying a coverage metric while providing zero real protection against regressions. Mutation testing measures something fundamentally different and more useful: would this test actually CATCH a bug if one were introduced — which is the property that actually matters for a test's real value.

  2. How is the human review gate here different from the review gate in the Prompt Management platform design?

    Both exist because automated validation alone leaves a specific gap only human judgment can fill, but the gaps are different in kind. The Prompt Management review gate catches risky EDITS (an accidentally-removed safety instruction) that a non-engineer editor might not recognize as risky. This design's review gate catches a subtler correctness gap: a test that's PROVEN non-trivial (via mutation testing) but might still be asserting the wrong expected behavior if the underlying code itself was already buggy — mutation testing can prove sensitivity to bugs, but can't independently verify what the CORRECT behavior actually was supposed to be. Different failure mode, same underlying lesson: automated gates catch what they're specifically designed to catch, and human review fills the remaining gap.

  3. Could the mutation-testing step itself become a performance bottleneck for large codebases with many generated tests?

    Yes — mutation testing is inherently expensive because it requires re-running the test suite (or at least the relevant tests) against each introduced mutant, and thoroughness scales with the number of mutants tried. This is exactly why Step 7 puts generation and validation in an asynchronous background lane rather than the fast CI path — the expense is real, but it doesn't need to block anyone's immediate feedback loop. Practical systems also often limit mutation testing to a representative sample of mutation operators rather than exhaustively trying every possible small code change, trading some thoroughness for tractable validation time.

  4. Why generate tests against public behavior rather than internal implementation details — isn't testing internals sometimes valuable?

    Testing internal implementation details isn't inherently wrong, but it creates tests that break whenever internals change EVEN IF the actual observable behavior (what callers experience) hasn't changed at all — a refactor that improves code structure without altering behavior shouldn't require touching the test suite, but implementation-coupled tests force exactly that. Preferring tests anchored to public behavior means the suite validates what actually matters to users of the code (does this function still do what it's supposed to do) while staying resilient to internal refactoring — reducing the long-term maintenance tax a large generated suite would otherwise accumulate.

  5. What signal would indicate the test-generation agent itself needs re-evaluation or retuning over time?

    A few concrete signals: a rising rate of generated tests failing mutation testing (suggesting the generator is producing weaker, less-targeted tests than before, perhaps due to a model or prompt change), a rising rate of tests rejected at human review specifically for asserting incorrect behavior (suggesting the intent-anchoring in Step 8 isn't working well), or — the most direct signal — real bugs slipping through in areas the generated suite was supposed to cover, which would indicate the whole pipeline's actual real-world effectiveness, not just its internal validation metrics, needs scrutiny. This mirrors the ongoing drift-monitoring principle from the Model Registry design: validation at generation time doesn't guarantee the system stays effective indefinitely without ongoing measurement.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Manual-only testing tends to leave coverage gaps because…

    This is a structural time-pressure dynamic, not a discipline problem — automation can genuinely help.

  2. Mutation testing validates a generated test by…

    This gives concrete, falsifiable proof the test is sensitive to real bugs, not just trivially passing.

  3. A flaky generated test is caught before merging because…

    A test with inconsistent results is arguably worse than no test — it trains developers to ignore failures.

  4. A human review gate is still needed even after mutation testing because…

    A test could pass mutation testing while still checking for the wrong expected outcome if the underlying code was already buggy.

  5. Test generation and validation run in an async background lane because…

    Separating latency-sensitive from latency-tolerant work keeps the interactive developer feedback loop fast.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Find gaps: identify exactly which lines and branches of new or changed code lack coverage.
  • Generate: produce a test exercising the specific uncovered path against the function’s real signature.
  • Prove it: mutation-test each generated test — it must fail against a deliberately-broken version.
  • Check flakiness: run a validated test repeatedly to confirm consistent results before trusting it.
  • Gate & merge: a human reviews intent before an accepted test joins the suite.

The qualities that shape everything

Each one names the mechanism that buys it.

Generation effort spent precisely
A coverage-gap analyzer targets exactly the lines/branches of new or changed code with no coverage, so every generated test adds new coverage, not duplicates.
The test actually exercises real logic
Generate against the function’s actual signature and observed behavior, aimed at the specific uncovered path — not a generic template that merely calls it.
Only meaningful tests reach the suite
Mutation testing introduces a deliberate bug and requires the test to fail against it; it fails closed, so nothing unproven merges.
No flaky test poisons CI signal
A flakiness checker runs a newly-validated test repeatedly and discards or flags inconsistent ones before they reach the suite.
Catch wrong-but-non-trivial assertions
A human review gate verifies the assertion reflects correct intended behavior — the gap mutation testing structurally can’t close.
The fast CI feedback loop stays fast
Generation and its expensive validation run in a separate async lane, surfacing a proposed test once ready instead of blocking every push.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Targeting the uncovered path over generating a test for every function

Blanket generation duplicates coverage that already exists and piles on maintenance for no benefit. Precise gap analysis means every generated test adds genuinely new coverage exactly where the real risk lives.

Generating against the real signature over a generic boilerplate template

Boilerplate might execute the function without meaningfully testing its behavior or edge cases. Building the test against the actual parameters, return behavior and the identified gap is what makes it exercise real logic.

Mutation testing over trusting that a passing test is meaningful

A test can pass by asserting something trivially true or not exercising the logic at all. Deliberately breaking the code and confirming the test fails gives objective, falsifiable proof it would catch a real bug — which “it passes” never can.

A flakiness check over trusting a mutation-proven test outright

Mutation testing proves sensitivity to bugs but says nothing about consistency. A flaky test is arguably worse than no test because it trains developers to ignore CI failures, so run it repeatedly before trusting it.

A human review gate over auto-merging mutation-proven tests

Mutation testing proves a test is non-trivial, not that its assertion reflects correct intended behavior — a test generated against subtly-buggy code can pass mutation testing while locking in the bug. Human review catches exactly that gap.

The answer, out loud

What a strong answer to “Design a CI Test-Generation Agent” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–4 min

    Name the hard half

    I'd set the frame before drawing anything, because generation isn't the interesting constraint here. What we ship isn't text, it's a gate — every test that joins the suite gets a vote on whether future changes are allowed to merge. So I'd hold a candidate to the bar I'd hold a stranger's pull request to, and ask what evidence earns it that vote. Three separate questions: does it catch real bugs, does it answer the same way every run, and does it assert what the code was meant to do rather than what it happens to do. None of them is answered by the test passing. So most of this design is the evidence chain.

  2. 4–10 min

    Start from the baseline

    I'd start with the situation we're replacing. Developers write the tests themselves, alongside the feature work, and under a real deadline the thorough test-writing is the first thing to get shortchanged. That isn't a discipline problem — testing effort competes directly with shipping the feature, and shipping wins. So coverage gaps accumulate on exactly the new and changed code where undetected bugs live. That's the gap I'm automating into, and the developer's push into CI is the event the whole design hangs off.

    Built in step 1: Manual tests only
  3. 10–16 min

    Aim before firing

    The first thing I'd add isn't a generator, it's a coverage-gap analyzer. If you generate a test for every function you duplicate coverage that already exists and hand people a maintenance bill for no benefit. I want to know exactly which lines and branches of the new or changed code have no coverage, and point generation at those, so every test we produce adds coverage that genuinely wasn't there. I wouldn't ask developers to flag what needs tests either — that reintroduces the same deadline problem from the baseline, because remembering to flag something is precisely what gets dropped when you're behind.

    Built in step 2: Target generation, don't duplicate coverage
  4. 16–22 min

    Generate against the real function

    Then the generator. What I'd insist on is that it builds against the function's actual signature and observed behavior, aimed at the specific uncovered path — not a boilerplate template filled in minimally. And not a test that only checks the function doesn't throw: the absence of an exception is close to a non-test, because it won't catch a function that runs perfectly happily and returns the wrong answer, which is the common and expensive class of bug. The value of a generated test is entirely in how precisely it exercises the real logic.

    Built in step 3: A specific test, not generic boilerplate
  5. 22–30 min

    Make the test prove itself

    This is the part I'd spend the most time on, because it's what makes the design defensible. A generated test passes — that tells me almost nothing. It could be asserting something trivially true, or not exercising the logic at all. So I'd validate every candidate with mutation testing: introduce a small deliberate bug into the code and require the test to fail against it. If it still passes against clearly broken code, it isn't testing anything and it goes back for regeneration. That's objective and falsifiable in a way that it passes, or a second model's opinion that the test looks correct, never is. And I'd have it fail closed — if the mutation tester is down, generation keeps running but nothing unproven joins the suite.

    Built in step 4: Mutation testing: does it actually catch bugs?
  6. 30–36 min

    Consistency is a separate question

    Mutation testing proves sensitivity to bugs. It says nothing about consistency, so a test can be genuinely meaningful and still be flaky — timing-dependent, order-dependent. I'd run every newly validated test repeatedly before trusting it, and discard or flag anything that comes back inconsistent. I treat a flaky test as worse than no test, because it teaches people to rerun a red build instead of reading it, and that scepticism doesn't stay contained to the one test. It spreads to how the entire CI signal gets read.

    Built in step 5: Inconsistent results are worse than no test
  7. 36–42 min

    The gap automation cannot close

    Even past both checks I wouldn't auto-merge. Mutation testing proves a test is non-trivial; it can't prove the assertion is right. If the code under test was already subtly buggy, a generated test can pass mutation testing and still lock that bug in as expected behavior — it fails on further mutation perfectly well, it's just asserting the wrong outcome. Nothing automated closes that, because closing it means knowing what the behavior was supposed to be, which isn't in the code. So a human reviews intent, and only then does the test join the suite.

    Built in step 6: Meaningful isn't the same as correct
  8. 42–48 min

    Keep the fast lane fast

    All of that is slow — generation, the mutation runs, the repeated flakiness runs. None of it belongs in the loop a developer is sitting there waiting on. I'd run the whole thing in a separate asynchronous lane and surface a proposed test, pending review, once it's ready. The primary CI signal on every push stays exactly as fast as it was. It's the same split you make anywhere else: latency-sensitive work and latency-tolerant work don't share a budget.

    Built in step 7: Expensive validation shouldn't block every push
  9. 48–52 min

    Close on the trade-off

    The trade-off I'd name is that this design buys trust with throughput. A chain of gates stands between a generated test and the suite, every one of them can reject, and the human gate adds real delay — so we ship far fewer tests than the model could write, and that's deliberate. The two sharp edges I've built for are already in there: intent-anchored generation, and testing public behavior. What I haven't designed is the other direction. This pipeline only ever adds — nothing retires a generated test that has stopped earning its place, and a suite that only grows gets deleted in bulk one day by someone with no time to read it. With more time I'd also have the analyzer rank gaps rather than list them. Right now every uncovered branch looks equally worth a test, and I'd want the ones in code that changes often to go first.

What this teaches

Learn AI system design by building an agent that automatically generates tests for new and changed code in a CI pipeline. An interactive guide covering why manually-written tests leave coverage gaps under deadline pressure, coverage-gap analysis to target generation precisely, generating tests against the specific uncovered path, mutation testing to verify a generated test is actually meaningful (not trivially passing), flaky-test detection before it poisons CI signal, why a human review gate is still required even after mutation testing, and running expensive validation in a background lane without slowing the developer feedback loop.

Key takeaways

  • Manual testing effort competes with feature delivery, and coverage gaps are a predictable outcome under real deadline pressure.
  • Coverage-gap analysis targets generation precisely at what's genuinely uncovered, not redundantly at what's already tested.
  • Tests are generated against the real function signature and behavior, exercising the specific identified gap.
  • Mutation testing proves a generated test is meaningful — actually sensitive to real bugs — not just trivially passing.
  • A flakiness check catches inconsistent test results before they poison CI trust, which a flaky test does disproportionately.
  • A human review gate catches what mutation testing structurally can't: whether a test's assertion reflects genuinely correct intended behavior, not just non-trivial behavior.
  • Generation and its expensive validation run in an async lane, keeping the fast primary CI feedback loop unaffected.
  • Generation anchored to intent (not just current behavior) avoids locking in existing bugs, and testing public behavior over internals reduces refactor-driven maintenance burden.

Concepts covered

  • A test that always passes is worse than no test
  • Manual tests only
  • Target generation, don't duplicate coverage
  • A specific test, not generic boilerplate
  • Mutation testing: does it actually catch bugs?
  • Inconsistent results are worse than no test
  • Meaningful isn't the same as correct
  • Expensive validation shouldn't block every push
  • Don't lock in a bug, and prefer testing behavior over implementation
RUN IT YOURSELF

The suite with 100% coverage that catches nothing

Real mutation testing, in your browser: the function under test is parsed with ast, one operator is applied per site, and each mutant is recompiled and run against three suites — one that executes every line and asserts almost nothing, one that runs fewer lines and checks exact values, and one that was simply wrong on arrival. Every row is gated on a run against the unmutated code first, so the table shows which scores count and which are rejected. Change OPERATORS, hit Run, and watch the bar for "meaningful" move.

HOW TO READ THE CODE — 6 IDEAS
  1. The high-coverage suite runs 11/11 lines and kills 3 of 21 mutants. Coverage proves a line ran, not that anyone checked the result (step 2).
  2. The real-assertion suite runs fewer lines and kills more: the two numbers rank the suites in opposite orders, which is the whole argument for step 4.
  3. Its ceiling is set by coverage — a mutant on a line no test executes always survives — so mutation score is coverage’s successor, not its rival.
  4. The on original column is the other half of the check: the broken suite kills 21/21 and its (100%) is bracketed and rejected, because a suite that fails against everything fails against the original too (step 4).
  5. The operator set is the definition of meaningful: with only aor enabled both suites tie at 25% and the check stops telling them apart (step 4).
  6. Two mutants are equivalent — the dead store on line 2, and nothing else on any other line — so they leave the denominator: 21, not 23.
CPython · WebAssembly
built so a test that always passes never sneaks into the suite — make the calls, generate a flaky test, run the gauntlet.
Finished this one? 0 / 65 AI System Designs done
NextDesign a Conversational AI Closest AI system design to this one

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs