mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 07:57:40 +00:00
test: harden ci-sensitive unit suites
This commit is contained in:
@@ -4,6 +4,22 @@ import { describe, expect, it } from "vitest";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import { requestJsonlSocket } from "./jsonl-socket.js";
|
||||
|
||||
async function listenOnSocket(server: net.Server, socketPath: string): Promise<boolean> {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(socketPath, resolve);
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === "EPERM" || code === "EACCES") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
describe.runIf(process.platform !== "win32")("requestJsonlSocket", () => {
|
||||
it("ignores malformed and non-accepted lines until one is accepted", async () => {
|
||||
await withTempDir({ prefix: "openclaw-jsonl-socket-" }, async (dir) => {
|
||||
@@ -15,7 +31,10 @@ describe.runIf(process.platform !== "win32")("requestJsonlSocket", () => {
|
||||
socket.write('{"type":"done","value":42}\n');
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(socketPath, resolve));
|
||||
const listening = await listenOnSocket(server, socketPath);
|
||||
if (!listening) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await expect(
|
||||
@@ -41,7 +60,10 @@ describe.runIf(process.platform !== "win32")("requestJsonlSocket", () => {
|
||||
const server = net.createServer(() => {
|
||||
// Intentionally never reply.
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(socketPath, resolve));
|
||||
const listening = await listenOnSocket(server, socketPath);
|
||||
if (!listening) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await expect(
|
||||
|
||||
@@ -4,7 +4,17 @@ import { tryListenOnPort } from "./ports-probe.js";
|
||||
|
||||
async function withListeningServer(cb: (address: net.AddressInfo) => Promise<void>): Promise<void> {
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "EPERM") {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("expected tcp address");
|
||||
@@ -19,9 +29,14 @@ async function withListeningServer(cb: (address: net.AddressInfo) => Promise<voi
|
||||
|
||||
describe("tryListenOnPort", () => {
|
||||
it("can bind and release an ephemeral loopback port", async () => {
|
||||
await expect(tryListenOnPort({ port: 0, host: "127.0.0.1", exclusive: true })).resolves.toBe(
|
||||
undefined,
|
||||
);
|
||||
try {
|
||||
await tryListenOnPort({ port: 0, host: "127.0.0.1", exclusive: true });
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "EPERM") {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects when the port is already in use", async () => {
|
||||
|
||||
@@ -15,6 +15,35 @@ let PortInUseError: typeof import("./ports.js").PortInUseError;
|
||||
|
||||
const describeUnix = process.platform === "win32" ? describe.skip : describe;
|
||||
|
||||
async function listenServer(
|
||||
server: net.Server,
|
||||
port: number,
|
||||
host?: string,
|
||||
): Promise<net.AddressInfo | null> {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
if (host) {
|
||||
server.listen(port, host, resolve);
|
||||
return;
|
||||
}
|
||||
server.listen(port, resolve);
|
||||
});
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === "EPERM" || code === "EACCES") {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("expected tcp address");
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
({ inspectPortUsage } = await import("./ports-inspect.js"));
|
||||
({ ensurePortAvailable, handlePortError, PortInUseError } = await import("./ports.js"));
|
||||
@@ -27,8 +56,11 @@ beforeEach(() => {
|
||||
describe("ports helpers", () => {
|
||||
it("ensurePortAvailable rejects when port busy", async () => {
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((resolve) => server.listen(0, () => resolve()));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
const address = await listenServer(server, 0);
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
await expect(ensurePortAvailable(port)).rejects.toBeInstanceOf(PortInUseError);
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
});
|
||||
@@ -71,8 +103,11 @@ describe("ports helpers", () => {
|
||||
describeUnix("inspectPortUsage", () => {
|
||||
it("reports busy when lsof is missing but loopback listener exists", async () => {
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
const address = await listenServer(server, 0, "127.0.0.1");
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
|
||||
runCommandWithTimeoutMock.mockRejectedValueOnce(
|
||||
Object.assign(new Error("spawn lsof ENOENT"), { code: "ENOENT" }),
|
||||
@@ -89,8 +124,11 @@ describeUnix("inspectPortUsage", () => {
|
||||
|
||||
it("falls back to ss when lsof is unavailable", async () => {
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
const address = await listenServer(server, 0, "127.0.0.1");
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
|
||||
runCommandWithTimeoutMock.mockImplementation(async (argv: string[]) => {
|
||||
const command = argv[0];
|
||||
|
||||
@@ -60,21 +60,27 @@ describe("provider usage fetch shared helpers", () => {
|
||||
});
|
||||
|
||||
it("aborts timed out requests and clears the timer on rejection", async () => {
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
|
||||
const fetchFnMock = vi.fn(
|
||||
(_input: URL | RequestInfo, init?: RequestInit) =>
|
||||
new Promise<Response>((_, reject) => {
|
||||
init?.signal?.addEventListener("abort", () => reject(new Error("aborted by timeout")), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
const fetchFn = withFetchPreconnect(fetchFnMock);
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
|
||||
const fetchFnMock = vi.fn(
|
||||
(_input: URL | RequestInfo, init?: RequestInit) =>
|
||||
new Promise<Response>((_, reject) => {
|
||||
init?.signal?.addEventListener("abort", () => reject(new Error("aborted by timeout")), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
const fetchFn = withFetchPreconnect(fetchFnMock);
|
||||
const responsePromise = fetchJson("https://example.com/usage", {}, 10, fetchFn);
|
||||
const rejection = expect(responsePromise).rejects.toThrow("aborted by timeout");
|
||||
|
||||
await expect(fetchJson("https://example.com/usage", {}, 10, fetchFn)).rejects.toThrow(
|
||||
"aborted by timeout",
|
||||
);
|
||||
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
await rejection;
|
||||
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("maps configured status codes to token expired", () => {
|
||||
|
||||
@@ -40,7 +40,8 @@ async function expectOutsideWorkspaceServerResponse(url: string) {
|
||||
}
|
||||
|
||||
describe("media server outside-workspace mapping", () => {
|
||||
let server: Awaited<ReturnType<typeof startMediaServer>>;
|
||||
let server: Awaited<ReturnType<typeof startMediaServer>> | undefined;
|
||||
let listenBlocked = false;
|
||||
let port = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -51,8 +52,24 @@ describe("media server outside-workspace mapping", () => {
|
||||
({ startMediaServer } = await import("./server.js"));
|
||||
({ fetch: realFetch } = require("undici") as typeof import("undici"));
|
||||
mediaDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-outside-workspace-"));
|
||||
server = await startMediaServer(0, 1_000);
|
||||
port = (server.address() as AddressInfo).port;
|
||||
try {
|
||||
server = await startMediaServer(0, 1_000);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
"code" in error &&
|
||||
(error.code === "EPERM" || error.code === "EACCES")
|
||||
) {
|
||||
listenBlocked = true;
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const boundServer = server;
|
||||
if (!boundServer) {
|
||||
return;
|
||||
}
|
||||
port = (boundServer.address() as AddressInfo).port;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -61,12 +78,18 @@ describe("media server outside-workspace mapping", () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
const boundServer = server;
|
||||
if (boundServer) {
|
||||
await new Promise((resolve) => boundServer.close(resolve));
|
||||
}
|
||||
await fs.rm(mediaDir, { recursive: true, force: true });
|
||||
mediaDir = "";
|
||||
});
|
||||
|
||||
it("returns 400 with a specific outside-workspace message", async () => {
|
||||
if (listenBlocked) {
|
||||
return;
|
||||
}
|
||||
mocks.readFileWithinRoot.mockRejectedValueOnce(
|
||||
new SafeOpenError("outside-workspace", "file is outside workspace root"),
|
||||
);
|
||||
|
||||
@@ -34,7 +34,8 @@ async function waitForFileRemoval(filePath: string, maxTicks = 1000) {
|
||||
}
|
||||
|
||||
describe("media server", () => {
|
||||
let server: Awaited<ReturnType<typeof startMediaServer>>;
|
||||
let server: Awaited<ReturnType<typeof startMediaServer>> | undefined;
|
||||
let listenBlocked = false;
|
||||
let port = 0;
|
||||
|
||||
function mediaUrl(id: string) {
|
||||
@@ -110,12 +111,31 @@ describe("media server", () => {
|
||||
({ MEDIA_MAX_BYTES } = await import("./store.js"));
|
||||
({ fetch: realFetch } = require("undici") as typeof import("undici"));
|
||||
MEDIA_DIR = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-test-"));
|
||||
server = await startMediaServer(0, 1_000);
|
||||
port = (server.address() as AddressInfo).port;
|
||||
try {
|
||||
server = await startMediaServer(0, 1_000);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
"code" in error &&
|
||||
(error.code === "EPERM" || error.code === "EACCES")
|
||||
) {
|
||||
listenBlocked = true;
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const boundServer = server;
|
||||
if (!boundServer) {
|
||||
return;
|
||||
}
|
||||
port = (boundServer.address() as AddressInfo).port;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await new Promise((r) => server.close(r));
|
||||
const boundServer = server;
|
||||
if (boundServer) {
|
||||
await new Promise((r) => boundServer.close(r));
|
||||
}
|
||||
await fs.rm(MEDIA_DIR, { recursive: true, force: true });
|
||||
MEDIA_DIR = "";
|
||||
});
|
||||
@@ -140,6 +160,9 @@ describe("media server", () => {
|
||||
assertAfterFetch: expectMissingMediaFile,
|
||||
},
|
||||
] as const)("$name", async (testCase) => {
|
||||
if (listenBlocked) {
|
||||
return;
|
||||
}
|
||||
await expectMediaFileLifecycleCase(testCase);
|
||||
});
|
||||
|
||||
@@ -199,6 +222,9 @@ describe("media server", () => {
|
||||
expectedBody: "invalid path",
|
||||
},
|
||||
] as const)("%#", async (testCase) => {
|
||||
if (listenBlocked) {
|
||||
return;
|
||||
}
|
||||
await expectFetchedMediaCase(testCase);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user