fix(test): stop lane-runtime leaking a live server on every run

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.
This commit is contained in:
2026-08-07 10:01:01 +07:00
parent 201eae68bb
commit 5a793e70cc
+41 -1
View File
@@ -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);
});
});