claude-code-best/.files/test_fixes.md
Onuzulike Anthony Ifechukwu eaeefafe5f Refactor tests and improve mocking for better isolation
- Updated test files to use `afterAll` for restoring mocks, ensuring cleaner test environments.
- Simplified platform mocking in `modifiers-napi` tests to avoid global state mutations.
- Enhanced `useFrustrationDetection` tests with clearer structure and consistent timeout settings.
- Consolidated React and Ink mocks into a dedicated `test-mock.ts` file for reuse across tests.
- Improved readability and maintainability of tests by standardizing timeout configurations.
- Adjusted imports and module mocks to prevent interference between test files.
2026-06-03 01:51:00 +01:00

186 lines
5.9 KiB
Markdown

# Test Isolation Fixes
Two bugs. Two files touched.
---
## 1. `packages/modifiers-napi/src/__tests__/index.test.ts`
**Problem:** `mock.module('bun:ffi', ...)` is declared at module scope with no cleanup.
Bun reuses workers across files in a full suite run, so the mock bleeds into other
files that import anything touching `bun:ffi`. When `index.test.ts` runs _after_
another file has already loaded `bun:ffi` into the module registry, the module-level
mock declaration loses the race and the test gets the wrong module — or nothing at all.
**Fix:** Add `afterAll(() => mock.restore())`.
```typescript
// packages/modifiers-napi/src/__tests__/index.test.ts
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test'
let ffiShouldThrow = false
let nativeFlags = 0
let dlopenCalls = 0
mock.module('bun:ffi', () => ({
FFIType: {
i32: 0,
u64: 0,
},
dlopen: () => {
dlopenCalls++
if (ffiShouldThrow) {
throw new Error('ffi load failed')
}
return {
symbols: {
CGEventSourceFlagsState: () => nativeFlags,
},
}
},
}))
// ✅ NEW: restore the bun:ffi mock after this file finishes
// so it doesn't bleed into other files running in the same worker
afterAll(() => {
mock.restore()
})
beforeEach(() => {
ffiShouldThrow = false
nativeFlags = 0
dlopenCalls = 0
})
// ... rest of file unchanged
```
---
## 2. `src/utils/staticRender.tsx`
**Problem:** `await instance.waitUntilExit()` hangs indefinitely when Ink's reconciler
commit phase doesn't complete. `waitUntilExit()` only resolves when `exit()` is called
inside `useLayoutEffect` — but if `@anthropic/ink`'s `wrappedRender` has stale
process-level state from a previous test file (singleton reconciler, leaked
`process.on('exit')` handlers, a cached fiber root), React never commits, `useLayoutEffect`
never fires, and the whole render silently hangs until the 10s test timeout kills it.
This only surfaces in the full suite because Bun shares workers across files sequentially.
Running the file alone gives it a clean worker with no prior Ink state.
**Fix:** Race `waitUntilExit()` against a 3s safety timeout. On timeout, call
`instance.unmount()` to force Ink cleanup and throw a descriptive error instead of
a silent 10s hang.
Also: explicitly remove the stream `data` listener and destroy the stream after render.
Without this, the PassThrough stream stays open and its listener accumulates garbage
across calls in the same process lifetime.
```typescript
// src/utils/staticRender.tsx
import * as React from 'react';
import { useLayoutEffect } from 'react';
import { PassThrough } from 'stream';
import stripAnsi from 'strip-ansi';
import { wrappedRender as render, useApp } from '@anthropic/ink';
function RenderOnceAndExit({ children }: { children: React.ReactNode }): React.ReactNode {
const { exit } = useApp();
useLayoutEffect(() => {
const timer = setTimeout(exit, 0);
return () => clearTimeout(timer);
}, [exit]);
return <>{children}</>;
}
const SYNC_START = '\x1B[?2026h';
const SYNC_END = '\x1B[?2026l';
function extractFirstFrame(output: string): string {
const startIndex = output.indexOf(SYNC_START);
if (startIndex === -1) return output;
const contentStart = startIndex + SYNC_START.length;
const endIndex = output.indexOf(SYNC_END, contentStart);
if (endIndex === -1) return output;
return output.slice(contentStart, endIndex);
}
export async function renderToAnsiString(node: React.ReactNode, columns?: number): Promise<string> {
let output = '';
const stream = new PassThrough();
if (columns !== undefined) {
(stream as unknown as { columns: number }).columns = columns;
}
// ✅ NEW: named handler so we can remove it after render
const dataHandler = (chunk: Buffer) => {
output += chunk.toString();
};
stream.on('data', dataHandler);
const instance = await render(<RenderOnceAndExit>{node}</RenderOnceAndExit>, {
stdout: stream as unknown as NodeJS.WriteStream,
patchConsole: false,
});
// ✅ NEW: race waitUntilExit against a 3s hard limit.
// If Ink's reconciler is stuck (stale worker state from a prior test file),
// this surfaces a real error instead of silently hanging for 10s.
await Promise.race([
instance.waitUntilExit(),
new Promise<void>((_, reject) =>
setTimeout(() => {
instance.unmount();
reject(
new Error(
'[staticRender] Ink render did not exit within 3s — wrappedRender may have stale process state from a prior test file',
),
);
}, 3000),
),
]);
// ✅ NEW: clean up the stream so it doesn't accumulate across calls
stream.off('data', dataHandler);
stream.destroy();
return extractFirstFrame(output);
}
export async function renderToString(node: React.ReactNode, columns?: number): Promise<string> {
const output = await renderToAnsiString(node, columns);
return stripAnsi(output);
}
```
---
## If AutofixProgress tests still hang after this
The 3s safety net will stop the silent timeout and give you a real error message.
If you're still seeing the hang, the root is inside `wrappedRender` from `@anthropic/ink`.
Look for any of these in that file:
- A module-level singleton (cached app instance, fiber root, reconciler)
- `process.on('exit')` / `process.on('SIGTERM')` handlers added on each `render()` call but never removed
- A global `stdout` patch applied once and assumed fresh on every call
Share that file and the exact line can be pinpointed.
---
## Summary
| File | Change | Why |
| ------------------ | ------------------------------------------------------- | ------------------------------------------------ |
| `index.test.ts` | Add `afterAll(() => mock.restore())` | Stops `bun:ffi` mock bleeding into other workers |
| `staticRender.tsx` | Race `waitUntilExit()` with 3s timeout + stream cleanup | Surfaces real errors instead of silent 10s hangs |