LogInteraction.test.tsx exhausts V8 heap during full-suite vitest run #123

Closed
opened 2026-06-30 23:59:16 +00:00 by Mark · 0 comments
Owner

Problem

The test suite is over-engineered for what the app actually does, and that over-engineering is what tips the runner into a 3-hour OOM on every full-suite verify.

This was filed first as a "config split" problem (#122) and a "LogInteraction heap" problem (#123). Both frames assume the test suite is the right size and we just need to slim a few files. It is not the right size. The numbers:

  • 51 test files, ~10,776 lines of test code vs ~32 source files in src/lib/. We have more test code than source under test, and most of the suite is re-implementation, not coverage.
  • Heaviest tests in the repo:
    • src/lib/utils.test.ts — 849 lines, 95 tests
    • src/lib/store.test.ts — 718 lines, 68 tests
    • src/lib/db.test.ts — 706 lines, 64 tests
    • src/lib/metrics-engine.test.ts — 630 lines, 41 tests
    • src/lib/schemas.test.ts — 587 lines
    • src/lib/native/__tests__/plugin-wrappers.test.ts — 537 lines
    • Plus a 348-line LogInteraction.test.tsx that mocks framer-motion via a Proxy, mocks the store via a global mockState, mocks the router, and pulls in the global Capacitor/IndexedDB/MSW setup from src/test/setup.ts.
  • Full-suite vitest run clocks 10,838s and OOMs V8 inside the LogInteraction.test.tsx worker. Targeted single-file runs: ~3s. That is not a flaky test. It is the cumulative retention cost of a suite that mocks the world.

The framing changed because the original gate-budget issue (#122) and the OOM issue (#123) were both pointing at the same root cause: the tests are stupid. They re-mock components, re-implement utilities against themselves, and stack fixture chains that don't reflect how the app is used. LogInteraction is just the leaf where V8 finally says no.

Evidence

From a manual full-suite verify run on 2026-06-30 (started 13:27 UTC, ran ~3h 0m 38s before crashing):

<--- Last few GCs --->
[1051227:0x180e3000] 10796351 ms: Scavenge (interleaved) 3871.5 (4000.8) -> 3833.8 (4013.8) MB
[1051227:0x180e3000] 10799562 ms: Mark-Compact (reduce) 3866.4 (4014.5) -> 3747.2 (3871.5) MB
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

 Vitest caught 1 unhandled error during the test run.
 Error: [vitest-pool]: Worker forks emitted error.
   Caused by: Error: Worker exited unexpectedly
   [vitest-pool]: Timeout terminating forks worker for test files src/pages/__tests__/LogInteraction.test.tsx.

  Test Files  50 passed (51)
       Tests  900 passed | 3 todo (918)
      Errors  1 error
   Duration  10838.35s (transform 8.34s, setup 15.87s, import 123.72s, tests 30.02s, environment 90.79s)

Specifically about the over-mock pattern (from the existing test, not fabricated):

// src/pages/__tests__/LogInteraction.test.tsx, lines 8-29
vi.mock("framer-motion", () => ({
  motion: new Proxy({}, { get: () => (props) => { /* ...strip animations... */ } }),
  AnimatePresence: ({ children }) => <>{children}</>,
}));

That Proxy exists because the test renders the page. The page shouldn't need to render framer-motion primitives in a unit test. If the test is testing LogInteraction's behavior, it should render the page with the real motion library, or it should target a smaller surface.

Scope

  1. Audit src/lib/*.test.ts top-heavies (utils, store, db, metrics-engine, schemas, plugin-wrappers). For each, ask:
    • Does this test exercise a real public surface, or is it re-implementing the function under test against itself?
    • Does it mock so much of the system under test that what it proves is "the mocks return what the test set up"?
    • Is there a real failure mode this test catches that a smaller test wouldn't?
    • Cut anything that fails those three questions.
  2. src/pages/__tests__/LogInteraction.test.tsx: stop rendering the full page. Pull the page apart. Cover the actual behavior (form state, submission, navigation) without mounting the whole component graph under a framer-motion Proxy. The framer-motion mock alone is a sign the test is testing how the test feels, not how the app behaves.
  3. src/test/setup.ts: review the global imports of fake-indexeddb, Capacitor mocks, MSW server. Decide which ones need to be global vs per-file. Most of the suite doesn't need all three wired in front of every test.
  4. Gate config (vitest.config.*, package.json scripts): once the suite is lean, set the gate to the actual npm test again. Do not split into test:unit and a separate full run — that hides the problem. The whole suite should be fast enough to gate on.

Out of scope

  • Adding more tests.
  • Splitting the gate into fast + slow.
  • Mock library changes unrelated to making the current tests smaller.
  • Touching tests under src/lib/native/__tests__/ beyond deleting the over-mocked ones; native bridge tests are deliberately low-level and most of them should stay.
  • The 3-test todo count.
  • #115 work (already shipped, separate domain).

Reproduction

cd ~/Projects/kimi_agent_kin_app_sync
NODE_OPTIONS="--max-old-space-size=4096" npx vitest run --reporter=verbose 2>&1 | tail -40
# ~3 hours in: Mark-Compact near heap limit, worker for LogInteraction.test.tsx dies

Suggested approach

  1. Start at src/pages/__tests__/LogInteraction.test.tsx. Refactor it to not render the page; cover behavior at a smaller surface. This should drop full-suite heap pressure immediately.
  2. Then walk down src/lib/utils.test.ts, src/lib/store.test.ts, src/lib/db.test.ts, src/lib/metrics-engine.test.ts, src/lib/schemas.test.ts, src/lib/native/__tests__/plugin-wrappers.test.ts — for each, delete tests that don't answer the three questions in Scope item 1.
  3. Tighten src/test/setup.ts so it only mocks what the suite that actually runs needs.
  4. Re-run the full suite. Target: full vitest run under 180s with no OOM. Then full-suite npm test is the gate, no split.

Constraints

  • Don't disable tests silently. If a test is wrong, delete it and say so in the commit.
  • Don't raise V8 heap or the gate budget to dodge the problem.
  • Don't introduce a test:unit / test:full split. One suite, fast enough to gate.
  • New tests added later must follow the same "test real public surface" rule, not the old "mock the world" pattern.

Verification

  • time npx vitest run finishes under 180s, exits 0, no V8 OOM
  • npm test (gate) clears on the first kilo-gsd verify for any dispatched issue
  • Test LOC roughly halved; the suite covers behavior, not implementation
  • LogInteraction specifically no longer renders via a framer-motion Proxy
## Problem The test suite is over-engineered for what the app actually does, and that over-engineering is what tips the runner into a 3-hour OOM on every full-suite verify. This was filed first as a "config split" problem (#122) and a "LogInteraction heap" problem (#123). Both frames assume the test suite is the right size and we just need to slim a few files. It is not the right size. The numbers: - **51 test files, ~10,776 lines of test code** vs **~32 source files in `src/lib/`**. We have more test code than source under test, and most of the suite is re-implementation, not coverage. - Heaviest tests in the repo: - `src/lib/utils.test.ts` — 849 lines, 95 tests - `src/lib/store.test.ts` — 718 lines, 68 tests - `src/lib/db.test.ts` — 706 lines, 64 tests - `src/lib/metrics-engine.test.ts` — 630 lines, 41 tests - `src/lib/schemas.test.ts` — 587 lines - `src/lib/native/__tests__/plugin-wrappers.test.ts` — 537 lines - Plus a 348-line `LogInteraction.test.tsx` that mocks framer-motion via a Proxy, mocks the store via a global `mockState`, mocks the router, and pulls in the global Capacitor/IndexedDB/MSW setup from `src/test/setup.ts`. - Full-suite `vitest run` clocks **10,838s** and OOMs V8 inside the `LogInteraction.test.tsx` worker. Targeted single-file runs: ~3s. That is not a flaky test. It is the cumulative retention cost of a suite that mocks the world. The framing changed because the original gate-budget issue (#122) and the OOM issue (#123) were both pointing at the same root cause: **the tests are stupid**. They re-mock components, re-implement utilities against themselves, and stack fixture chains that don't reflect how the app is used. LogInteraction is just the leaf where V8 finally says no. ## Evidence From a manual full-suite verify run on 2026-06-30 (started 13:27 UTC, ran ~3h 0m 38s before crashing): ``` <--- Last few GCs ---> [1051227:0x180e3000] 10796351 ms: Scavenge (interleaved) 3871.5 (4000.8) -> 3833.8 (4013.8) MB [1051227:0x180e3000] 10799562 ms: Mark-Compact (reduce) 3866.4 (4014.5) -> 3747.2 (3871.5) MB FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory Vitest caught 1 unhandled error during the test run. Error: [vitest-pool]: Worker forks emitted error. Caused by: Error: Worker exited unexpectedly [vitest-pool]: Timeout terminating forks worker for test files src/pages/__tests__/LogInteraction.test.tsx. Test Files 50 passed (51) Tests 900 passed | 3 todo (918) Errors 1 error Duration 10838.35s (transform 8.34s, setup 15.87s, import 123.72s, tests 30.02s, environment 90.79s) ``` Specifically about the over-mock pattern (from the existing test, not fabricated): ```tsx // src/pages/__tests__/LogInteraction.test.tsx, lines 8-29 vi.mock("framer-motion", () => ({ motion: new Proxy({}, { get: () => (props) => { /* ...strip animations... */ } }), AnimatePresence: ({ children }) => <>{children}</>, })); ``` That Proxy exists because the test renders the page. The page shouldn't need to render framer-motion primitives in a unit test. If the test is testing LogInteraction's behavior, it should render the page with the real motion library, or it should target a smaller surface. ## Scope 1. **Audit `src/lib/*.test.ts` top-heavies** (`utils`, `store`, `db`, `metrics-engine`, `schemas`, `plugin-wrappers`). For each, ask: - Does this test exercise a real public surface, or is it re-implementing the function under test against itself? - Does it mock so much of the system under test that what it proves is "the mocks return what the test set up"? - Is there a real failure mode this test catches that a smaller test wouldn't? - Cut anything that fails those three questions. 2. **`src/pages/__tests__/LogInteraction.test.tsx`**: stop rendering the full page. Pull the page apart. Cover the actual behavior (form state, submission, navigation) without mounting the whole component graph under a framer-motion Proxy. The framer-motion mock alone is a sign the test is testing how the test feels, not how the app behaves. 3. **`src/test/setup.ts`**: review the global imports of `fake-indexeddb`, Capacitor mocks, MSW server. Decide which ones need to be global vs per-file. Most of the suite doesn't need all three wired in front of every test. 4. **Gate config** (`vitest.config.*`, `package.json` scripts): once the suite is lean, set the gate to the actual `npm test` again. Do not split into `test:unit` and a separate full run — that hides the problem. The whole suite should be fast enough to gate on. ## Out of scope - Adding more tests. - Splitting the gate into fast + slow. - Mock library changes unrelated to making the current tests smaller. - Touching tests under `src/lib/native/__tests__/` beyond deleting the over-mocked ones; native bridge tests are deliberately low-level and most of them should stay. - The 3-test todo count. - #115 work (already shipped, separate domain). ## Reproduction ``` cd ~/Projects/kimi_agent_kin_app_sync NODE_OPTIONS="--max-old-space-size=4096" npx vitest run --reporter=verbose 2>&1 | tail -40 # ~3 hours in: Mark-Compact near heap limit, worker for LogInteraction.test.tsx dies ``` ## Suggested approach 1. Start at `src/pages/__tests__/LogInteraction.test.tsx`. Refactor it to not render the page; cover behavior at a smaller surface. This should drop full-suite heap pressure immediately. 2. Then walk down `src/lib/utils.test.ts`, `src/lib/store.test.ts`, `src/lib/db.test.ts`, `src/lib/metrics-engine.test.ts`, `src/lib/schemas.test.ts`, `src/lib/native/__tests__/plugin-wrappers.test.ts` — for each, delete tests that don't answer the three questions in Scope item 1. 3. Tighten `src/test/setup.ts` so it only mocks what the suite that actually runs needs. 4. Re-run the full suite. Target: full `vitest run` under 180s with no OOM. Then full-suite `npm test` is the gate, no split. ## Constraints - Don't disable tests silently. If a test is wrong, delete it and say so in the commit. - Don't raise V8 heap or the gate budget to dodge the problem. - Don't introduce a `test:unit` / `test:full` split. One suite, fast enough to gate. - New tests added later must follow the same "test real public surface" rule, not the old "mock the world" pattern. ## Verification - `time npx vitest run` finishes under 180s, exits 0, no V8 OOM - `npm test` (gate) clears on the first kilo-gsd verify for any dispatched issue - Test LOC roughly halved; the suite covers behavior, not implementation - LogInteraction specifically no longer renders via a framer-motion Proxy
Mark 2026-06-30 23:59:28 +00:00
  • closed this issue
  • added the
    bug
    label
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Mark/kimi_agent_kin_app_sync#123
No description provided.