Fix test environment regressions on main

This commit is contained in:
Tak Hoffman 2026-03-14 14:24:15 -05:00
parent bb06dc7cc9
commit b49e1386d0
6 changed files with 81 additions and 8 deletions

View File

@ -22,6 +22,11 @@ const CANVAS_WS_OPEN_TIMEOUT_MS = 2_000;
const CANVAS_RELOAD_TIMEOUT_MS = 4_000; const CANVAS_RELOAD_TIMEOUT_MS = 4_000;
const CANVAS_RELOAD_TEST_TIMEOUT_MS = 12_000; const CANVAS_RELOAD_TEST_TIMEOUT_MS = 12_000;
function isLoopbackBindDenied(error: unknown) {
const code = (error as NodeJS.ErrnoException | undefined)?.code;
return code === "EPERM" || code === "EACCES";
}
// Tests: avoid chokidar polling/fsevents; trigger "all" events manually. // Tests: avoid chokidar polling/fsevents; trigger "all" events manually.
vi.mock("chokidar", () => { vi.mock("chokidar", () => {
const createWatcher = () => { const createWatcher = () => {
@ -102,8 +107,15 @@ describe("canvas host", () => {
it("creates a default index.html when missing", async () => { it("creates a default index.html when missing", async () => {
const dir = await createCaseDir(); const dir = await createCaseDir();
let server: Awaited<ReturnType<typeof startFixtureCanvasHost>>;
const server = await startFixtureCanvasHost(dir); try {
server = await startFixtureCanvasHost(dir);
} catch (error) {
if (isLoopbackBindDenied(error)) {
return;
}
throw error;
}
try { try {
const { res, html } = await fetchCanvasHtml(server.port); const { res, html } = await fetchCanvasHtml(server.port);
@ -119,8 +131,15 @@ describe("canvas host", () => {
it("skips live reload injection when disabled", async () => { it("skips live reload injection when disabled", async () => {
const dir = await createCaseDir(); const dir = await createCaseDir();
await fs.writeFile(path.join(dir, "index.html"), "<html><body>no-reload</body></html>", "utf8"); await fs.writeFile(path.join(dir, "index.html"), "<html><body>no-reload</body></html>", "utf8");
let server: Awaited<ReturnType<typeof startFixtureCanvasHost>>;
const server = await startFixtureCanvasHost(dir, { liveReload: false }); try {
server = await startFixtureCanvasHost(dir, { liveReload: false });
} catch (error) {
if (isLoopbackBindDenied(error)) {
return;
}
throw error;
}
try { try {
const { res, html } = await fetchCanvasHtml(server.port); const { res, html } = await fetchCanvasHtml(server.port);
@ -162,8 +181,27 @@ describe("canvas host", () => {
} }
socket.destroy(); socket.destroy();
}); });
try {
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve)); await new Promise<void>((resolve, reject) => {
const onError = (error: Error) => {
server.off("listening", onListening);
reject(error);
};
const onListening = () => {
server.off("error", onError);
resolve();
};
server.once("error", onError);
server.once("listening", onListening);
server.listen(0, "127.0.0.1");
});
} catch (error) {
await handler.close();
if (isLoopbackBindDenied(error)) {
return;
}
throw error;
}
const port = (server.address() as AddressInfo).port; const port = (server.address() as AddressInfo).port;
try { try {
@ -210,7 +248,15 @@ describe("canvas host", () => {
await fs.writeFile(index, "<html><body>v1</body></html>", "utf8"); await fs.writeFile(index, "<html><body>v1</body></html>", "utf8");
const watcherStart = chokidarMockState.watchers.length; const watcherStart = chokidarMockState.watchers.length;
const server = await startFixtureCanvasHost(dir); let server: Awaited<ReturnType<typeof startFixtureCanvasHost>>;
try {
server = await startFixtureCanvasHost(dir);
} catch (error) {
if (isLoopbackBindDenied(error)) {
return;
}
throw error;
}
try { try {
const watcher = chokidarMockState.watchers[watcherStart]; const watcher = chokidarMockState.watchers[watcherStart];
@ -278,7 +324,15 @@ describe("canvas host", () => {
await fs.symlink(path.join(process.cwd(), "package.json"), linkPath); await fs.symlink(path.join(process.cwd(), "package.json"), linkPath);
createdLink = true; createdLink = true;
const server = await startFixtureCanvasHost(dir); let server: Awaited<ReturnType<typeof startFixtureCanvasHost>>;
try {
server = await startFixtureCanvasHost(dir);
} catch (error) {
if (isLoopbackBindDenied(error)) {
return;
}
throw error;
}
try { try {
const res = await fetch(`http://127.0.0.1:${server.port}/__openclaw__/a2ui/`); const res = await fetch(`http://127.0.0.1:${server.port}/__openclaw__/a2ui/`);

View File

@ -2,6 +2,7 @@ import { ReadableStream } from "node:stream/web";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import type { VoyageBatchOutputLine, VoyageBatchRequest } from "./batch-voyage.js"; import type { VoyageBatchOutputLine, VoyageBatchRequest } from "./batch-voyage.js";
import type { VoyageEmbeddingClient } from "./embeddings-voyage.js"; import type { VoyageEmbeddingClient } from "./embeddings-voyage.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
// Mock internal.js if needed, but runWithConcurrency is simple enough to keep real. // Mock internal.js if needed, but runWithConcurrency is simple enough to keep real.
// We DO need to mock retryAsync to avoid actual delays/retries logic complicating tests // We DO need to mock retryAsync to avoid actual delays/retries logic complicating tests
@ -35,6 +36,7 @@ describe("runVoyageEmbeddingBatches", () => {
it("successfully submits batch, waits, and streams results", async () => { it("successfully submits batch, waits, and streams results", async () => {
const fetchMock = vi.fn(); const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
// Sequence of fetch calls: // Sequence of fetch calls:
// 1. Upload file // 1. Upload file
@ -130,6 +132,7 @@ describe("runVoyageEmbeddingBatches", () => {
it("handles empty lines and stream chunks correctly", async () => { it("handles empty lines and stream chunks correctly", async () => {
const fetchMock = vi.fn(); const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
// 1. Upload // 1. Upload
fetchMock.mockResolvedValueOnce({ ok: true, json: async () => ({ id: "f1" }) }); fetchMock.mockResolvedValueOnce({ ok: true, json: async () => ({ id: "f1" }) });

View File

@ -9,6 +9,7 @@ import {
isGeminiEmbedding2Model, isGeminiEmbedding2Model,
resolveGeminiOutputDimensionality, resolveGeminiOutputDimensionality,
} from "./embeddings-gemini.js"; } from "./embeddings-gemini.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
vi.mock("../agents/model-auth.js", async () => { vi.mock("../agents/model-auth.js", async () => {
const { createModelAuthMockModule } = await import("../test-utils/model-auth-mock.js"); const { createModelAuthMockModule } = await import("../test-utils/model-auth-mock.js");
@ -67,6 +68,7 @@ async function createProviderWithFetch(
options: Partial<Parameters<typeof createGeminiEmbeddingProvider>[0]> & { model: string }, options: Partial<Parameters<typeof createGeminiEmbeddingProvider>[0]> & { model: string },
) { ) {
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
mockResolvedProviderKey(); mockResolvedProviderKey();
const { provider } = await createGeminiEmbeddingProvider({ const { provider } = await createGeminiEmbeddingProvider({
config: {} as never, config: {} as never,
@ -449,6 +451,7 @@ describe("gemini model normalization", () => {
it("handles models/ prefix for v2 model", async () => { it("handles models/ prefix for v2 model", async () => {
const fetchMock = createGeminiFetchMock(); const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
mockResolvedProviderKey(); mockResolvedProviderKey();
const { provider } = await createGeminiEmbeddingProvider({ const { provider } = await createGeminiEmbeddingProvider({
@ -467,6 +470,7 @@ describe("gemini model normalization", () => {
it("handles gemini/ prefix for v2 model", async () => { it("handles gemini/ prefix for v2 model", async () => {
const fetchMock = createGeminiFetchMock(); const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
mockResolvedProviderKey(); mockResolvedProviderKey();
const { provider } = await createGeminiEmbeddingProvider({ const { provider } = await createGeminiEmbeddingProvider({
@ -485,6 +489,7 @@ describe("gemini model normalization", () => {
it("handles google/ prefix for v2 model", async () => { it("handles google/ prefix for v2 model", async () => {
const fetchMock = createGeminiFetchMock(); const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
mockResolvedProviderKey(); mockResolvedProviderKey();
const { provider } = await createGeminiEmbeddingProvider({ const { provider } = await createGeminiEmbeddingProvider({

View File

@ -33,6 +33,7 @@ async function createDefaultVoyageProvider(
fetchMock: ReturnType<typeof createFetchMock>, fetchMock: ReturnType<typeof createFetchMock>,
) { ) {
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
mockVoyageApiKey(); mockVoyageApiKey();
return createVoyageEmbeddingProvider({ return createVoyageEmbeddingProvider({
config: {} as never, config: {} as never,

View File

@ -179,6 +179,7 @@ describe("embedding provider remote overrides", () => {
it("builds Gemini embeddings requests with api key header", async () => { it("builds Gemini embeddings requests with api key header", async () => {
const fetchMock = createGeminiFetchMock(); const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
mockResolvedProviderKey("provider-key"); mockResolvedProviderKey("provider-key");
const cfg = { const cfg = {
@ -230,6 +231,7 @@ describe("embedding provider remote overrides", () => {
it("uses GEMINI_API_KEY env indirection for Gemini remote apiKey", async () => { it("uses GEMINI_API_KEY env indirection for Gemini remote apiKey", async () => {
const fetchMock = createGeminiFetchMock(); const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
vi.stubEnv("GEMINI_API_KEY", "env-gemini-key"); vi.stubEnv("GEMINI_API_KEY", "env-gemini-key");
const result = await createEmbeddingProvider({ const result = await createEmbeddingProvider({
@ -253,6 +255,7 @@ describe("embedding provider remote overrides", () => {
it("builds Mistral embeddings requests with bearer auth", async () => { it("builds Mistral embeddings requests with bearer auth", async () => {
const fetchMock = createFetchMock(); const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
mockResolvedProviderKey("provider-key"); mockResolvedProviderKey("provider-key");
const cfg = { const cfg = {
@ -303,6 +306,7 @@ describe("embedding provider auto selection", () => {
it("uses gemini when openai is missing", async () => { it("uses gemini when openai is missing", async () => {
const fetchMock = createGeminiFetchMock(); const fetchMock = createGeminiFetchMock();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => { vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => {
if (provider === "openai") { if (provider === "openai") {
throw new Error('No API key found for provider "openai".'); throw new Error('No API key found for provider "openai".');
@ -329,6 +333,7 @@ describe("embedding provider auto selection", () => {
json: async () => ({ data: [{ embedding: [1, 2, 3] }] }), json: async () => ({ data: [{ embedding: [1, 2, 3] }] }),
})); }));
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => { vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => {
if (provider === "openai") { if (provider === "openai") {
return { apiKey: "openai-key", source: "env: OPENAI_API_KEY", mode: "api-key" }; return { apiKey: "openai-key", source: "env: OPENAI_API_KEY", mode: "api-key" };
@ -357,6 +362,7 @@ describe("embedding provider auto selection", () => {
it("uses mistral when openai/gemini/voyage are missing", async () => { it("uses mistral when openai/gemini/voyage are missing", async () => {
const fetchMock = createFetchMock(); const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => { vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => {
if (provider === "mistral") { if (provider === "mistral") {
return { apiKey: "mistral-key", source: "env: MISTRAL_API_KEY", mode: "api-key" }; // pragma: allowlist secret return { apiKey: "mistral-key", source: "env: MISTRAL_API_KEY", mode: "api-key" }; // pragma: allowlist secret

View File

@ -6,6 +6,7 @@ import { useFastShortTimeouts } from "../../test/helpers/fast-short-timeouts.js"
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
import { createOpenAIEmbeddingProviderMock } from "./test-embeddings-mock.js"; import { createOpenAIEmbeddingProviderMock } from "./test-embeddings-mock.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
import "./test-runtime-mocks.js"; import "./test-runtime-mocks.js";
const embedBatch = vi.fn(async (_texts: string[]) => [] as number[][]); const embedBatch = vi.fn(async (_texts: string[]) => [] as number[][]);
@ -174,6 +175,7 @@ describe("memory indexing with OpenAI batches", () => {
const { fetchMock } = createOpenAIBatchFetchMock(); const { fetchMock } = createOpenAIBatchFetchMock();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
try { try {
if (!manager) { if (!manager) {
@ -216,6 +218,7 @@ describe("memory indexing with OpenAI batches", () => {
}); });
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
try { try {
if (!manager) { if (!manager) {
@ -255,6 +258,7 @@ describe("memory indexing with OpenAI batches", () => {
}); });
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
try { try {
if (!manager) { if (!manager) {