From ed4e1394f0bb44126f6989a711f305bb1e4ecab8 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Thu, 26 Feb 2026 09:40:07 +0200 Subject: [PATCH] test(memory): isolate MCP baseline per test --- .../test/memory/mcp-lifecycle.test.ts | 228 +++++++++--------- 1 file changed, 111 insertions(+), 117 deletions(-) diff --git a/packages/opencode/test/memory/mcp-lifecycle.test.ts b/packages/opencode/test/memory/mcp-lifecycle.test.ts index 66a4955df12..509eac0dc8f 100644 --- a/packages/opencode/test/memory/mcp-lifecycle.test.ts +++ b/packages/opencode/test/memory/mcp-lifecycle.test.ts @@ -1,157 +1,151 @@ -import { describe, test, expect, afterEach } from "bun:test" +import { describe, test, expect } from "bun:test" import path from "path" import { Instance } from "../../src/project/instance" import { MCP } from "../../src/mcp" import { tmpdir } from "../fixture/fixture" -import { - PROJECT_ROOT, - snapshotDescendants, - assertNoOrphans, - forceKillAll, - stableHeapMB, -} from "./helper" +import { PROJECT_ROOT, snapshotDescendants, assertNoOrphans, forceKillAll, stableHeapMB } from "./helper" const FAKE_MCP_SERVER = path.join(PROJECT_ROOT, "test/fixture/mcp/fake-mcp-server.js") -let beforePids: Set = new Set() - describe("memory: MCP lifecycle", () => { - afterEach(async () => { + const cleanup = async (baseline: Set, name: string) => { try { const afterPids = await snapshotDescendants(process.pid) - const orphans: number[] = [] - for (const pid of afterPids) { - if (!beforePids.has(pid)) orphans.push(pid) - } + const orphans = [...afterPids].filter((pid) => !baseline.has(pid)) forceKillAll(orphans) - } catch { - // Best-effort cleanup + } catch (err) { + console.log(`[${name}] cleanup failed: ${err}`) } - }) + } - test( - "MCP.add closes existing client before overwriting", - async () => { - await using tmp = await tmpdir({ git: true }) - - beforePids = await snapshotDescendants(process.pid) + test("MCP.add closes existing client before overwriting", async () => { + await using tmp = await tmpdir({ git: true }) + const baseline = await snapshotDescendants(process.pid) + try { await Instance.provide({ directory: tmp.path, fn: async () => { - // Add first server - await MCP.add("lifecycle-test", { - type: "local", - command: ["bun", FAKE_MCP_SERVER], - }) - - const afterFirstPids = await snapshotDescendants(process.pid) - const firstCount = [...afterFirstPids].filter((p) => !beforePids.has(p)).length - - // Add second server with same key — should close first - await MCP.add("lifecycle-test", { - type: "local", - command: ["bun", FAKE_MCP_SERVER], - }) - - await Bun.sleep(500) - const afterSecondPids = await snapshotDescendants(process.pid) - const secondCount = [...afterSecondPids].filter((p) => !beforePids.has(p)).length - - // Process count should stay roughly constant (old killed, new spawned) - // Allow +1 for timing of process teardown - expect(secondCount).toBeLessThanOrEqual(firstCount + 1) - - await Instance.dispose() - }, - }) - - await Bun.sleep(500) - const afterPids = await snapshotDescendants(process.pid) - await assertNoOrphans(beforePids, afterPids) - }, - 120_000, - ) - - test( - "MCP.connect closes existing client before reconnecting", - async () => { - await using tmp = await tmpdir({ - git: true, - config: { - mcp: { - "connect-test": { + try { + // Add first server + await MCP.add("lifecycle-test", { type: "local", command: ["bun", FAKE_MCP_SERVER], - }, - }, - }, - }) + }) - beforePids = await snapshotDescendants(process.pid) + const afterFirstPids = await snapshotDescendants(process.pid) + const firstCount = [...afterFirstPids].filter((p) => !baseline.has(p)).length - await Instance.provide({ - directory: tmp.path, - fn: async () => { - // First connect (state init may already connect from config) - await MCP.connect("connect-test") + // Add second server with same key — should close first + await MCP.add("lifecycle-test", { + type: "local", + command: ["bun", FAKE_MCP_SERVER], + }) - const afterFirstPids = await snapshotDescendants(process.pid) - const firstCount = [...afterFirstPids].filter((p) => !beforePids.has(p)).length + await Bun.sleep(500) + const afterSecondPids = await snapshotDescendants(process.pid) + const secondCount = [...afterSecondPids].filter((p) => !baseline.has(p)).length - // Second connect — should close existing before reconnecting - await MCP.connect("connect-test") - - await Bun.sleep(500) - const afterSecondPids = await snapshotDescendants(process.pid) - const secondCount = [...afterSecondPids].filter((p) => !beforePids.has(p)).length - - // Should not accumulate processes - expect(secondCount).toBeLessThanOrEqual(firstCount + 1) - - await Instance.dispose() + // Process count should stay roughly constant (old killed, new spawned) + // Allow +1 for timing of process teardown + expect(secondCount).toBeLessThanOrEqual(firstCount + 1) + } finally { + await Instance.dispose() + } }, }) await Bun.sleep(500) const afterPids = await snapshotDescendants(process.pid) - await assertNoOrphans(beforePids, afterPids) - }, - 120_000, - ) + await assertNoOrphans(baseline, afterPids) + } finally { + await cleanup(baseline, "MCP.add closes existing client before overwriting") + } + }, 120_000) - test( - "MCP.tools() does not leak memory", - async () => { - await using tmp = await tmpdir({ git: true }) + test("MCP.connect closes existing client before reconnecting", async () => { + await using tmp = await tmpdir({ + git: true, + config: { + mcp: { + "connect-test": { + type: "local", + command: ["bun", FAKE_MCP_SERVER], + }, + }, + }, + }) + const baseline = await snapshotDescendants(process.pid) + try { await Instance.provide({ directory: tmp.path, fn: async () => { - await MCP.add("tools-test", { - type: "local", - command: ["bun", FAKE_MCP_SERVER], - }) + try { + // First connect (state init may already connect from config) + await MCP.connect("connect-test") - // Warm up - await MCP.tools() + const afterFirstPids = await snapshotDescendants(process.pid) + const firstCount = [...afterFirstPids].filter((p) => !baseline.has(p)).length - const baseline = await stableHeapMB() + // Second connect — should close existing before reconnecting + await MCP.connect("connect-test") - for (let i = 0; i < 50; i++) { - await MCP.tools() + await Bun.sleep(500) + const afterSecondPids = await snapshotDescendants(process.pid) + const secondCount = [...afterSecondPids].filter((p) => !baseline.has(p)).length + + // Should not accumulate processes + expect(secondCount).toBeLessThanOrEqual(firstCount + 1) + } finally { + await Instance.dispose() } - - const after = await stableHeapMB() - const growth = after - baseline - - console.log(` MCP.tools() 50x growth: ${growth.toFixed(2)} MB`) - expect(growth).toBeLessThan(5) - - await Instance.dispose() }, }) - }, - 60_000, - ) + + await Bun.sleep(500) + const afterPids = await snapshotDescendants(process.pid) + await assertNoOrphans(baseline, afterPids) + } finally { + await cleanup(baseline, "MCP.connect closes existing client before reconnecting") + } + }, 120_000) + + test("MCP.tools() does not leak memory", async () => { + await using tmp = await tmpdir({ git: true }) + const baseline = await snapshotDescendants(process.pid) + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + try { + await MCP.add("tools-test", { + type: "local", + command: ["bun", FAKE_MCP_SERVER], + }) + + // Warm up + await MCP.tools() + + const baseline = await stableHeapMB() + + for (let i = 0; i < 50; i++) { + await MCP.tools() + } + + const after = await stableHeapMB() + const growth = after - baseline + + console.log(` MCP.tools() 50x growth: ${growth.toFixed(2)} MB`) + expect(growth).toBeLessThan(5) + } finally { + await Instance.dispose() + } + }, + }) + } finally { + await cleanup(baseline, "MCP.tools() does not leak memory") + } + }, 60_000) })