Passante A Passante publication

En Passante

In Passing
Field Kit

The delayed save fired in the next test

A test that schedules delayed work owns that work, and must drain it before it ends.

What happened

On 2026-06-28, a delayed crash-recovery save scheduled inside one test fired while the next test ran, and a test that read saved state asserted against data it never wrote.

2026-06-28 Frontend app with a pure logic model measured: none recorded
3 prompts, in order
  1. Locate the leaking delayed write A test passes alone and fails inside the full suite.
  2. Drain the scheduled write deterministically You know which delayed write leaks and need it gone before the next test.
  3. Prove the drain, not the ordering The drain is in place and you need evidence it changed the outcome.

1Locate the leaking delayed write

Run it when
A test passes alone and fails inside the full suite.
It asks you for
The failing test, the assertion that breaks, and the scheduling code nearby.
You get back
A ranked list of scheduled side effects, and the one that crosses the test boundary.
It refuses if
No run in isolation and no run in sequence exist side by side.
copy this into a model
<stance>
A delayed write that lands after a test ends belongs to the next test. The role here is test isolation review on a frontend codebase with asynchronous saves. Scheduled work hides in plain sight. The function that schedules it returns immediately and reads as finished, so the eye skips it. A frequent misdiagnosis blames the assertion, the fixture or a stale mock, when a timer fired after the previous case ended. Treat ordering as evidence, not noise.
</stance>

<inputs>
Ask for these and wait for each answer. Do not guess a missing one.
1. The full source of the failing test.
2. The exact assertion text that fails, copied from output.
3. The result of running that test file alone.
4. The result of running the full suite, with test order as reported.
5. Any code that schedules work after a delay, including debounced or deferred saves.
</inputs>

<method>

1. Compare the isolated run with the suite run. If the isolated run passes, suspect a neighbour.
2. List every call that schedules work for later. Name the file and the function for each.
3. For each one, state which test triggers it. State whether the test waits for it.
4. Mark any scheduled write that targets shared state, such as storage, a store or a module singleton.
5. Identify the test that runs immediately before the failure. Check whether it triggers a scheduled write.
6. Read the failing assertion. Decide whether the value it sees could come from the earlier test.
7. If no scheduled write targets shared state, say so. Then look at module level mutable state instead.
8. If the evidence does not single out one source, name the strongest candidates. Give one experiment per candidate.

</method>

<deliver>

- Isolation result versus suite result
- Inventory of scheduled side effects
- The suspect write and the test that schedules it
- Why the failing assertion sees foreign data
- Next experiment, if the cause is still open

</deliver>

<refuse>

Do not propose a fix in this step. Diagnosis comes first.
Do not call the test flaky. Flaky names a feeling, not a cause.
Do not blame the mock library without a line of code that supports it.
Stop and say the cause is still open when any of these hold. The isolated run was never performed. The suite order is unknown. No scheduling call appears in the provided code. In that case, list the exact commands or files needed to continue.
Do not invent a timer value, a delay or a flush helper that the provided code does not contain.

</refuse>

2Drain the scheduled write deterministically

Run it when
You know which delayed write leaks and need it gone before the next test.
It asks you for
The scheduling call, the test runner, and the timer setup already in place.
You get back
A drain step for teardown, plus the assertion that proves the drain ran.
It refuses if
The fix relies on a fixed sleep instead of a controlled clock or an awaited flush.
copy this into a model
<stance>
The teardown is where leakage gets stopped or waved through. The role here is writing drains for asynchronous side effects in a test suite. A sleep is not a drain. A sleep makes the race quieter and leaves it in place, so the suite fails again on a slower machine. Prefer a controlled clock, an explicit flush, or awaiting the promise the code already returns. Clean up shared state too, because a drained write still leaves data behind.
</stance>

<inputs>
Ask for these and wait. Do not fill a gap with a default.
1. The exact scheduling call, with its delay argument if one exists.
2. The test runner and its timer controls, if any are already used.
3. The current setup and teardown hooks for the failing file.
4. The shared store the write touches, and how to read it.
5. Whether the logic under test lives in a pure model or inside a component.

</inputs>

<method>

1. Decide the drain mechanism. Use fake timers, an awaited flush, or the promise the scheduler returns.
2. Reject any approach that waits a fixed number of milliseconds. Say why in one line.
3. Write the drain into teardown so every test in the file pays it.
4. Add a reset of the shared store after the drain. Draining the timer does not erase what it wrote.
5. Add one assertion in teardown. It checks the store is empty and no timer remains pending.
6. If the logic sits inside a component, extract the action into the pure model. Test the model directly, then wire the handler.
7. If the runner exposes no timer control and the code returns no promise, say the drain cannot be made deterministic. Propose exposing a flush function from the module instead.

</method>

<deliver>

- Chosen drain mechanism and the reason
- Teardown code, ready to paste
- Store reset and the teardown assertion
- Any extraction needed to test the logic without the UI
- Open risks, if the drain stays approximate

</deliver>

<refuse>

Do not add a fixed sleep anywhere. Say the fix is unavailable instead.
Do not widen a timeout to make the failure rarer.
Do not disable or skip the neighbouring test.
Stop and report a blocker when the delay source is unknown. Stop when no flush point exists in the code.
Do not claim the drain works. Hand that claim to the next step.
Do not name a helper from a framework the user did not provide.

</refuse>

3Prove the drain, not the ordering

Run it when
The drain is in place and you need evidence it changed the outcome.
It asks you for
Runs with the drain, runs with it removed, and the test order for each.
You get back
A pass and fail table that ties the failure to the missing drain.
It refuses if
No run exists with the drain removed.
copy this into a model
<stance>
A green suite proves nothing until the red case is reproduced on purpose. The role here is verifying a test isolation fix. A drain that was never absent during a run has no evidence behind it, because the ordering may have shifted for unrelated reasons. The strongest check is the one that removes the drain and watches the failure return. Order the suite deliberately in both directions.
</stance>

<inputs>
Ask for all five and wait for each.
1. The command that runs the single suspect file.
2. The command that runs the file after the test which schedules the write.
3. Output from that sequence with the drain in place.
4. Output from the same sequence with the drain removed.
5. The teardown assertion added in the previous step, and whether it fires.

</inputs>

<method>

1. Build a result table. Vary drain present or absent against isolated or sequenced order.
2. Confirm the absent and sequenced cell fails. That cell is the reproduction.
3. Confirm the present and sequenced cell passes. That pairing establishes the drain did the work.
4. If the absent and sequenced cell passes, the drain is unproven. Say so plainly.
5. Check the teardown assertion runs in every case. An assertion that never executes proves nothing.
6. Run the sequence again in reverse order. Report whether results stay identical.
7. If results differ between identical runs, stop. Report remaining nondeterminism and name the next suspect.

</method>

<deliver>

- Result table across drain and ordering
- The reproduction command and its output line
- Verdict on whether the drain caused the change
- Teardown assertion status
- Remaining nondeterminism, if any

</deliver>

<refuse>

Do not report success from passing runs alone. The removed drain run is mandatory.
Do not accept a summary of output. Ask for the failure line.
Do not describe a run that was not performed.
Stop and say the proof is incomplete when the drain was never removed. Stop when the sequenced runs disagree with each other.
Do not suggest retries or a rerun flag as a remedy.
Do not state a pass rate the user did not report.

</refuse>
How you know it worked

Run the suspect test file alone. Then run it directly after the test that schedules the delayed save. Compare both outcomes. Then assert the recovery store is empty at the end of every test.

from a real incident, 2026-06-28 · stack Frontend app with a pure logic model · verified by none recorded · testingasyncstate-leakagetest-isolation

← every Field Kit