mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
test: harden ci-sensitive unit suites
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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