## Red on main, fix-forward
batch-engine (#2773) left 32 failing cases on main:
`notifier.runtime.test.ts` (19) and `notifier.test.ts` (13), all
`expected "vi.fn()" to be called 1 times, but got 0 times`.
## The failure is in the harness, not the notifier
`notifier.ts` is untouched by #2773.
`notification/notification-service.ts` was converted, and
`handleTaskMoved` is fire-and-forget (`void
this.handleTaskMovedAsync(data)`). The async path now awaits
`resolveLifecycleColumnsForTask` and then `resolveReviewColumnsForTask`
(which awaits `resolveWorkflowIrForTask`) — several more await hops
after `store.emit(...)` returns.
The harness had no slack to absorb them:
```ts
export async function flushAsyncWork(): Promise<void> {
await vi.waitFor(() => { expect(true).toBe(true); });
}
```
The condition is true on the first tick, so `waitFor` resolves
immediately. **It never waited for anything.** It worked only while the
handler completed within a single turn — and it reported the resulting
breakage as a notifier defect rather than as its own.
Another entry in the recurring pattern this program keeps hitting: a
cheap check that reads as authoritative. A `waitFor` looks like
synchronization at the call site; this one was a no-op.
## Evidence
| run | result |
|---|---|
| before | 32 failed / 68 passed (100) |
| after | **100 passed (100)**, 3.25s |
| after, mutated back to a single `await Promise.resolve()` | 32 failed
/ 68 passed — the same 32 |
The mutation run is the point: the fix is load-bearing, not a
coincidence of timing.
Gate 732 green · `pnpm lint` clean · engine `tsc --noEmit` clean.
## Reversible decision, noted: microtasks only
A `setTimeout(0)` drain also turns all 100 green, and it was my first
version. Rejected on measurement:
- it costs real wall-clock at every call site — the two files went **~2s
→ over 2 minutes**;
- it **stalls under the fake timers** `notifier.test.ts` installs (lines
355/381/589), where a pending `setTimeout` never fires — 4 cases hung.
The awaits being drained are promise-based (workflow-IR resolution), so
microtask turns are the right currency, they work identically under real
and fake timers, and they cost nothing. Per AGENTS.md *"Do Not Add Slow
Tests"* — prefer fake timers over real time waits.
16 turns is slack, not a tuned number; the chain is ~4 deep today.
## Scope
One test-harness file. No product code, no behavior change. Tests
asserting a specific outcome should still prefer `vi.waitFor` on *that
outcome* — this helper covers the "let the fire-and-forget handler
finish" case, and now actually does it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>