test(memory): isolate MCP baseline per test

This commit is contained in:
Alex Alecu
2026-02-26 09:40:07 +02:00
parent 05c0847eb3
commit ed4e1394f0
@@ -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<number> = new Set()
describe("memory: MCP lifecycle", () => {
afterEach(async () => {
const cleanup = async (baseline: Set<number>, 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)
})