test: harden ci-sensitive unit suites

This commit is contained in:
ImLukeF
2026-04-01 20:17:28 +11:00
parent 4e63dc0b1c
commit 101c31f5e1
9 changed files with 248 additions and 49 deletions

View File

@@ -2,6 +2,25 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
import * as authModule from "../../../../src/agents/model-auth.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
vi.mock("../../../../src/infra/net/fetch-guard.js", () => ({
fetchWithSsrFGuard: async (params: {
url: string;
init?: RequestInit;
fetchImpl?: typeof fetch;
}) => {
const fetchImpl = params.fetchImpl ?? globalThis.fetch;
if (!fetchImpl) {
throw new Error("fetch is not available");
}
const response = await fetchImpl(params.url, params.init);
return {
response,
finalUrl: params.url,
release: async () => {},
};
},
}));
vi.mock("../../../../src/agents/model-auth.js", async () => {
const { createModelAuthMockModule } =
await import("../../../../src/test-utils/model-auth-mock.js");
@@ -24,6 +43,10 @@ const createGeminiBatchFetchMock = (count: number, embeddingValues = [1, 2, 3])
}),
}));
function installFetchMock(fetchMock: typeof globalThis.fetch) {
globalThis.fetch = fetchMock;
}
function readFirstFetchRequest(fetchMock: { mock: { calls: unknown[][] } }) {
const [url, init] = fetchMock.mock.calls[0] ?? [];
return { url, init: init as RequestInit | undefined };
@@ -87,7 +110,7 @@ async function createProviderWithFetch(
fetchMock: GeminiFetchMock,
options: Partial<Parameters<typeof createGeminiEmbeddingProvider>[0]> & { model: string },
) {
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockResolvedProviderKey();
const { provider } = await createGeminiEmbeddingProvider({
@@ -470,7 +493,7 @@ describe("gemini-embedding-2-preview provider", () => {
describe("gemini model normalization", () => {
it("handles models/ prefix for v2 model", async () => {
const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockResolvedProviderKey();
@@ -489,7 +512,7 @@ describe("gemini model normalization", () => {
it("handles gemini/ prefix for v2 model", async () => {
const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockResolvedProviderKey();
@@ -508,7 +531,7 @@ describe("gemini model normalization", () => {
it("handles google/ prefix for v2 model", async () => {
const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockResolvedProviderKey();
@@ -527,7 +550,7 @@ describe("gemini model normalization", () => {
it("defaults to gemini-embedding-001 when model is empty", async () => {
const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockResolvedProviderKey();
const { provider, client } = await createGeminiEmbeddingProvider({

View File

@@ -2,6 +2,25 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { type FetchMock, withFetchPreconnect } from "../../../../src/test-utils/fetch-mock.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
vi.mock("../../../../src/infra/net/fetch-guard.js", () => ({
fetchWithSsrFGuard: async (params: {
url: string;
init?: RequestInit;
fetchImpl?: typeof fetch;
}) => {
const fetchImpl = params.fetchImpl ?? globalThis.fetch;
if (!fetchImpl) {
throw new Error("fetch is not available");
}
const response = await fetchImpl(params.url, params.init);
return {
response,
finalUrl: params.url,
release: async () => {},
};
},
}));
vi.mock("../../../../src/agents/model-auth.js", async () => {
const { createModelAuthMockModule } =
await import("../../../../src/test-utils/model-auth-mock.js");
@@ -19,6 +38,10 @@ const createFetchMock = () => {
return withFetchPreconnect(fetchMock);
};
function installFetchMock(fetchMock: typeof globalThis.fetch) {
globalThis.fetch = fetchMock;
}
let authModule: typeof import("../../../../src/agents/model-auth.js");
let createVoyageEmbeddingProvider: typeof import("./embeddings-voyage.js").createVoyageEmbeddingProvider;
let normalizeVoyageModel: typeof import("./embeddings-voyage.js").normalizeVoyageModel;
@@ -44,7 +67,7 @@ async function createDefaultVoyageProvider(
model: string,
fetchMock: ReturnType<typeof createFetchMock>,
) {
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockVoyageApiKey();
return createVoyageEmbeddingProvider({
@@ -91,7 +114,7 @@ describe("voyage embedding provider", () => {
it("respects remote overrides for baseUrl and apiKey", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
const result = await createVoyageEmbeddingProvider({

View File

@@ -3,6 +3,25 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
vi.mock("../../../../src/infra/net/fetch-guard.js", () => ({
fetchWithSsrFGuard: async (params: {
url: string;
init?: RequestInit;
fetchImpl?: typeof fetch;
}) => {
const fetchImpl = params.fetchImpl ?? globalThis.fetch;
if (!fetchImpl) {
throw new Error("fetch is not available");
}
const response = await fetchImpl(params.url, params.init);
return {
response,
finalUrl: params.url,
release: async () => {},
};
},
}));
const createFetchMock = () =>
vi.fn(async (_input?: unknown, _init?: unknown) => ({
ok: true,
@@ -17,6 +36,10 @@ const createGeminiFetchMock = () =>
json: async () => ({ embedding: { values: [1, 2, 3] } }),
}));
function installFetchMock(fetchMock: typeof globalThis.fetch) {
globalThis.fetch = fetchMock;
}
function readFirstFetchRequest(fetchMock: { mock: { calls: unknown[][] } }) {
const [url, init] = fetchMock.mock.calls[0] ?? [];
return { url, init: init as RequestInit | undefined };
@@ -99,7 +122,7 @@ function createAutoProvider(model = "") {
describe("embedding provider remote overrides", () => {
it("uses remote baseUrl/apiKey and merges headers", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockResolvedProviderKey("provider-key");
@@ -149,7 +172,7 @@ describe("embedding provider remote overrides", () => {
it("falls back to resolved api key when remote apiKey is blank", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockResolvedProviderKey("provider-key");
@@ -185,7 +208,7 @@ describe("embedding provider remote overrides", () => {
it("builds Gemini embeddings requests with api key header", async () => {
const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockResolvedProviderKey("provider-key");
@@ -237,7 +260,7 @@ describe("embedding provider remote overrides", () => {
it("uses GEMINI_API_KEY env indirection for Gemini remote apiKey", async () => {
const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
vi.stubEnv("GEMINI_API_KEY", "env-gemini-key");
@@ -261,7 +284,7 @@ describe("embedding provider remote overrides", () => {
it("builds Mistral embeddings requests with bearer auth", async () => {
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockResolvedProviderKey("provider-key");
@@ -304,7 +327,7 @@ describe("embedding provider auto selection", () => {
status: 200,
json: async () => ({ data: [{ embedding: [1, 2, 3] }] }),
}));
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => {
if (provider === "openai") {
@@ -392,7 +415,7 @@ describe("embedding provider auto selection", () => {
vi.resetAllMocks();
vi.unstubAllGlobals();
const fetchMock = testCase.fetchMockFactory();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) =>
testCase.resolveApiKey(provider),
@@ -412,7 +435,7 @@ describe("embedding provider local fallback", () => {
mockMissingLocalEmbeddingDependency();
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockResolvedProviderKey("provider-key");

View File

@@ -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(

View File

@@ -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 () => {

View File

@@ -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];

View File

@@ -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", () => {

View File

@@ -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"),
);

View File

@@ -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);
});
});