From 5a793e70cc9a99c5eabf9f03c413c168e6c2fa23 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Fri, 7 Aug 2026 10:01:01 +0700 Subject: [PATCH] fix(test): stop lane-runtime leaking a live server on every run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two `upLane qc option` tests booted a real stack and then only released the slot. `downLane` locates a service's pid file through the lane's slot directory, so releasing the slot first orphaned the child with nothing left able to reach it — one `python3 -m http.server` survived every run, holding a port from a pool that is only ten wide. Thirteen had accumulated; the eleventh run onwards fails with EPORTBUSY in whichever test boots next, which reads as an unrelated flake. Both tests now stop the stack before releasing the slot, and assert the port went quiet — so a teardown that breaks again fails here rather than leaking into the next run. A suite-level `after` covers the case a test throws before its own teardown; it runs before SUITE_ROOT is removed, since the pid files it needs live inside it. --- server/__tests__/lane-runtime.test.js | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/server/__tests__/lane-runtime.test.js b/server/__tests__/lane-runtime.test.js index b17e36c..e5820e7 100644 --- a/server/__tests__/lane-runtime.test.js +++ b/server/__tests__/lane-runtime.test.js @@ -29,7 +29,30 @@ const profileLib = require("../lib/lane-profile"); const runtime = require("../lib/lane-runtime"); const { isListening } = require("../lib/ports"); -after(() => fsMod.rmSync(SUITE_ROOT, { recursive: true, force: true })); +/** + * Lanes whose boot hook actually spawned a detached process. `harness_spawn` + * children outlive the hook by design, so a test that throws before its own + * `downLane` — or never calls one — leaves a real server holding a real port + * AFTER the suite exits. The port pool is only ten wide per name, so ten such + * runs exhaust it and every later boot fails with EPORTBUSY: a leak in these + * tests reads as a failure in whichever test runs next. + */ +const bootedLanes = new Set(); + +// One teardown, in this order on purpose: downLane reads the pid files under +// LANES_ROOT, which lives inside SUITE_ROOT — removing the tree first would +// leave nothing to kill the children with. +after(async () => { + for (const id of bootedLanes) { + try { + const lane = lanesLib.getLane(id); + if (lane) await runtime.downLane(lane); + } catch { + /* already stopped, or torn down by the test itself */ + } + } + fsMod.rmSync(SUITE_ROOT, { recursive: true, force: true }); +}); let laneSeq = 0; /** A lane row backed by a real directory, so profile lookup and mkdir work. */ @@ -274,6 +297,7 @@ describe("lifecycle", () => { const lane = makeLane(); serverProfile(lane, 19100); + bootedLanes.add(lane.id); const facts = await runtime.upLane(lanesLib.getLane(lane.id)); assert.equal(facts.available, true); assert.equal(facts.up, true); @@ -301,6 +325,7 @@ describe("lifecycle", () => { assert.equal(afterDown.up, false); assert.deepEqual(afterDown.services, []); + bootedLanes.delete(lane.id); slots.releaseSlot(lane.id); }); @@ -377,12 +402,22 @@ describe("upLane qc option", () => { ].join("\n"), }); + bootedLanes.add(lane.id); await runtime.upLane(lanesLib.getLane(lane.id), { qc: true }); const booted = lanesLib.getLane(lane.id); const markerPath = pathMod.join(booted.cwd, "qc-marker.txt"); const marker = fsMod.readFileSync(markerPath, "utf8"); assert.match(marker, /QC=from-qc/); + // Stop the stack BEFORE releasing the slot: downLane locates the pid file + // through the lane's slot directory, so a released slot orphans a running + // child with no way left to reach it. Asserting the port went quiet is what + // makes a broken teardown fail HERE instead of leaking a live server into + // the next run's port pool. + const bootedPort = booted.ports.web; + await runtime.downLane(lanesLib.getLane(lane.id)); + assert.equal(await isListening(bootedPort), false, "boot hook's child outlived downLane"); + bootedLanes.delete(lane.id); slots.releaseSlot(lane.id); }); @@ -408,12 +443,17 @@ describe("upLane qc option", () => { ].join("\n"), }); + bootedLanes.add(lane.id); await runtime.upLane(lanesLib.getLane(lane.id)); const booted = lanesLib.getLane(lane.id); const markerPath = pathMod.join(booted.cwd, "qc-marker-no-qc.txt"); const marker = fsMod.readFileSync(markerPath, "utf8"); assert.match(marker, /QC=not-set/); + const bootedPort = booted.ports.web; + await runtime.downLane(lanesLib.getLane(lane.id)); + assert.equal(await isListening(bootedPort), false, "boot hook's child outlived downLane"); + bootedLanes.delete(lane.id); slots.releaseSlot(lane.id); }); });