Files
fusion/packages
gsxdsm b42b40aa48 fix: make flushAsyncWork actually drain — clears 32 notifier failures on main (#2776)
## 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>
2026-07-30 09:25:21 -07:00
..
2026-07-26 18:11:47 -07:00