opencode icon indicating copy to clipboard operation
opencode copied to clipboard

fix(test): prevent memory and resource leaks in test infrastructure

Open wvanderen opened this issue 1 day ago • 1 comments

GitHub Issue Draft


Title: `fix(test): prevent memory and resource leaks in test infrastructure

Type: bug

Labels: bug, perf, test, infrastructure


Description

Critical resource leaks in test infrastructure causing disk/memory bloat and OOM crashes during development.

When running tests (bun test or bun test --watch), the test framework accumulates resources without cleanup, leading to:

  • Temporary directories never being deleted
  • Spawned processes leaking on test failures
  • Eventually triggering OOM killer (vitest workers consuming ~900MB each)
  • Terminals closing unexpectedly (appears as "freezes" to developers)

This affects anyone running tests locally, especially in watch mode.


Evidence

System Logs (journalctl)

Jan 17 13:14 - vitest worker memory usage:
• vitest 1: 897 MB
• vitest 2: 900 MB
• vitest 3: 910 MB
• vitest 4: 905 MB
• vitest 5: 898 MB
• vitest 7: 912 MB

Total: ~5.3 GB for test workers alone!

OOM Killer Activity

Jan 17 13:14:28 lemarchy kernel: Out of memory: Killed process 1750015 (node (vitest 7))
  total-vm:4985788kB, anon-rss:3646756kB, file-rss:620kB

Jan 17 11:52:14 lemarchy kernel: Out of memory: Killed process 1627971 (node (vitest 3))
  total-vm:3727456kB, anon-rss:2268820kB

V8 stack trace shows:
  V8::FatalProcessOutOfMemory
  → Node aborts (signal 6/ABRT)
  → Terminal closes

System State

  • Swap: 3.9 GB / 4 GB used (95% full)
  • Workers: 6-11 vitest workers spawned per run
  • Impact: System becomes sluggish, eventually unresponsive

Root Causes

1. Temp Directory Cleanup Disabled

File: packages/opencode/test/fixture/fixture.ts

const result = {
  [Symbol.asyncDispose]: async () => {
    await options?.dispose?.(dirpath)
    // await fs.rm(dirpath, { recursive: true, force: true })  // ❌ COMMENTED OUT
  },
  path: realpath,
  extra: extra as T,
}

Every tmpdir() fixture call creates a permanent directory that is never deleted.


2. Process Leaks in LSP Tests

File: packages/opencode/test/lsp/client.test.ts

test("handles workspace/workspaceFolders request", async () => {
  const handle = spawnFakeServer() as any

  const client = await Instance.provide({...})  // ❌ If this throws, handle leaks

  await client.shutdown()  // Never reached if above throws
})

Fake LSP server processes are spawned for each test but only cleaned up if test completes successfully.


Impact

  • Developers: Test runs consume increasing amounts of memory/disk space
  • CI/CD: May experience OOM failures in test pipelines
  • System: Swap fills up, causing system-wide slowdowns
  • Experience: Terminals "freeze" then close, disrupting development workflow

Steps to Reproduce

Temp Directory Leak

  1. Run tests multiple times:
    cd packages/opencode
    bun test
    
  2. Check /tmp/ for accumulated directories:
    ls -la /tmp/ | grep opencode-test
    
  3. Observe hundreds of persistent temp directories

Process Leak

  1. Force a test to fail in packages/opencode/test/lsp/client.test.ts:
    test("handles workspace/workspaceFolders request", async () => {
      const handle = spawnFakeServer() as any
      throw new Error("Force failure to test cleanup")  // ✅ Process leaks here
    })
    
  2. Run tests: bun test
  3. Check for orphan Node.js processes:
    ps aux | grep fake-lsp-server
    
  4. Observe processes remain after test completes

Expected Behavior

  1. Temp directories: Should be deleted when test fixtures are disposed
  2. Processes: Should always be cleaned up, even on test failures
  3. Memory: Should remain stable across test runs in watch mode

Actual Behavior

  1. Temp directories: Accumulate in /tmp/ directory forever
  2. Processes: Leak when tests fail before cleanup
  3. Memory: Grows until OOM killer terminates tests

Related Issues

  • #7261 - "v1.1.6 still has memory bloat - heap not released + MCP orphan processes"

    • Similar pattern of resource leaks (MCP servers not terminated)
    • Our fix addresses test infrastructure specifically
  • #8899 / #8898 - "test: tests fail for developers with global git hooks"

    • Also touches test/fixture/fixture.ts
    • Shows test infrastructure needing isolation improvements

Proposed Fix

See PR: fix(test): prevent memory and resource leaks

Changes

  1. Re-enable temp directory cleanup in tmpdir() fixture
  2. Add try/finally blocks to LSP client tests ensuring process cleanup
  3. Processes now killed even when tests throw exceptions

Files Changed

  • packages/opencode/test/fixture/fixture.ts - Uncomment cleanup line
  • packages/opencode/test/lsp/client.test.ts - Add try/finally blocks

Testing Verification

After applying fix, verify:

# 1. Temp directories are cleaned up
cd packages/opencode
bun test --run
ls -la /tmp/ | grep opencode-test  # Should show empty or minimal results

# 2. No orphan processes after test failures
# Force a test to fail temporarily, then:
ps aux | grep fake-lsp-server  # Should show no processes

# 3. Memory stable in watch mode
watch -n 5 'ps aux | grep "vitest\|bun test" | awk "{sum += \$6} END {print "Total RSS:", sum/1024, "MB"}'
# Should remain stable (not grow indefinitely)

Workaround (Until PR Merged)

For developers experiencing this issue now:

# Add to shell config (~/.zshrc or ~/.bashrc)
export BUN_MAX_THREADS=2

# Disable coverage for development (edit bunfig.toml)
# coverage = false

# Clean up accumulated temp directories manually
rm -rf /tmp/opencode-test-*

OpenCode Version

v1.1.24 (current dev branch)


Environment

  • OS: Arch Linux 6.18.3-arch1-1
  • Terminal: Alacritty
  • Node: v25.2.1
  • RAM: 30 GB (Swap: 4 GB, 95% full during test runs)

Additional Notes

This is a first-time contribution. Happy to make any adjustments to the fix or documentation.

Process leaks in LSP tests are a common pattern - any test spawning subprocesses should use try/finally or afterEach hooks for guaranteed cleanup.

wvanderen avatar Jan 17 '26 23:01 wvanderen