A new harness runtime runs beside the old one while cases migrate — what must be true for both to stay green?
answer
- Two front doors, one house
- Behaviour lives below both runners
- Same deployed target, same data story
- One result store, tagged by runtime
- Overlap needs a deadline and a burndown
basics
~20 sBoth runtimes must call one shared implementation of the business actions and report into a single result store. Only the entry point differs. Copy the behaviour instead of sharing it and the two halves drift apart silently.
solid answer
~50 sTreat the second runtime as a **new front door onto the same house**. Extract the business actions, data setup and assertion helpers into a layer neither runner owns, then let each runner's case entry points call into it, so a fix lands once and both see it. Keep one deployed target and one result store, with each outcome tagged by runtime, so a divergence shows up as *the same case passing on one side and failing on the other* rather than as a mystery. Cap the overlap: move a case and delete its old copy in the same change. Publish a deadline and a burndown, because a side-by-side with no end date becomes two suites maintained forever. Gate merges on both runtimes until the last case moves, then delete the old runner and its configuration outright.
code
pseudocode · 18 lines# shared behaviour layer - owned by neither runner
action place_order(actor, item):
http_post("/orders", { actor: actor, item: item })
return wait_until(order_visible(actor, item), deadline = 30s)
# old runner: its own discovery style, thin entry point
old_case "order appears in history":
actor = new_actor()
place_order(actor, "widget")
expect(history_of(actor)).contains("widget")
# new runner: different discovery, identical body
new_case("order appears in history", tags = ["migrated"]):
actor = new_actor()
place_order(actor, "widget")
expect(history_of(actor)).contains("widget")
# a fix to place_order reaches both. a copied body would not.go deeper
Be ready to say what a harness runtime is — the machinery that discovers, schedules and reports cases — and why the behaviour a case encodes is worth far more than the runner it happens to sit on.
Explain the mechanics: lift business actions into a layer neither runner owns, have both entry points call it, and keep one deployed target and one result store so a divergence is visible rather than mysterious.
Show you have run one. Talk about separating a migration defect from a product defect, capping the overlap with a published burndown, and gating merges on both runtimes until the last case moves.
Own the end date. Price the overlap honestly as doubled run cost and two upkeep surfaces, and say in advance what evidence would make you stop the migration rather than quietly extend it.
## What a harness runtime is, and what it is not A **harness runtime** is the machinery a case executes on: the component that discovers cases, schedules them, fires setup and teardown at defined points, decides the unit of parallelism, and writes outcomes somewhere. It is not the suite. The suite is the *behaviour* the cases encode — the business actions they perform against the deployed system and the outcomes they assert. That distinction is the entire migration strategy: you replace machinery while preserving behaviour, and every decision below follows from protecting the part that took years to accumulate. Running a new runtime **beside** the old one is the strangler shape applied to your own test code. Both live in the same repository, both run in the same pipeline, both drive the same deployed target, and cases move in batches. At any moment the pack is part old and part new, and what makes that survivable is that a reader can still answer one question when something goes red: *is the product broken, or is the migration broken?* ## Four invariants that keep both sides green 1. **One implementation of behaviour, two entry points.** Before a single case moves, lift the business actions, the data setup and the assertion helpers into a layer that neither runner owns. A case in either runtime becomes a thin entry point that calls into that layer. A fix then lands once and both runtimes see it. The alternative — copying each case and editing both from then on — diverges at the first behaviour change and doubles the upkeep you were trying to reduce. 2. **One deployed target and one data story.** Both runtimes drive the same running system and create their data the same way. Give the new runtime its own target and every divergence acquires a second candidate explanation, so you can no longer read a split as evidence about the migration. 3. **One result store.** Both runners report into the same place, each outcome tagged with which runtime produced it. A divergence then presents itself as a searchable fact — the same case passing on one side and failing on the other — instead of as two reports nobody compares. 4. **One green bar.** Merges gate on both runtimes for as long as both exist. The moment the old runner's outcome stops blocking a merge it stops being maintained, and the fallback you are paying for quietly evaporates. | Choice during the overlap | What it buys | What it costs | | --- | --- | --- | | Shared behaviour layer, thin entry points | One fix reaches both runtimes; divergence means the runtime, not the code | An extraction pass before any case moves | | Copied case bodies per runtime | Nothing to design up front; first cases move immediately | Every later fix applied twice; silent drift between halves | | Single deployed target | A split is attributable to the runtime | Contention between the two runs on shared data | | Separate targets per runtime | Runs cannot interfere | Two explanations for every divergence; the signal is lost | ## Telling a migration defect from a product defect The characteristic event is a migrated case that fails on the new runtime while the old one passes on the same commit. Re-run the pair on a commit the old runtime was green on; if the split persists on unchanged code, the difference lives in the migration surface, not the product. That surface is small and enumerable: discovery, the order setup and teardown fire in, the unit of parallelism, how the new runner waits for something to become actionable, and how a failure is captured. Then reduce the case: strip it to a single shared action call, assert nothing, and add layers back until the split reappears. What you must not do is label the split intermittent. A reproducible runtime-specific difference is a migration defect wearing a convenient name, and the label lets it survive to cutover, when it is indistinguishable from the hundred other things that broke that day. ## The overlap window is the expensive half Every moved case runs twice: doubled pipeline minutes, two configurations to keep valid, and two chances for an unrelated intermittent failure to block a merge. That cost buys a controlled cutover and is worthless as a steady state, so the overlap needs a shape: - Publish the count of unmigrated cases on a fixed cadence, and treat a flat week as a decision point rather than as a delay. - Move a case and delete its old copy in the **same** change, so no case is ever knowingly maintained in two places. - Keep an explicit list of cases the new runtime genuinely cannot run, with the capability each is waiting on. A growing list means the new runtime is missing something the plan assumed, which you want to know in week three rather than week thirty. - Set the end date at the start. A side-by-side with no end date is not a migration; it is a decision to run two suites forever, taken by nobody. At the end, delete the old runner, its configuration, its reporting adapter and any helper that existed only to serve it — in one change, not gradually. Half a retired runtime is something every future reader must investigate before being sure it is dead.
- The new runtime reports a case as failed that the old one passes on the same commit. How do you tell a migration defect from a real product defect?Re-run the pair on a commit the old runtime was green on. If the split persists on unchanged code, the difference is in the migration surface: discovery, the order setup and teardown fire in, the unit of parallelism, or how the new runner waits for something to become actionable. Then reduce the case to a single shared action call asserting nothing, and add layers back until the split reappears.
- How long should both runtimes stay in the pipeline?Only as long as the burndown is moving. Set the date at the start, publish the count of unmigrated cases on a fixed cadence, and treat a flat period as a decision point rather than a delay. A permanent overlap is not a migration; it is two suites, two sets of upkeep and doubled run cost for no added coverage.
- What do you do with a case the new runtime cannot run at all?Separate cannot from not-yet. If the case depends on something only the old runner provides — a lifecycle ordering, a data setup point, a reporting detail — the honest options are to grow that capability in the new runtime or to re-express the case against a different interface. Record which, and count them. A growing blocked list means the new runtime is missing something the plan assumed it had.
Two doors into the same building. Refurnish the rooms once and both doors show the same thing; furnish each door's rooms separately and visitors start describing different buildings.
saying these in an interview costs you the question
- Copies each case into the new runtime and edits both from then on
- Runs the two runtimes against different deployed targets
- Calls a case that fails only on the new runtime intermittent
- Starts the side-by-side with no end date and no burndown
- Lets the old runner's outcome stop blocking a merge
- Replaces the runner and the business actions in one change