fix(testing): bind QA docker port probes to loopback

This commit is contained in:
Vincent Koc
2026-06-17 14:17:56 +02:00
parent 4e2351dd4d
commit c05acc7a14
3 changed files with 44 additions and 34 deletions
@@ -1,6 +1,5 @@
// Qa Lab tests cover docker up plugin behavior.
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { createServer } from "node:net";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
@@ -8,31 +7,6 @@ import { runQaDockerUp } from "./docker-up.runtime.js";
type QaDockerUpDeps = NonNullable<Parameters<typeof runQaDockerUp>[1]>;
async function occupyPortOrAcceptExisting(port: number): Promise<{ close: () => Promise<void> }> {
const server = createServer();
const listening = await new Promise<boolean>((resolve, reject) => {
server.once("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
resolve(false);
return;
}
reject(error);
});
server.listen(port, "127.0.0.1", () => resolve(true));
});
return {
close: async () => {
if (!listening) {
return;
}
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
};
}
function createHealthyDockerDeps(calls: string[]): QaDockerUpDeps {
return {
async runCommand(command, args, cwd) {
@@ -163,7 +137,8 @@ describe("runQaDockerUp", () => {
const outputDir = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-"));
const gatewayPort = 18789;
const qaLabPort = 43124;
const resolveHostPort = vi.fn(async (preferredPort: number) => {
const resolveHostPort = vi.fn(async (preferredPort: number, pinned: boolean) => {
expect(pinned).toBe(false);
if (preferredPort === gatewayPort) {
return 28001;
}
@@ -172,16 +147,12 @@ describe("runQaDockerUp", () => {
}
return preferredPort;
});
const gatewayPortReservation = await occupyPortOrAcceptExisting(18789);
const qaLabPortReservation = await occupyPortOrAcceptExisting(43124);
try {
const result = await runQaDockerUp(
{
repoRoot: "/repo/openclaw",
outputDir,
gatewayPort,
qaLabPort,
skipUiBuild: true,
usePrebuiltImage: true,
},
@@ -202,9 +173,9 @@ describe("runQaDockerUp", () => {
expect(result.qaLabUrl).not.toBe(`http://127.0.0.1:${qaLabPort}`);
expect(result.gatewayUrl).toBe("http://127.0.0.1:28001/");
expect(result.qaLabUrl).toBe("http://127.0.0.1:28002");
expect(resolveHostPort).toHaveBeenCalledWith(gatewayPort, false);
expect(resolveHostPort).toHaveBeenCalledWith(qaLabPort, false);
} finally {
await gatewayPortReservation.close();
await qaLabPortReservation.close();
await rm(outputDir, { recursive: true, force: true });
}
});
+39
View File
@@ -2,6 +2,7 @@
* Tests QA runtime command loading and private CLI gating.
*/
import { Command } from "commander";
import { createServer } from "node:net";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
cleanupTempDirs,
@@ -36,6 +37,29 @@ describe("plugin-sdk qa-runtime", () => {
restorePrivateQaCliEnv(originalPrivateQaCli);
});
async function occupyLoopbackPort(): Promise<{ close: () => Promise<void>; port: number }> {
const server = createServer();
const port = await new Promise<number>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
reject(new Error("test server address unavailable"));
return;
}
resolve(address.port);
});
});
return {
port,
close: async () => {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
};
}
it("stays cold until the runtime seam is used", async () => {
const module = await import("./qa-runtime.js");
@@ -273,4 +297,19 @@ describe("plugin-sdk qa-runtime", () => {
expect(runCommand).toHaveBeenCalledTimes(2);
expect(sleepImpl).toHaveBeenCalledTimes(1);
});
it("resolves an unpinned QA Docker host port away from an occupied loopback default", async () => {
const module = await import("./qa-runtime.js");
const reservation = await occupyLoopbackPort();
try {
await expect(module.resolveQaDockerHostPort(reservation.port, true)).resolves.toBe(
reservation.port,
);
const fallbackPort = await module.resolveQaDockerHostPort(reservation.port, false);
expect(fallbackPort).toBeGreaterThan(0);
expect(fallbackPort).not.toBe(reservation.port);
} finally {
await reservation.close();
}
});
});
+1 -1
View File
@@ -395,7 +395,7 @@ async function findFreeQaDockerPort() {
return await new Promise<number>((resolve, reject) => {
const server = createServer();
server.once("error", reject);
server.listen(0, () => {
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close();