mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
fix(test): keep Gateway sandbox ports out of the client range (#151140)
Fix intermittent Canvas and dashboard sandbox failures in Linux real-Gateway tests. The shared fixture previously selected a kernel client-range Gateway port while the lazy sandbox used the adjacent, unchecked port. An outbound connection could occupy that sandbox port before its listener started. Use the existing port-block allocator to check Gateway and sandbox listeners outside the Linux client range, then retain exact Gateway-port reservation across CLI work and restarts. Explicit caller-owned ports remain caller-owned. Production behavior and browser waits are unchanged. Validation: exact-head CI run 35297154119 is green; recorded Linux proof covers the original collision, 78 helper tests, 20 Canvas file repetitions, and the complete 30-file/60-test real-Gateway suite. Committed-branch Codex review is scoped-clean and the landing rebase is patch-identical. Related: #151003, #150983
This commit is contained in:
parent
b10b53175f
commit
cc96aefe25
2 changed files with 47 additions and 9 deletions
|
|
@ -14,6 +14,39 @@ import { createDeferred, withTestTimeout } from "./promise.js";
|
|||
import { runQaGatewayFixture } from "./qa-gateway-cleanup.js";
|
||||
|
||||
describe("createOpenClawTestInstance acquisition", () => {
|
||||
it.skipIf(process.platform !== "linux")(
|
||||
"keeps Gateway and deferred sandbox listeners outside the kernel client-port range",
|
||||
async () => {
|
||||
const [low, high] = (await fs.readFile("/proc/sys/net/ipv4/ip_local_port_range", "utf8"))
|
||||
.trim()
|
||||
.split(/\s+/u)
|
||||
.map(Number);
|
||||
const instance = await createOpenClawTestInstance({ name: "sandbox-port-allocation" });
|
||||
const sandbox = net.createServer();
|
||||
await runQaGatewayFixture(
|
||||
async () => {
|
||||
for (const port of [instance.port, instance.port + 1]) {
|
||||
expect(port < low! || port > high!, `listener ${port} overlaps ${low}–${high}`).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
sandbox.once("error", reject);
|
||||
sandbox.listen(instance.port + 1, "127.0.0.1", resolve);
|
||||
});
|
||||
expect(sandbox.listening).toBe(true);
|
||||
},
|
||||
() =>
|
||||
sandbox.listening
|
||||
? new Promise<void>((resolve, reject) => {
|
||||
sandbox.close((error) => (error ? reject(error) : resolve()));
|
||||
})
|
||||
: undefined,
|
||||
() => instance.cleanup(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["state", "config", "rollback failure"] as const)(
|
||||
"joins and rolls back owner cancellation during %s acquisition",
|
||||
async (stage) => {
|
||||
|
|
@ -37,7 +70,9 @@ describe("createOpenClawTestInstance acquisition", () => {
|
|||
const allocated = await mkdtemp(...args);
|
||||
if (args[0].endsWith("instance-owner-cancel-")) {
|
||||
root = await fs.realpath(allocated);
|
||||
const address = serverSpy.mock.results[0]?.value.address();
|
||||
const address = serverSpy.mock.results
|
||||
.find((result) => result.type === "return" && result.value.listening)
|
||||
?.value.address();
|
||||
reservedPort = address && typeof address !== "string" ? address.port : undefined;
|
||||
if (stage === "state") {
|
||||
entered.resolve();
|
||||
|
|
@ -162,7 +197,9 @@ describe("createOpenClawTestInstance acquisition", () => {
|
|||
const mkdtemp = fs.mkdtemp;
|
||||
const allocationSpy = vi.spyOn(fs, "mkdtemp").mockImplementation(async (...args) => {
|
||||
if (args[0].endsWith("instance-wrapper-failure-")) {
|
||||
const address = serverSpy.mock.results[0]?.value.address();
|
||||
const address = serverSpy.mock.results
|
||||
.find((result) => result.type === "return" && result.value.listening)
|
||||
?.value.address();
|
||||
reservedPort = address && typeof address !== "string" ? address.port : undefined;
|
||||
expect(reservedPort).toBeTypeOf("number");
|
||||
if (stage === "state") {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
createOpenClawTestState,
|
||||
type OpenClawTestState,
|
||||
} from "../../src/test-utils/openclaw-test-state.js";
|
||||
import { getDeterministicFreePortBlock } from "../../src/test-utils/ports.js";
|
||||
import { sleep } from "../../src/utils.js";
|
||||
import { decodeUtf8Tail } from "./bounded-child-output.js";
|
||||
import { runQaGatewayFixture } from "./qa-gateway-cleanup.js";
|
||||
|
|
@ -263,7 +264,7 @@ async function resolveGatewayEntrypoint(cwd: string): Promise<string[]> {
|
|||
}
|
||||
|
||||
async function reserveGatewayPort(
|
||||
port = 0,
|
||||
port: number,
|
||||
verifyCleanup?: OpenClawTestInstanceOptions["verifyCleanup"],
|
||||
) {
|
||||
// A probe must not retain a connection that can delay release before spawn.
|
||||
|
|
@ -280,11 +281,7 @@ async function reserveGatewayPort(
|
|||
resolve();
|
||||
});
|
||||
});
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("failed to reserve gateway port");
|
||||
}
|
||||
return { port: address.port, release };
|
||||
return { release };
|
||||
} catch (error) {
|
||||
return await runQaGatewayFixture(
|
||||
async (): Promise<never> => {
|
||||
|
|
@ -716,7 +713,11 @@ export async function createOpenClawTestInstance(
|
|||
signal?.addEventListener("abort", closeAdmission, { once: true });
|
||||
try {
|
||||
signal?.throwIfAborted();
|
||||
port = options.port ?? (reservation = await reserveGatewayPort(0, options.verifyCleanup)).port;
|
||||
// The lazy sandbox uses port + 1; keep both listeners out of Linux's client-port pool.
|
||||
port = options.port ?? (await getDeterministicFreePortBlock({ offsets: [0, 1] }));
|
||||
if (options.port === undefined) {
|
||||
reservation = await reserveGatewayPort(port, options.verifyCleanup);
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
state = await createOpenClawTestState({
|
||||
label: options.name,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue