Problem
TUI integration tests intermittently fail in CI because tick() only waits one macrotask (setTimeout(resolve, 0)), but Ink (React) rendering sometimes needs 2-3 macrotask cycles to complete the event → state update → reconcile → render pipeline.
Example failure (PR #1117 CI, eventbus-ui.integration.spec.tsx:313):
expect(lastFrame()).toContain('RUNNING')
// Actual: full TUI frame rendered but still showing 'idle' instead of 'RUNNING'
Same test passes in PRs #1114 and #1115 — classic flaky timing issue.
Root Cause
// Current: single macrotask — not enough for Ink rendering pipeline
const tick = () => new Promise(resolve => setTimeout(resolve, 0));
This pattern is duplicated in 5 files with 74 await tick() call sites:
eventbus-ui.integration.spec.tsx (44 calls)
dashboard-app.spec.tsx (14 calls)
transport-tui.integration.spec.tsx (7 calls)
__perf__/memory-stability.spec.tsx (5 calls)
__perf__/rendering-performance.spec.tsx (4 calls)
Solution
1. Create shared test utility: apps/mcp-server/src/tui/testing/tui-test-utils.ts
/**
* Flush multiple macrotask cycles so Ink completes its render pipeline.
* Replaces the fragile single-setTimeout tick() used across TUI tests.
*/
export async function flushInk(cycles = 3): Promise<void> {
for (let i = 0; i < cycles; i++) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
/**
* Retry an assertion until it passes or times out.
* Use for frame content assertions that depend on Ink render timing.
*/
export async function waitForFrame(
getFrame: () => string | undefined,
assertion: (frame: string) => void,
timeoutMs = 500,
): Promise<void> {
const start = Date.now();
let lastError: Error | undefined;
while (Date.now() - start < timeoutMs) {
try {
assertion(getFrame() ?? '');
return;
} catch (err) {
lastError = err as Error;
await new Promise(resolve => setTimeout(resolve, 10));
}
}
throw lastError ?? new Error('waitForFrame timeout');
}
2. Replace tick() in all 5 files
// BEFORE (each file):
const tick = () => new Promise(resolve => setTimeout(resolve, 0));
// AFTER (each file):
import { flushInk } from './testing/tui-test-utils';
// or: import { flushInk } from '../testing/tui-test-utils';
Then replace await tick() → await flushInk() across all 74 call sites.
3. (Optional) Migrate critical assertions to waitForFrame
For assertions most prone to flakiness (long tests with many sequential events), use waitForFrame:
// BEFORE:
await tick();
expect(lastFrame()).toContain('RUNNING');
// AFTER:
await waitForFrame(lastFrame, frame => expect(frame).toContain('RUNNING'));
Files to Modify
| File |
Changes |
src/tui/testing/tui-test-utils.ts |
CREATE — flushInk() + waitForFrame() |
src/tui/testing/tui-test-utils.spec.ts |
CREATE — unit tests for utilities |
src/tui/eventbus-ui.integration.spec.tsx |
Replace tick → flushInk (44 sites) |
src/tui/dashboard-app.spec.tsx |
Replace tick → flushInk (14 sites) |
src/tui/transport-tui.integration.spec.tsx |
Replace tick → flushInk (7 sites) |
src/tui/__perf__/rendering-performance.spec.tsx |
Replace tick → flushInk (4 sites) |
src/tui/__perf__/memory-stability.spec.tsx |
Replace tick → flushInk (5 sites) |
Verification
# Run all TUI tests multiple times to confirm no flakiness
for i in {1..5}; do
yarn workspace codingbuddy test -- --testPathPattern="src/tui/(eventbus|dashboard|transport|__perf__)" && echo "Run $i: PASS" || echo "Run $i: FAIL"
done
Acceptance Criteria
Problem
TUI integration tests intermittently fail in CI because
tick()only waits one macrotask (setTimeout(resolve, 0)), but Ink (React) rendering sometimes needs 2-3 macrotask cycles to complete the event → state update → reconcile → render pipeline.Example failure (PR #1117 CI,
eventbus-ui.integration.spec.tsx:313):Same test passes in PRs #1114 and #1115 — classic flaky timing issue.
Root Cause
This pattern is duplicated in 5 files with 74
await tick()call sites:eventbus-ui.integration.spec.tsx(44 calls)dashboard-app.spec.tsx(14 calls)transport-tui.integration.spec.tsx(7 calls)__perf__/memory-stability.spec.tsx(5 calls)__perf__/rendering-performance.spec.tsx(4 calls)Solution
1. Create shared test utility:
apps/mcp-server/src/tui/testing/tui-test-utils.ts2. Replace
tick()in all 5 filesThen replace
await tick()→await flushInk()across all 74 call sites.3. (Optional) Migrate critical assertions to
waitForFrameFor assertions most prone to flakiness (long tests with many sequential events), use
waitForFrame:Files to Modify
src/tui/testing/tui-test-utils.tsflushInk()+waitForFrame()src/tui/testing/tui-test-utils.spec.tssrc/tui/eventbus-ui.integration.spec.tsxtick→flushInk(44 sites)src/tui/dashboard-app.spec.tsxtick→flushInk(14 sites)src/tui/transport-tui.integration.spec.tsxtick→flushInk(7 sites)src/tui/__perf__/rendering-performance.spec.tsxtick→flushInk(4 sites)src/tui/__perf__/memory-stability.spec.tsxtick→flushInk(5 sites)Verification
Acceptance Criteria
flushInk()utility created with teststick()definitions remain in TUI test filesflushInk()