test: harden vitest no-isolate coverage

This commit is contained in:
Peter Steinberger
2026-03-22 10:47:52 -07:00
parent 719bfb46ff
commit 1ceaad18a6
39 changed files with 827 additions and 295 deletions

View File

@@ -68,9 +68,11 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
through the real `run.ts` / `compact.ts` paths; helper-only tests are not a
sufficient substitute for those integration paths.
- Pool note:
- OpenClaw uses Vitest `vmForks` on Node 22, 23, and 24 for faster unit shards.
- On Node 25+, OpenClaw automatically falls back to regular `forks` until the repo is re-validated there.
- Override manually with `OPENCLAW_TEST_VM_FORKS=0` (force `forks`) or `OPENCLAW_TEST_VM_FORKS=1` (force `vmForks`).
- OpenClaw uses Vitest `forks` by default for unit shards.
- `pnpm test` also defaults to `--isolate=false` at the wrapper level for faster file startup.
- Opt back into Vitest file isolation with `OPENCLAW_TEST_ISOLATE=1 pnpm test`.
- `OPENCLAW_TEST_NO_ISOLATE=0` or `OPENCLAW_TEST_NO_ISOLATE=false` also force isolated runs.
- `OPENCLAW_TEST_VM_FORKS=1` remains an opt-in experiment on Node 22, 23, and 24 only.
### E2E (gateway smoke)
@@ -78,8 +80,8 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- Config: `vitest.e2e.config.ts`
- Files: `src/**/*.e2e.test.ts`, `test/**/*.e2e.test.ts`
- Runtime defaults:
- Uses Vitest `vmForks` for faster file startup.
- Uses adaptive workers (CI: 2-4, local: 4-8).
- Uses Vitest `forks` for deterministic cross-file isolation.
- Uses adaptive workers (CI: up to 2, local: 1 by default).
- Runs in silent mode by default to reduce console I/O overhead.
- Useful overrides:
- `OPENCLAW_E2E_WORKERS=<n>` to force worker count (capped at 16).

View File

@@ -1,9 +1,15 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("@mariozechner/pi-ai/oauth", () => ({
getOAuthApiKey: vi.fn(),
getOAuthProviders: vi.fn(() => []),
}));
vi.mock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
"@mariozechner/pi-ai/oauth",
);
return {
...actual,
getOAuthApiKey: vi.fn(),
getOAuthProviders: vi.fn(() => []),
};
});
vi.mock("openclaw/plugin-sdk/provider-models", () => ({
normalizeModelCompat: (model: Record<string, unknown>) => model,

View File

@@ -94,7 +94,12 @@ const isMacMiniProfile = testProfile === "macmini";
// Preserve OPENCLAW_TEST_VM_FORKS=1 as the explicit override/debug escape hatch.
const supportsVmForks = Number.isFinite(nodeMajor) ? nodeMajor <= 24 : true;
const useVmForks = process.env.OPENCLAW_TEST_VM_FORKS === "1" && supportsVmForks;
const disableIsolation = process.env.OPENCLAW_TEST_NO_ISOLATE === "1";
const forceIsolation =
process.env.OPENCLAW_TEST_ISOLATE === "1" || process.env.OPENCLAW_TEST_ISOLATE === "true";
const disableIsolation =
!forceIsolation &&
process.env.OPENCLAW_TEST_NO_ISOLATE !== "0" &&
process.env.OPENCLAW_TEST_NO_ISOLATE !== "false";
const includeGatewaySuite = process.env.OPENCLAW_TEST_INCLUDE_GATEWAY === "1";
const includeExtensionsSuite = process.env.OPENCLAW_TEST_INCLUDE_EXTENSIONS === "1";
// Even on low-memory hosts, keep the isolated lane split so files like
@@ -699,10 +704,40 @@ const createTargetedEntry = (owner, isolated, filters) => {
],
};
};
const formatPerFileEntryName = (owner, file) => {
const baseName = path
.basename(file)
.replace(/\.live\.test\.ts$/u, "")
.replace(/\.e2e\.test\.ts$/u, "")
.replace(/\.test\.ts$/u, "");
return `${owner}-${baseName}`;
};
const createPerFileTargetedEntry = (file) => {
const target = inferTarget(file);
const owner = isThreadSingletonUnitFile(file)
? "unit-threads"
: isVmForkSingletonUnitFile(file)
? "unit-vmforks"
: target.owner;
return {
...createTargetedEntry(owner, target.isolated, [file]),
name: formatPerFileEntryName(owner, file),
};
};
const targetedEntries = (() => {
if (passthroughFileFilters.length === 0) {
return [];
}
if (disableIsolation) {
const matchedFiles = passthroughFileFilters.flatMap((fileFilter) => {
const resolved = resolveFilterMatches(fileFilter);
if (resolved.length > 0) {
return resolved;
}
return [normalizeRepoPath(fileFilter)];
});
return [...new Set(matchedFiles)].map((file) => createPerFileTargetedEntry(file));
}
const groups = passthroughFileFilters.reduce((acc, fileFilter) => {
const matchedFiles = resolveFilterMatches(fileFilter);
if (matchedFiles.length === 0) {
@@ -738,6 +773,9 @@ const targetedEntries = (() => {
return createTargetedEntry(owner, mode === "isolated", [...new Set(filters)]);
});
})();
if (disableIsolation && passthroughFileFilters.length === 0) {
runs = allKnownUnitFiles.map((file) => createPerFileTargetedEntry(file));
}
// Node 25 local runs still show cross-process worker shutdown contention even
// after moving the known heavy files into singleton lanes.
const topLevelParallelEnabled =
@@ -745,8 +783,17 @@ const topLevelParallelEnabled =
testProfile !== "serial" &&
!(!isCI && nodeMajor >= 25) &&
!isMacMiniProfile;
const defaultTopLevelParallelLimit =
testProfile === "serial"
const defaultTopLevelParallelLimit = disableIsolation
? isCI
? isWindows
? 2
: 4
: highMemLocalHost
? Math.min(16, hostCpuCount)
: lowMemLocalHost
? Math.min(8, hostCpuCount)
: Math.min(12, hostCpuCount)
: testProfile === "serial"
? 1
: testProfile === "low"
? lowMemLocalHost

View File

@@ -1,3 +1,5 @@
import { setTimeout as scheduleNativeTimeout } from "node:timers";
import { setTimeout as sleep } from "node:timers/promises";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js";
@@ -148,6 +150,7 @@ function extractRuntimeOptionsFromUpserts(): Array<AcpSessionRuntimeOptions | un
describe("AcpSessionManager", () => {
beforeEach(() => {
vi.useRealTimers();
hoisted.listAcpSessionEntriesMock.mockReset().mockResolvedValue([]);
hoisted.readAcpSessionEntryMock.mockReset();
hoisted.upsertAcpSessionMetaMock.mockReset().mockResolvedValue(null);
@@ -240,7 +243,7 @@ describe("AcpSessionManager", () => {
inFlight += 1;
maxInFlight = Math.max(maxInFlight, inFlight);
try {
await new Promise((resolve) => setTimeout(resolve, 10));
await sleep(10);
yield { type: "done" };
} finally {
inFlight -= 1;
@@ -321,7 +324,7 @@ describe("AcpSessionManager", () => {
(error) => ({ status: "rejected" as const, error }),
),
new Promise<{ status: "pending" }>((resolve) => {
setTimeout(() => resolve({ status: "pending" }), 100);
scheduleNativeTimeout(() => resolve({ status: "pending" }), 100);
}),
]);
@@ -538,7 +541,7 @@ describe("AcpSessionManager", () => {
inFlight += 1;
maxInFlight = Math.max(maxInFlight, inFlight);
try {
await new Promise((resolve) => setTimeout(resolve, 15));
await sleep(15);
yield { type: "done" as const };
} finally {
inFlight -= 1;

View File

@@ -13,8 +13,10 @@ const hoisted = vi.hoisted(() => {
};
});
vi.mock("@mariozechner/pi-ai", () => {
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const original = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...original,
streamAnthropic: (model: unknown, context: unknown, options: unknown) =>
hoisted.streamAnthropicMock(model, context, options),
};

View File

@@ -29,13 +29,19 @@ const {
buildProviderAuthDoctorHintWithPluginMock: vi.fn(async () => undefined),
}));
vi.mock("@mariozechner/pi-ai/oauth", () => ({
getOAuthApiKey: getOAuthApiKeyMock,
getOAuthProviders: () => [
{ id: "openai-codex", envApiKey: "OPENAI_API_KEY", oauthTokenEnv: "OPENAI_OAUTH_TOKEN" }, // pragma: allowlist secret
{ id: "anthropic", envApiKey: "ANTHROPIC_API_KEY", oauthTokenEnv: "ANTHROPIC_OAUTH_TOKEN" }, // pragma: allowlist secret
],
}));
vi.mock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
"@mariozechner/pi-ai/oauth",
);
return {
...actual,
getOAuthApiKey: getOAuthApiKeyMock,
getOAuthProviders: () => [
{ id: "openai-codex", envApiKey: "OPENAI_API_KEY", oauthTokenEnv: "OPENAI_OAUTH_TOKEN" }, // pragma: allowlist secret
{ id: "anthropic", envApiKey: "ANTHROPIC_API_KEY", oauthTokenEnv: "ANTHROPIC_OAUTH_TOKEN" }, // pragma: allowlist secret
],
};
});
vi.mock("../../plugins/provider-runtime.runtime.js", () => ({
refreshProviderOAuthCredentialWithPlugin: refreshProviderOAuthCredentialWithPluginMock,

View File

@@ -16,9 +16,13 @@ const resolveSessionAuthProfileOverrideMock = vi.fn();
const getActiveEmbeddedRunSnapshotMock = vi.fn();
const diagDebugMock = vi.fn();
vi.mock("@mariozechner/pi-ai", () => ({
streamSimple: (...args: unknown[]) => streamSimpleMock(...args),
}));
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const original = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...original,
streamSimple: (...args: unknown[]) => streamSimpleMock(...args),
};
});
vi.mock("@mariozechner/pi-coding-agent", () => ({
SessionManager: {

View File

@@ -192,10 +192,16 @@ export async function loadCompactHooksHarness(): Promise<{
};
});
vi.doMock("@mariozechner/pi-ai/oauth", () => ({
getOAuthApiKey: vi.fn(),
getOAuthProviders: vi.fn(() => []),
}));
vi.doMock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
"@mariozechner/pi-ai/oauth",
);
return {
...actual,
getOAuthApiKey: vi.fn(),
getOAuthProviders: vi.fn(() => []),
};
});
vi.doMock("@mariozechner/pi-coding-agent", () => ({
AuthStorage: class AuthStorage {},

View File

@@ -2,12 +2,16 @@ import type { Model } from "@mariozechner/pi-ai";
import { describe, expect, it, vi } from "vitest";
import { runExtraParamsCase } from "./extra-params.test-support.js";
vi.mock("@mariozechner/pi-ai", () => ({
streamSimple: vi.fn(() => ({
push: vi.fn(),
result: vi.fn(),
})),
}));
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const original = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...original,
streamSimple: vi.fn(() => ({
push: vi.fn(),
result: vi.fn(),
})),
};
});
describe("extra-params: Google thinking payload compatibility", () => {
it("strips negative thinking budgets and fills Gemini 3.1 thinkingLevel", () => {

View File

@@ -2,12 +2,16 @@ import type { Model } from "@mariozechner/pi-ai";
import { describe, expect, it, vi } from "vitest";
import { runExtraParamsCase } from "./extra-params.test-support.js";
vi.mock("@mariozechner/pi-ai", () => ({
streamSimple: vi.fn(() => ({
push: vi.fn(),
result: vi.fn(),
})),
}));
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const original = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...original,
streamSimple: vi.fn(() => ({
push: vi.fn(),
result: vi.fn(),
})),
};
});
describe("extra-params: xAI tool payload compatibility", () => {
it("strips function.strict for xai providers", () => {

View File

@@ -4,12 +4,16 @@ import type { OpenClawConfig } from "../../config/config.js";
import { runExtraParamsCase } from "./extra-params.test-support.js";
// Mock streamSimple for testing
vi.mock("@mariozechner/pi-ai", () => ({
streamSimple: vi.fn(() => ({
push: vi.fn(),
result: vi.fn(),
})),
}));
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const original = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...original,
streamSimple: vi.fn(() => ({
push: vi.fn(),
result: vi.fn(),
})),
};
});
type ToolStreamCase = {
applyProvider: string;

View File

@@ -10,10 +10,16 @@ vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
};
});
vi.mock("@mariozechner/pi-ai/oauth", () => ({
getOAuthApiKey: () => undefined,
getOAuthProviders: () => [],
}));
vi.mock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
"@mariozechner/pi-ai/oauth",
);
return {
...actual,
getOAuthApiKey: () => undefined,
getOAuthProviders: () => [],
};
});
import { createOpenClawCodingTools } from "./pi-tools.js";

View File

@@ -34,10 +34,16 @@ vi.mock("../config/config.js", async (importOriginal) => {
};
});
vi.mock("@mariozechner/pi-ai/oauth", () => ({
getOAuthApiKey: () => "",
getOAuthProviders: () => [],
}));
vi.mock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
"@mariozechner/pi-ai/oauth",
);
return {
...actual,
getOAuthApiKey: () => "",
getOAuthProviders: () => [],
};
});
vi.mock("./subagent-registry.js", () => ({
countActiveRunsForSession: () => 0,

View File

@@ -7,9 +7,13 @@ const resolveDefaultModelForAgent = vi.hoisted(() => vi.fn());
const resolveModelAsync = vi.hoisted(() => vi.fn());
const prepareModelForSimpleCompletion = vi.hoisted(() => vi.fn());
vi.mock("@mariozechner/pi-ai", () => ({
completeSimple,
}));
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const original = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...original,
completeSimple,
};
});
vi.mock("../../agents/model-auth.js", () => ({
getApiKeyForModel,

View File

@@ -1,11 +1,16 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { beforeAll, describe, expect, it } from "vitest";
import { defaultRuntime } from "../runtime.js";
import { withStateDirEnv } from "../test-helpers/state-dir-env.js";
import { createCanvasHostHandler } from "./server.js";
describe("canvas host state dir defaults", () => {
let createCanvasHostHandler: typeof import("./server.js").createCanvasHostHandler;
beforeAll(async () => {
({ createCanvasHostHandler } = await import("./server.js"));
});
it("uses OPENCLAW_STATE_DIR for the default canvas root", async () => {
await withStateDirEnv("openclaw-canvas-state-", async ({ stateDir }) => {
const handler = await createCanvasHostHandler({

View File

@@ -3,20 +3,20 @@ import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { WebSocket } from "ws";
import {
clearTimeout as clearNativeTimeout,
setTimeout as scheduleNativeTimeout,
} from "node:timers";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { rawDataToString } from "../infra/ws.js";
import { defaultRuntime } from "../runtime.js";
import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH, injectCanvasLiveReload } from "./a2ui.js";
import { createCanvasHostHandler, startCanvasHost } from "./server.js";
const chokidarMockState = vi.hoisted(() => ({
watchers: [] as Array<{
on: (event: string, cb: (...args: unknown[]) => void) => unknown;
close: () => Promise<void>;
__emit: (event: string, ...args: unknown[]) => void;
}>,
}));
type MockWatcher = {
on: (event: string, cb: (...args: unknown[]) => void) => MockWatcher;
close: () => Promise<void>;
__emit: (event: string, ...args: unknown[]) => void;
};
const CANVAS_WS_OPEN_TIMEOUT_MS = 2_000;
const CANVAS_RELOAD_TIMEOUT_MS = 4_000;
@@ -27,11 +27,11 @@ function isLoopbackBindDenied(error: unknown) {
return code === "EPERM" || code === "EACCES";
}
// Tests: avoid chokidar polling/fsevents; trigger "all" events manually.
vi.mock("chokidar", () => {
function createMockWatcherState() {
const watchers: MockWatcher[] = [];
const createWatcher = () => {
const handlers = new Map<string, Array<(...args: unknown[]) => void>>();
const api = {
const api: MockWatcher = {
on: (event: string, cb: (...args: unknown[]) => void) => {
const list = handlers.get(event) ?? [];
list.push(cb);
@@ -45,22 +45,26 @@ vi.mock("chokidar", () => {
}
},
};
chokidarMockState.watchers.push(api);
watchers.push(api);
return api;
};
const watch = () => createWatcher();
return {
default: { watch },
watch,
watchers,
watchFactory: () => createWatcher(),
};
});
}
describe("canvas host", () => {
const quietRuntime = {
...defaultRuntime,
log: (..._args: Parameters<typeof console.log>) => {},
};
let createCanvasHostHandler: typeof import("./server.js").createCanvasHostHandler;
let startCanvasHost: typeof import("./server.js").startCanvasHost;
let realFetch: typeof import("undici").fetch;
let WebSocketClient: typeof import("ws").WebSocket;
let WebSocketServerClass: typeof import("ws").WebSocketServer;
let watcherState: ReturnType<typeof createMockWatcherState>;
let fixtureRoot = "";
let fixtureCount = 0;
@@ -80,19 +84,34 @@ describe("canvas host", () => {
port: 0,
listenHost: "127.0.0.1",
allowInTests: true,
watchFactory: watcherState.watchFactory as unknown as Parameters<
typeof startCanvasHost
>[0]["watchFactory"],
webSocketServerClass: WebSocketServerClass,
...overrides,
});
const fetchCanvasHtml = async (port: number) => {
const res = await fetch(`http://127.0.0.1:${port}${CANVAS_HOST_PATH}/`);
const res = await realFetch(`http://127.0.0.1:${port}${CANVAS_HOST_PATH}/`);
const html = await res.text();
return { res, html };
};
beforeAll(async () => {
({ createCanvasHostHandler, startCanvasHost } = await import("./server.js"));
const undiciModule = await vi.importActual<typeof import("undici")>("undici");
realFetch = undiciModule.fetch;
const wsModule = await vi.importActual<typeof import("ws")>("ws");
WebSocketClient = wsModule.WebSocket;
WebSocketServerClass = wsModule.WebSocketServer;
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-fixtures-"));
});
beforeEach(() => {
vi.useRealTimers();
watcherState = createMockWatcherState();
});
afterAll(async () => {
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
@@ -147,7 +166,7 @@ describe("canvas host", () => {
expect(html).toContain("no-reload");
expect(html).not.toContain(CANVAS_WS_PATH);
const wsRes = await fetch(`http://127.0.0.1:${server.port}${CANVAS_WS_PATH}`);
const wsRes = await realFetch(`http://127.0.0.1:${server.port}${CANVAS_WS_PATH}`);
expect(wsRes.status).toBe(404);
} finally {
await server.close();
@@ -163,6 +182,10 @@ describe("canvas host", () => {
rootDir: dir,
basePath: CANVAS_HOST_PATH,
allowInTests: true,
watchFactory: watcherState.watchFactory as unknown as Parameters<
typeof createCanvasHostHandler
>[0]["watchFactory"],
webSocketServerClass: WebSocketServerClass,
});
const server = createServer((req, res) => {
@@ -205,13 +228,13 @@ describe("canvas host", () => {
const port = (server.address() as AddressInfo).port;
try {
const res = await fetch(`http://127.0.0.1:${port}${CANVAS_HOST_PATH}/`);
const res = await realFetch(`http://127.0.0.1:${port}${CANVAS_HOST_PATH}/`);
const html = await res.text();
expect(res.status).toBe(200);
expect(html).toContain("v1");
expect(html).toContain(CANVAS_WS_PATH);
const miss = await fetch(`http://127.0.0.1:${port}/`);
const miss = await realFetch(`http://127.0.0.1:${port}/`);
expect(miss.status).toBe(404);
} finally {
await new Promise<void>((resolve, reject) =>
@@ -247,7 +270,7 @@ describe("canvas host", () => {
const index = path.join(dir, "index.html");
await fs.writeFile(index, "<html><body>v1</body></html>", "utf8");
const watcherStart = chokidarMockState.watchers.length;
const watcherStart = watcherState.watchers.length;
let server: Awaited<ReturnType<typeof startFixtureCanvasHost>>;
try {
server = await startFixtureCanvasHost(dir);
@@ -259,7 +282,7 @@ describe("canvas host", () => {
}
try {
const watcher = chokidarMockState.watchers[watcherStart];
const watcher = watcherState.watchers[watcherStart];
expect(watcher).toBeTruthy();
const { res, html } = await fetchCanvasHtml(server.port);
@@ -267,29 +290,29 @@ describe("canvas host", () => {
expect(html).toContain("v1");
expect(html).toContain(CANVAS_WS_PATH);
const ws = new WebSocket(`ws://127.0.0.1:${server.port}${CANVAS_WS_PATH}`);
const ws = new WebSocketClient(`ws://127.0.0.1:${server.port}${CANVAS_WS_PATH}`);
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(
const timer = scheduleNativeTimeout(
() => reject(new Error("ws open timeout")),
CANVAS_WS_OPEN_TIMEOUT_MS,
);
ws.on("open", () => {
clearTimeout(timer);
clearNativeTimeout(timer);
resolve();
});
ws.on("error", (err) => {
clearTimeout(timer);
clearNativeTimeout(timer);
reject(err);
});
});
const msg = new Promise<string>((resolve, reject) => {
const timer = setTimeout(
const timer = scheduleNativeTimeout(
() => reject(new Error("reload timeout")),
CANVAS_RELOAD_TIMEOUT_MS,
);
ws.on("message", (data) => {
clearTimeout(timer);
clearNativeTimeout(timer);
resolve(rawDataToString(data));
});
});
@@ -297,7 +320,7 @@ describe("canvas host", () => {
await fs.writeFile(index, "<html><body>v2</body></html>", "utf8");
watcher.__emit("all", "change", index);
expect(await msg).toBe("reload");
ws.close();
ws.terminate();
} finally {
await server.close();
}
@@ -335,24 +358,24 @@ describe("canvas host", () => {
throw error;
}
const res = await fetch(`http://127.0.0.1:${server.port}/__openclaw__/a2ui/`);
const res = await realFetch(`http://127.0.0.1:${server.port}/__openclaw__/a2ui/`);
const html = await res.text();
expect(res.status).toBe(200);
expect(html).toContain("openclaw-a2ui-host");
expect(html).toContain("openclawCanvasA2UIAction");
const bundleRes = await fetch(
const bundleRes = await realFetch(
`http://127.0.0.1:${server.port}/__openclaw__/a2ui/a2ui.bundle.js`,
);
const js = await bundleRes.text();
expect(bundleRes.status).toBe(200);
expect(js).toContain("openclawA2UI");
const traversalRes = await fetch(
const traversalRes = await realFetch(
`http://127.0.0.1:${server.port}${A2UI_PATH}/%2e%2e%2fpackage.json`,
);
expect(traversalRes.status).toBe(404);
expect(await traversalRes.text()).toBe("not found");
const symlinkRes = await fetch(`http://127.0.0.1:${server.port}${A2UI_PATH}/${linkName}`);
const symlinkRes = await realFetch(`http://127.0.0.1:${server.port}${A2UI_PATH}/${linkName}`);
expect(symlinkRes.status).toBe(404);
expect(await symlinkRes.text()).toBe("not found");
} finally {

View File

@@ -4,6 +4,10 @@ import http, { type IncomingMessage, type Server, type ServerResponse } from "no
import type { Socket } from "node:net";
import path from "node:path";
import type { Duplex } from "node:stream";
import {
clearTimeout as clearNativeTimeout,
setTimeout as scheduleNativeTimeout,
} from "node:timers";
import chokidar from "chokidar";
import { type WebSocket, WebSocketServer } from "ws";
import { resolveStateDir } from "../config/paths.js";
@@ -26,6 +30,8 @@ export type CanvasHostOpts = {
listenHost?: string;
allowInTests?: boolean;
liveReload?: boolean;
watchFactory?: typeof chokidar.watch;
webSocketServerClass?: typeof WebSocketServer;
};
export type CanvasHostServerOpts = CanvasHostOpts & {
@@ -45,6 +51,8 @@ export type CanvasHostHandlerOpts = {
basePath?: string;
allowInTests?: boolean;
liveReload?: boolean;
watchFactory?: typeof chokidar.watch;
webSocketServerClass?: typeof WebSocketServer;
};
export type CanvasHostHandler = {
@@ -224,7 +232,8 @@ export async function createCanvasHostHandler(
const reloadDebounceMs = testMode ? 12 : 75;
const writeStabilityThresholdMs = testMode ? 12 : 75;
const writePollIntervalMs = testMode ? 5 : 10;
const wss = liveReload ? new WebSocketServer({ noServer: true }) : null;
const WebSocketServerClass = opts.webSocketServerClass ?? WebSocketServer;
const wss = liveReload ? new WebSocketServerClass({ noServer: true }) : null;
const sockets = new Set<WebSocket>();
if (wss) {
wss.on("connection", (ws) => {
@@ -248,9 +257,9 @@ export async function createCanvasHostHandler(
};
const scheduleReload = () => {
if (debounce) {
clearTimeout(debounce);
clearNativeTimeout(debounce);
}
debounce = setTimeout(() => {
debounce = scheduleNativeTimeout(() => {
debounce = null;
broadcastReload();
}, reloadDebounceMs);
@@ -258,8 +267,9 @@ export async function createCanvasHostHandler(
};
let watcherClosed = false;
const watchFactory = opts.watchFactory ?? chokidar.watch.bind(chokidar);
const watcher = liveReload
? chokidar.watch(rootReal, {
? watchFactory(rootReal, {
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: writeStabilityThresholdMs,
@@ -385,10 +395,17 @@ export async function createCanvasHostHandler(
handleUpgrade,
close: async () => {
if (debounce) {
clearTimeout(debounce);
clearNativeTimeout(debounce);
}
watcherClosed = true;
await watcher?.close().catch(() => {});
for (const ws of sockets) {
try {
ws.terminate?.();
} catch {
// ignore
}
}
if (wss) {
await new Promise<void>((resolve) => wss.close(() => resolve()));
}
@@ -409,6 +426,8 @@ export async function startCanvasHost(opts: CanvasHostServerOpts): Promise<Canva
basePath: CANVAS_HOST_PATH,
allowInTests: opts.allowInTests,
liveReload: opts.liveReload,
watchFactory: opts.watchFactory,
webSocketServerClass: opts.webSocketServerClass,
}));
const ownsHandler = opts.ownsHandler ?? opts.handler === undefined;

View File

@@ -1,26 +1,54 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
readFileUtf8AndCleanup,
stubFetchResponse,
} from "../test-utils/camera-url-test-helpers.js";
import { withTempDir } from "../test-utils/temp-dir.js";
import {
cameraTempPath,
parseCameraClipPayload,
parseCameraSnapPayload,
writeCameraClipPayloadToFile,
writeBase64ToFile,
writeUrlToFile,
} from "./nodes-camera.js";
import { parseScreenRecordPayload, screenRecordTempPath } from "./nodes-screen.js";
const fetchGuardMocks = vi.hoisted(() => ({
fetchWithSsrFGuard: vi.fn(async (params: { url: string }) => {
return {
response: await globalThis.fetch(params.url),
finalUrl: params.url,
release: async () => {},
};
}),
}));
vi.mock("../infra/net/fetch-guard.js", () => ({
fetchWithSsrFGuard: fetchGuardMocks.fetchWithSsrFGuard,
}));
let cameraTempPath: typeof import("./nodes-camera.js").cameraTempPath;
let parseCameraClipPayload: typeof import("./nodes-camera.js").parseCameraClipPayload;
let parseCameraSnapPayload: typeof import("./nodes-camera.js").parseCameraSnapPayload;
let writeCameraClipPayloadToFile: typeof import("./nodes-camera.js").writeCameraClipPayloadToFile;
let writeBase64ToFile: typeof import("./nodes-camera.js").writeBase64ToFile;
let writeUrlToFile: typeof import("./nodes-camera.js").writeUrlToFile;
let parseScreenRecordPayload: typeof import("./nodes-screen.js").parseScreenRecordPayload;
let screenRecordTempPath: typeof import("./nodes-screen.js").screenRecordTempPath;
async function withCameraTempDir<T>(run: (dir: string) => Promise<T>): Promise<T> {
return await withTempDir("openclaw-test-", run);
}
describe("nodes camera helpers", () => {
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
({
cameraTempPath,
parseCameraClipPayload,
parseCameraSnapPayload,
writeCameraClipPayloadToFile,
writeBase64ToFile,
writeUrlToFile,
} = await import("./nodes-camera.js"));
({ parseScreenRecordPayload, screenRecordTempPath } = await import("./nodes-screen.js"));
});
it("parses camera.snap payload", () => {
expect(
parseCameraSnapPayload({

View File

@@ -35,8 +35,8 @@ vi.mock("../runtime.js", () => ({
defaultRuntime: runtime,
}));
const { registerQrCli } = await import("./qr-cli.js");
const { registerMaintenanceCommands } = await import("./program/register.maintenance.js");
let registerQrCli: typeof import("./qr-cli.js").registerQrCli;
let registerMaintenanceCommands: typeof import("./program/register.maintenance.js").registerMaintenanceCommands;
function createGatewayTokenRefFixture() {
return {
@@ -100,6 +100,12 @@ describe("cli integration: qr + dashboard token SecretRef", () => {
]);
});
beforeAll(async () => {
vi.resetModules();
({ registerQrCli } = await import("./qr-cli.js"));
({ registerMaintenanceCommands } = await import("./program/register.maintenance.js"));
});
afterAll(() => {
envSnapshot.restore();
});

View File

@@ -8,9 +8,15 @@ const mocks = vi.hoisted(() => ({
formatOpenAIOAuthTlsPreflightFix: vi.fn(),
}));
vi.mock("@mariozechner/pi-ai/oauth", () => ({
loginOpenAICodex: mocks.loginOpenAICodex,
}));
vi.mock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
"@mariozechner/pi-ai/oauth",
);
return {
...actual,
loginOpenAICodex: mocks.loginOpenAICodex,
};
});
vi.mock("../plugins/provider-openai-codex-oauth-tls.js", () => ({
runOpenAIOAuthTlsPreflight: mocks.runOpenAIOAuthTlsPreflight,

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { setTimeout as sleep } from "node:timers/promises";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { withTempHome } from "./home-env.test-harness.js";
import { createConfigIO } from "./io.js";
@@ -14,12 +15,16 @@ async function waitForPersistedSecret(configPath: string, expectedSecret: string
if (parsed.commands?.ownerDisplaySecret === expectedSecret) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 5));
await sleep(5);
}
throw new Error("timed out waiting for ownerDisplaySecret persistence");
}
describe("config io owner display secret autofill", () => {
beforeEach(() => {
vi.useRealTimers();
});
it("auto-generates and persists commands.ownerDisplaySecret in hash mode", async () => {
await withTempHome("openclaw-owner-display-secret-", async (home) => {
const configPath = path.join(home, ".openclaw", "openclaw.json");

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { setTimeout as scheduleNativeTimeout } from "node:timers";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createCronStoreHarness } from "./service.test-harness.js";
import { loadCronStore, resolveCronStorePath, saveCronStore } from "./store.js";
import type { CronStoreFile } from "./types.js";
@@ -149,6 +150,10 @@ describe("cron store", () => {
describe("saveCronStore", () => {
const dummyStore: CronStoreFile = { version: 1, jobs: [] };
beforeEach(() => {
vi.useRealTimers();
});
it("persists and round-trips a store file", async () => {
const { storePath } = await makeStorePath();
await saveCronStore(storePath, dummyStore);
@@ -158,11 +163,10 @@ describe("saveCronStore", () => {
it("retries rename on EBUSY then succeeds", async () => {
const { storePath } = await makeStorePath();
const realSetTimeout = globalThis.setTimeout;
const setTimeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation(((handler: TimerHandler, _timeout?: number, ...args: unknown[]) =>
realSetTimeout(handler, 0, ...args)) as typeof setTimeout);
scheduleNativeTimeout(handler, 0, ...args)) as typeof setTimeout);
const origRename = fs.rename.bind(fs);
let ebusyCount = 0;
const spy = vi.spyOn(fs, "rename").mockImplementation(async (src, dest) => {

View File

@@ -435,17 +435,21 @@ describe("tilde expansion in file tools", () => {
it("keeps tilde expansion behavior aligned", async () => {
const { expandHomePrefix } = await import("./home-dir.js");
const originalHome = process.env.HOME;
const originalOpenClawHome = process.env.OPENCLAW_HOME;
const fakeHome = path.resolve(path.sep, "tmp", "fake-home-test");
process.env.HOME = fakeHome;
process.env.OPENCLAW_HOME = fakeHome;
try {
const result = expandHomePrefix("~/file.txt");
expect(path.normalize(result)).toBe(path.join(fakeHome, "file.txt"));
} finally {
process.env.HOME = originalHome;
process.env.OPENCLAW_HOME = originalOpenClawHome;
}
const root = await tempDirs.make("openclaw-tilde-test-");
process.env.HOME = root;
process.env.OPENCLAW_HOME = root;
try {
await fs.writeFile(path.join(root, "hello.txt"), "tilde-works");
const result = await openFileWithinRoot({
@@ -466,6 +470,7 @@ describe("tilde expansion in file tools", () => {
expect(content).toBe("tilde-write-works");
} finally {
process.env.HOME = originalHome;
process.env.OPENCLAW_HOME = originalOpenClawHome;
}
const outsideRoot = await tempDirs.make("openclaw-tilde-outside-");

View File

@@ -1,9 +1,29 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { buildProviderRegistry, runCapability } from "./runner.js";
import { withAudioFixture, withVideoFixture } from "./runner.test-utils.js";
import type { AudioTranscriptionRequest, VideoDescriptionRequest } from "./types.js";
const proxyFetchMocks = vi.hoisted(() => {
const proxyFetch = vi.fn() as unknown as typeof fetch;
const resolveProxyFetchFromEnv = vi.fn((env: NodeJS.ProcessEnv = process.env) => {
const hasProxy = Boolean(
env.https_proxy?.trim() ||
env.HTTPS_PROXY?.trim() ||
env.http_proxy?.trim() ||
env.HTTP_PROXY?.trim(),
);
return hasProxy ? proxyFetch : undefined;
});
return { proxyFetch, resolveProxyFetchFromEnv };
});
vi.mock("../infra/net/proxy-fetch.js", () => ({
resolveProxyFetchFromEnv: proxyFetchMocks.resolveProxyFetchFromEnv,
}));
let buildProviderRegistry: typeof import("./runner.js").buildProviderRegistry;
let runCapability: typeof import("./runner.js").runCapability;
async function runAudioCapabilityWithFetchCapture(params: {
fixturePrefix: string;
outputText: string;
@@ -55,7 +75,12 @@ async function runAudioCapabilityWithFetchCapture(params: {
}
describe("runCapability proxy fetch passthrough", () => {
beforeEach(() => vi.clearAllMocks());
beforeEach(async () => {
vi.useRealTimers();
vi.resetModules();
vi.clearAllMocks();
({ buildProviderRegistry, runCapability } = await import("./runner.js"));
});
afterEach(() => vi.unstubAllEnvs());
it("passes fetchFn to audio provider when HTTPS_PROXY is set", async () => {
@@ -64,8 +89,7 @@ describe("runCapability proxy fetch passthrough", () => {
fixturePrefix: "openclaw-audio-proxy",
outputText: "transcribed",
});
expect(seenFetchFn).toBeDefined();
expect(seenFetchFn).not.toBe(globalThis.fetch);
expect(seenFetchFn).toBe(proxyFetchMocks.proxyFetch);
});
it("passes fetchFn to video provider when HTTPS_PROXY is set", async () => {
@@ -113,8 +137,7 @@ describe("runCapability proxy fetch passthrough", () => {
});
expect(result.outputs[0]?.text).toBe("video ok");
expect(seenFetchFn).toBeDefined();
expect(seenFetchFn).not.toBe(globalThis.fetch);
expect(seenFetchFn).toBe(proxyFetchMocks.proxyFetch);
});
});

View File

@@ -1,5 +1,15 @@
import { describe, expect, it, vi } from "vitest";
import { fetchRemoteMedia } from "./fetch.js";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn());
vi.mock("../infra/net/fetch-guard.js", () => ({
fetchWithSsrFGuard: (...args: unknown[]) => fetchWithSsrFGuardMock(...args),
withStrictGuardedFetchMode: <T>(params: T) => params,
}));
type FetchRemoteMedia = typeof import("./fetch.js").fetchRemoteMedia;
type LookupFn = NonNullable<Parameters<FetchRemoteMedia>[0]["lookupFn"]>;
let fetchRemoteMedia: FetchRemoteMedia;
function makeStream(chunks: Uint8Array[]) {
return new ReadableStream<Uint8Array>({
@@ -25,10 +35,8 @@ function makeStallingFetch(firstChunk: Uint8Array) {
});
}
function makeLookupFn() {
return vi.fn(async () => [{ address: "149.154.167.220", family: 4 }]) as unknown as NonNullable<
Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]
>;
function makeLookupFn(): LookupFn {
return vi.fn(async () => ({ address: "149.154.167.220", family: 4 })) as unknown as LookupFn;
}
async function expectRedactedTelegramFetchError(params: {
@@ -59,10 +67,38 @@ describe("fetchRemoteMedia", () => {
const redactedTelegramToken = `${telegramToken.slice(0, 6)}${telegramToken.slice(-4)}`;
const telegramFileUrl = `https://api.telegram.org/file/bot${telegramToken}/photos/1.jpg`;
beforeAll(async () => {
({ fetchRemoteMedia } = await import("./fetch.js"));
});
beforeEach(() => {
vi.useRealTimers();
fetchWithSsrFGuardMock.mockReset().mockImplementation(async (paramsUnknown: unknown) => {
const params = paramsUnknown as {
url: string;
fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
init?: RequestInit;
};
if (params.url.startsWith("http://127.0.0.1/")) {
throw new Error("Blocked hostname or private/internal/special-use IP address");
}
const fetcher = params.fetchImpl ?? globalThis.fetch;
if (!fetcher) {
throw new Error("fetch is not available");
}
return {
response: await fetcher(params.url, params.init),
finalUrl: params.url,
release: async () => {},
};
});
});
it("rejects when content-length exceeds maxBytes", async () => {
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as NonNullable<Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]>;
const lookupFn = vi.fn(async () => ({
address: "93.184.216.34",
family: 4,
})) as unknown as LookupFn;
const fetchImpl = async () =>
new Response(makeStream([new Uint8Array([1, 2, 3, 4, 5])]), {
status: 200,
@@ -80,9 +116,10 @@ describe("fetchRemoteMedia", () => {
});
it("rejects when streamed payload exceeds maxBytes", async () => {
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as NonNullable<Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]>;
const lookupFn = vi.fn(async () => ({
address: "93.184.216.34",
family: 4,
})) as unknown as LookupFn;
const fetchImpl = async () =>
new Response(makeStream([new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])]), {
status: 200,
@@ -99,23 +136,30 @@ describe("fetchRemoteMedia", () => {
});
it("aborts stalled body reads when idle timeout expires", async () => {
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as NonNullable<Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]>;
const fetchImpl = makeStallingFetch(new Uint8Array([1, 2]));
await expect(
fetchRemoteMedia({
vi.useFakeTimers();
try {
const lookupFn = vi.fn(async () => ({
address: "93.184.216.34",
family: 4,
})) as unknown as LookupFn;
const fetchImpl = makeStallingFetch(new Uint8Array([1, 2]));
const fetchPromise = fetchRemoteMedia({
url: "https://example.com/file.bin",
fetchImpl,
lookupFn,
maxBytes: 1024,
readIdleTimeoutMs: 20,
}),
).rejects.toMatchObject({
code: "fetch_failed",
name: "MediaFetchError",
});
});
const rejection = expect(fetchPromise).rejects.toMatchObject({
code: "fetch_failed",
name: "MediaFetchError",
});
await vi.advanceTimersByTimeAsync(25);
await rejection;
} finally {
vi.useRealTimers();
}
}, 5_000);
it("redacts Telegram bot tokens from fetch failure messages", async () => {

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { readResponseWithLimit } from "./read-response-with-limit.js";
function makeStream(chunks: Uint8Array[], delayMs?: number) {
@@ -26,6 +26,10 @@ function makeStallingStream(initialChunks: Uint8Array[]) {
}
describe("readResponseWithLimit", () => {
beforeEach(() => {
vi.useRealTimers();
});
it("reads all chunks within the limit", async () => {
const body = makeStream([new Uint8Array([1, 2]), new Uint8Array([3, 4])]);
const res = new Response(body);
@@ -50,17 +54,30 @@ describe("readResponseWithLimit", () => {
});
it("times out when no new chunk arrives before idle timeout", async () => {
const body = makeStallingStream([new Uint8Array([1, 2])]);
const res = new Response(body);
await expect(readResponseWithLimit(res, 1024, { chunkTimeoutMs: 50 })).rejects.toThrow(
/stalled/i,
);
vi.useFakeTimers();
try {
const body = makeStallingStream([new Uint8Array([1, 2])]);
const res = new Response(body);
const readPromise = readResponseWithLimit(res, 1024, { chunkTimeoutMs: 50 });
const rejection = expect(readPromise).rejects.toThrow(/stalled/i);
await vi.advanceTimersByTimeAsync(60);
await rejection;
} finally {
vi.useRealTimers();
}
}, 5_000);
it("does not time out while chunks keep arriving", async () => {
const body = makeStream([new Uint8Array([1]), new Uint8Array([2])], 10);
const res = new Response(body);
const buf = await readResponseWithLimit(res, 100, { chunkTimeoutMs: 500 });
expect(buf).toEqual(Buffer.from([1, 2]));
vi.useFakeTimers();
try {
const body = makeStream([new Uint8Array([1]), new Uint8Array([2])], 10);
const res = new Response(body);
const readPromise = readResponseWithLimit(res, 100, { chunkTimeoutMs: 500 });
await vi.advanceTimersByTimeAsync(25);
const buf = await readPromise;
expect(buf).toEqual(Buffer.from([1, 2]));
} finally {
vi.useRealTimers();
}
});
});

View File

@@ -1,15 +1,25 @@
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { GeminiEmbeddingClient } from "./embeddings-gemini.js";
vi.mock("./remote-http.js", () => ({
withRemoteHttpResponse: vi.fn(),
}));
function magnitude(values: number[]) {
return Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
}
describe("runGeminiEmbeddingBatches", () => {
let runGeminiEmbeddingBatches: typeof import("./batch-gemini.js").runGeminiEmbeddingBatches;
let withRemoteHttpResponse: typeof import("./remote-http.js").withRemoteHttpResponse;
let remoteHttpMock: ReturnType<typeof vi.mocked<typeof withRemoteHttpResponse>>;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
({ runGeminiEmbeddingBatches } = await import("./batch-gemini.js"));
({ withRemoteHttpResponse } = await import("./remote-http.js"));
remoteHttpMock = vi.mocked(withRemoteHttpResponse);
});
afterEach(() => {
@@ -27,24 +37,26 @@ describe("runGeminiEmbeddingBatches", () => {
};
it("includes outputDimensionality in batch upload requests", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url =
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.includes("/upload/v1beta/files?uploadType=multipart")) {
const body = init?.body;
if (!(body instanceof Blob)) {
throw new Error("expected multipart blob body");
}
const text = await body.text();
expect(text).toContain('"taskType":"RETRIEVAL_DOCUMENT"');
expect(text).toContain('"outputDimensionality":1536');
return new Response(JSON.stringify({ name: "files/file-123" }), {
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toContain("/upload/v1beta/files?uploadType=multipart");
const body = params.init?.body;
if (!(body instanceof Blob)) {
throw new Error("expected multipart blob body");
}
const text = await body.text();
expect(text).toContain('"taskType":"RETRIEVAL_DOCUMENT"');
expect(text).toContain('"outputDimensionality":1536');
return await params.onResponse(
new Response(JSON.stringify({ name: "files/file-123" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.endsWith(":asyncBatchEmbedContent")) {
return new Response(
}),
);
});
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toMatch(/:asyncBatchEmbedContent$/u);
return await params.onResponse(
new Response(
JSON.stringify({
name: "batches/batch-1",
state: "COMPLETED",
@@ -54,10 +66,13 @@ describe("runGeminiEmbeddingBatches", () => {
status: 200,
headers: { "Content-Type": "application/json" },
},
);
}
if (url.endsWith("/files/output-1:download")) {
return new Response(
),
);
});
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toMatch(/\/files\/output-1:download$/u);
return await params.onResponse(
new Response(
JSON.stringify({
key: "req-1",
response: { embedding: { values: [3, 4] } },
@@ -66,13 +81,10 @@ describe("runGeminiEmbeddingBatches", () => {
status: 200,
headers: { "Content-Type": "application/jsonl" },
},
);
}
throw new Error(`unexpected fetch ${url}`);
),
);
});
vi.stubGlobal("fetch", fetchMock);
const results = await runGeminiEmbeddingBatches({
gemini: mockClient,
agentId: "main",
@@ -97,6 +109,6 @@ describe("runGeminiEmbeddingBatches", () => {
expect(embedding?.[0]).toBeCloseTo(0.6, 5);
expect(embedding?.[1]).toBeCloseTo(0.8, 5);
expect(magnitude(embedding ?? [])).toBeCloseTo(1, 5);
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(remoteHttpMock).toHaveBeenCalledTimes(3);
});
});

View File

@@ -1,8 +1,7 @@
import { ReadableStream } from "node:stream/web";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { VoyageBatchOutputLine, VoyageBatchRequest } from "./batch-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.
// We DO need to mock retryAsync to avoid actual delays/retries logic complicating tests
@@ -10,11 +9,21 @@ vi.mock("../infra/retry.js", () => ({
retryAsync: async <T>(fn: () => Promise<T>) => fn(),
}));
vi.mock("./remote-http.js", () => ({
withRemoteHttpResponse: vi.fn(),
}));
describe("runVoyageEmbeddingBatches", () => {
let runVoyageEmbeddingBatches: typeof import("./batch-voyage.js").runVoyageEmbeddingBatches;
let withRemoteHttpResponse: typeof import("./remote-http.js").withRemoteHttpResponse;
let remoteHttpMock: ReturnType<typeof vi.mocked<typeof withRemoteHttpResponse>>;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
({ runVoyageEmbeddingBatches } = await import("./batch-voyage.js"));
({ withRemoteHttpResponse } = await import("./remote-http.js"));
remoteHttpMock = vi.mocked(withRemoteHttpResponse);
});
afterEach(() => {
@@ -34,39 +43,6 @@ describe("runVoyageEmbeddingBatches", () => {
];
it("successfully submits batch, waits, and streams results", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
// Sequence of fetch calls:
// 1. Upload file
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: "file-123" }),
});
// 2. Create batch
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: "batch-abc", status: "pending" }),
});
// 3. Poll status (pending) - Optional depending on wait loop, let's say it finishes immediately for this test
// Actually the code does: initial check (if completed) -> wait loop.
// If create returns "pending", it enters waitForVoyageBatch.
// waitForVoyageBatch fetches status.
// 3. Poll status (completed)
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
id: "batch-abc",
status: "completed",
output_file_id: "file-out-999",
}),
});
// 4. Download content (Streaming)
const outputLines: VoyageBatchOutputLine[] = [
{
custom_id: "req-1",
@@ -86,10 +62,64 @@ describe("runVoyageEmbeddingBatches", () => {
controller.close();
},
});
fetchMock.mockResolvedValueOnce({
ok: true,
body: stream,
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toContain("/files");
const uploadBody = params.init?.body;
expect(uploadBody).toBeInstanceOf(FormData);
expect((uploadBody as FormData).get("purpose")).toBe("batch");
return await params.onResponse(
new Response(JSON.stringify({ id: "file-123" }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
});
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toContain("/batches");
const body = params.init?.body;
expect(typeof body).toBe("string");
const createBody = JSON.parse(body as string) as {
input_file_id: string;
completion_window: string;
request_params: { model: string; input_type: string };
};
expect(createBody.input_file_id).toBe("file-123");
expect(createBody.completion_window).toBe("12h");
expect(createBody.request_params).toEqual({
model: "voyage-4-large",
input_type: "document",
});
return await params.onResponse(
new Response(JSON.stringify({ id: "batch-abc", status: "pending" }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
});
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toContain("/batches/batch-abc");
return await params.onResponse(
new Response(
JSON.stringify({
id: "batch-abc",
status: "completed",
output_file_id: "file-out-999",
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
),
);
});
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toContain("/files/file-out-999/content");
return await params.onResponse(
new Response(stream as unknown as BodyInit, {
status: 200,
headers: { "Content-Type": "application/x-ndjson" },
}),
);
});
const results = await runVoyageEmbeddingBatches({
@@ -105,43 +135,10 @@ describe("runVoyageEmbeddingBatches", () => {
expect(results.size).toBe(2);
expect(results.get("req-1")).toEqual([0.1, 0.1]);
expect(results.get("req-2")).toEqual([0.2, 0.2]);
// Verify calls
expect(fetchMock).toHaveBeenCalledTimes(4);
// Verify File Upload
expect(fetchMock.mock.calls[0][0]).toContain("/files");
const uploadBody = fetchMock.mock.calls[0][1].body as FormData;
expect(uploadBody).toBeInstanceOf(FormData);
expect(uploadBody.get("purpose")).toBe("batch");
// Verify Batch Create
expect(fetchMock.mock.calls[1][0]).toContain("/batches");
const createBody = JSON.parse(fetchMock.mock.calls[1][1].body);
expect(createBody.input_file_id).toBe("file-123");
expect(createBody.completion_window).toBe("12h");
expect(createBody.request_params).toEqual({
model: "voyage-4-large",
input_type: "document",
});
// Verify Content Fetch
expect(fetchMock.mock.calls[3][0]).toContain("/files/file-out-999/content");
expect(remoteHttpMock).toHaveBeenCalledTimes(4);
});
it("handles empty lines and stream chunks correctly", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
mockPublicPinnedHostname();
// 1. Upload
fetchMock.mockResolvedValueOnce({ ok: true, json: async () => ({ id: "f1" }) });
// 2. Create (completed immediately)
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: "b1", status: "completed", output_file_id: "out1" }),
});
// 3. Download Content (Streaming with chunks and newlines)
const stream = new ReadableStream({
start(controller) {
const line1 = JSON.stringify({
@@ -160,8 +157,22 @@ describe("runVoyageEmbeddingBatches", () => {
controller.close();
},
});
fetchMock.mockResolvedValueOnce({ ok: true, body: stream });
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toContain("/files");
return await params.onResponse(new Response(JSON.stringify({ id: "f1" }), { status: 200 }));
});
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toContain("/batches");
return await params.onResponse(
new Response(JSON.stringify({ id: "b1", status: "completed", output_file_id: "out1" }), {
status: 200,
}),
);
});
remoteHttpMock.mockImplementationOnce(async (params) => {
expect(params.url).toContain("/files/out1/content");
return await params.onResponse(new Response(stream as unknown as BodyInit, { status: 200 }));
});
const results = await runVoyageEmbeddingBatches({
client: mockClient,

View File

@@ -34,11 +34,29 @@ vi.mock("../media/mime.js", () => ({
detectMime: vi.fn(async () => "image/png"),
}));
import { runBrowserProxyCommand } from "./invoke-browser.js";
let runBrowserProxyCommand: typeof import("./invoke-browser.js").runBrowserProxyCommand;
describe("runBrowserProxyCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
beforeEach(async () => {
// No-isolate runs can reuse a cached invoke-browser module that was loaded
// via node-host entrypoints before this file's mocks were declared.
vi.useRealTimers();
vi.resetModules();
dispatcherMocks.dispatch.mockReset();
dispatcherMocks.createBrowserRouteDispatcher.mockReset().mockImplementation(() => ({
dispatch: dispatcherMocks.dispatch,
}));
controlServiceMocks.createBrowserControlContext.mockReset().mockReturnValue({ control: true });
controlServiceMocks.startBrowserControlServiceFromConfig.mockReset().mockResolvedValue(true);
configMocks.loadConfig.mockReset().mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true } },
});
browserConfigMocks.resolveBrowserConfig.mockReset().mockReturnValue({
enabled: true,
defaultProfile: "openclaw",
});
({ runBrowserProxyCommand } = await import("./invoke-browser.js"));
configMocks.loadConfig.mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true } },
@@ -51,6 +69,7 @@ describe("runBrowserProxyCommand", () => {
});
it("adds profile and browser status details on ws-backed timeouts", async () => {
vi.useFakeTimers();
dispatcherMocks.dispatch
.mockImplementationOnce(async () => {
await new Promise(() => {});
@@ -65,7 +84,7 @@ describe("runBrowserProxyCommand", () => {
},
});
await expect(
const result = expect(
runBrowserProxyCommand(
JSON.stringify({
method: "GET",
@@ -77,9 +96,12 @@ describe("runBrowserProxyCommand", () => {
).rejects.toThrow(
/browser proxy timed out for GET \/snapshot after 5ms; ws-backed browser action; profile=openclaw; status\(running=true, cdpHttp=true, cdpReady=false, cdpUrl=http:\/\/127\.0\.0\.1:18792\)/,
);
await vi.advanceTimersByTimeAsync(10);
await result;
});
it("includes chrome-mcp transport in timeout diagnostics when no CDP URL exists", async () => {
vi.useFakeTimers();
dispatcherMocks.dispatch
.mockImplementationOnce(async () => {
await new Promise(() => {});
@@ -95,7 +117,7 @@ describe("runBrowserProxyCommand", () => {
},
});
await expect(
const result = expect(
runBrowserProxyCommand(
JSON.stringify({
method: "GET",
@@ -107,9 +129,12 @@ describe("runBrowserProxyCommand", () => {
).rejects.toThrow(
/browser proxy timed out for GET \/snapshot after 5ms; ws-backed browser action; profile=user; status\(running=true, cdpHttp=true, cdpReady=false, transport=chrome-mcp\)/,
);
await vi.advanceTimersByTimeAsync(10);
await result;
});
it("redacts sensitive cdpUrl details in timeout diagnostics", async () => {
vi.useFakeTimers();
dispatcherMocks.dispatch
.mockImplementationOnce(async () => {
await new Promise(() => {});
@@ -125,7 +150,7 @@ describe("runBrowserProxyCommand", () => {
},
});
await expect(
const result = expect(
runBrowserProxyCommand(
JSON.stringify({
method: "GET",
@@ -137,6 +162,8 @@ describe("runBrowserProxyCommand", () => {
).rejects.toThrow(
/status\(running=true, cdpHttp=true, cdpReady=false, cdpUrl=https:\/\/example\.com\/chrome\?token=supers…7890\)/,
);
await vi.advanceTimersByTimeAsync(10);
await result;
});
it("keeps non-timeout browser errors intact", async () => {

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, type Mock, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from "vitest";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
import type { SystemRunApprovalPlan } from "../infra/exec-approvals.js";
import { loadExecApprovals, saveExecApprovals } from "../infra/exec-approvals.js";
@@ -31,6 +31,29 @@ describe("formatSystemRunAllowlistMissMessage", () => {
});
describe("handleSystemRunInvoke mac app exec host routing", () => {
let testOpenClawHome = "";
let previousOpenClawHome: string | undefined;
beforeEach(() => {
previousOpenClawHome = process.env.OPENCLAW_HOME;
testOpenClawHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-node-host-home-"));
process.env.OPENCLAW_HOME = testOpenClawHome;
clearRuntimeConfigSnapshot();
});
afterEach(() => {
clearRuntimeConfigSnapshot();
if (previousOpenClawHome === undefined) {
delete process.env.OPENCLAW_HOME;
} else {
process.env.OPENCLAW_HOME = previousOpenClawHome;
}
if (testOpenClawHome) {
fs.rmSync(testOpenClawHome, { recursive: true, force: true });
testOpenClawHome = "";
}
});
function createLocalRunResult(stdout = "local-ok") {
return {
success: true,

View File

@@ -4,6 +4,7 @@ const path = require("node:path");
const fs = require("node:fs");
let monolithicSdk = null;
let diagnosticEventsModule = null;
const jitiLoaders = new Map();
const pluginSdkSubpathsCache = new Map();
@@ -64,10 +65,14 @@ function resolveControlCommandGate(params) {
function onDiagnosticEvent(listener) {
const monolithic = loadMonolithicSdk();
if (!monolithic || typeof monolithic.onDiagnosticEvent !== "function") {
if (monolithic && typeof monolithic.onDiagnosticEvent === "function") {
return monolithic.onDiagnosticEvent(listener);
}
const diagnosticEvents = loadDiagnosticEventsModule();
if (!diagnosticEvents || typeof diagnosticEvents.onDiagnosticEvent !== "function") {
throw new Error("openclaw/plugin-sdk root alias could not resolve onDiagnosticEvent");
}
return monolithic.onDiagnosticEvent(listener);
return diagnosticEvents.onDiagnosticEvent(listener);
}
function getPackageRoot() {
@@ -150,6 +155,34 @@ function loadMonolithicSdk() {
return monolithicSdk;
}
function loadDiagnosticEventsModule() {
if (diagnosticEventsModule) {
return diagnosticEventsModule;
}
const distCandidate = path.resolve(
__dirname,
"..",
"..",
"dist",
"infra",
"diagnostic-events.js",
);
if (fs.existsSync(distCandidate)) {
try {
diagnosticEventsModule = getJiti(true)(distCandidate);
return diagnosticEventsModule;
} catch {
// Fall through to source path if dist is unavailable or stale.
}
}
diagnosticEventsModule = getJiti(false)(
path.join(__dirname, "..", "infra", "diagnostic-events.ts"),
);
return diagnosticEventsModule;
}
function tryLoadMonolithicSdk() {
try {
return loadMonolithicSdk();

View File

@@ -1,7 +1,6 @@
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { fileURLToPath } from "node:url";
import type {
BaseProbeResult as ContractBaseProbeResult,
BaseTokenResolution as ContractBaseTokenResolution,
@@ -47,12 +46,10 @@ import { pluginSdkSubpaths } from "./entrypoints.js";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const PLUGIN_SDK_DIR = resolve(ROOT_DIR, "plugin-sdk");
const requireFromHere = createRequire(import.meta.url);
const sourceCache = new Map<string, string>();
const representativeRuntimeSmokeSubpaths = ["channel-runtime", "conversation-runtime"] as const;
const importResolvedPluginSdkSubpath = async (specifier: string) =>
import(pathToFileURL(requireFromHere.resolve(specifier)).href);
const importResolvedPluginSdkSubpath = async (specifier: string) => import(specifier);
function readPluginSdkSource(subpath: string): string {
const file = resolve(PLUGIN_SDK_DIR, `${subpath}.ts`);

View File

@@ -15,10 +15,16 @@ vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
};
});
vi.mock("@mariozechner/pi-ai/oauth", () => ({
getOAuthProviders: () => [],
getOAuthApiKey: vi.fn(async () => null),
}));
vi.mock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
"@mariozechner/pi-ai/oauth",
);
return {
...actual,
getOAuthProviders: () => [],
getOAuthApiKey: vi.fn(async () => null),
};
});
function createResolvedModel(provider: string, modelId: string, api = "openai-completions") {
return {

View File

@@ -1,11 +1,11 @@
import type { Component } from "@mariozechner/pi-tui";
import {
Input,
Key,
matchesKey,
type SelectItem,
SelectList,
type SelectListTheme,
getEditorKeybindings,
} from "@mariozechner/pi-tui";
import chalk from "chalk";
import { fuzzyFilterLower, prepareSearchItems } from "./fuzzy-filter.js";
@@ -110,7 +110,8 @@ export class FilterableSelectList implements Component {
}
// Escape: clear filter or cancel
if (matchesKey(keyData, Key.escape)) {
const kb = getEditorKeybindings();
if (kb.matches(keyData, "selectCancel")) {
if (this.filterText) {
this.filterText = "";
this.input.setValue("");

View File

@@ -1,8 +1,8 @@
import {
type Component,
getEditorKeybindings,
Input,
isKeyRelease,
Key,
matchesKey,
type SelectItem,
type SelectListTheme,
@@ -362,7 +362,8 @@ export class SearchableSelectList implements Component {
return;
}
if (matchesKey(keyData, Key.escape)) {
const kb = getEditorKeybindings();
if (kb.matches(keyData, "selectCancel")) {
if (this.onCancel) {
this.onCancel();
}

View File

@@ -1,7 +1,7 @@
import { setTimeout as nativeSetTimeout } from "node:timers";
import { vi } from "vitest";
export function useFastShortTimeouts(maxDelayMs = 2000): () => void {
const realSetTimeout = setTimeout;
const spy = vi.spyOn(global, "setTimeout").mockImplementation(((
handler: TimerHandler,
timeout?: number,
@@ -9,9 +9,9 @@ export function useFastShortTimeouts(maxDelayMs = 2000): () => void {
) => {
const delay = typeof timeout === "number" ? timeout : 0;
if (delay > 0 && delay <= maxDelayMs) {
return realSetTimeout(handler, 0, ...args);
return nativeSetTimeout(handler, 0, ...args);
}
return realSetTimeout(handler, delay, ...args);
return nativeSetTimeout(handler, delay, ...args);
}) as typeof setTimeout);
return () => spy.mockRestore();
}

View File

@@ -0,0 +1,68 @@
import fs from "node:fs";
import { TestRunner, type RunnerTestSuite, vi } from "vitest";
type EvaluatedModuleNode = {
promise?: unknown;
exports?: unknown;
evaluated?: boolean;
importers: Set<string>;
};
type EvaluatedModules = {
idToModuleMap: Map<string, EvaluatedModuleNode>;
};
function resetEvaluatedModules(modules: EvaluatedModules, resetMocks: boolean) {
const skipPaths = [
/\/vitest\/dist\//,
/vitest-virtual-\w+\/dist/u,
/@vitest\/dist/u,
...(resetMocks ? [] : [/^mock:/u]),
];
modules.idToModuleMap.forEach((node, modulePath) => {
if (skipPaths.some((pattern) => pattern.test(modulePath))) {
return;
}
node.promise = undefined;
node.exports = undefined;
node.evaluated = false;
node.importers.clear();
});
}
export default class OpenClawNonIsolatedRunner extends TestRunner {
override onCollectStart(file: { filepath: string }) {
super.onCollectStart(file);
const orderLogPath = process.env.OPENCLAW_VITEST_FILE_ORDER_LOG?.trim();
if (orderLogPath) {
fs.appendFileSync(orderLogPath, `START ${file.filepath}\n`);
}
}
override async onAfterRunSuite(suite: RunnerTestSuite) {
await super.onAfterRunSuite(suite);
if (this.config.isolate || !("filepath" in suite) || typeof suite.filepath !== "string") {
return;
}
const orderLogPath = process.env.OPENCLAW_VITEST_FILE_ORDER_LOG?.trim();
if (orderLogPath) {
fs.appendFileSync(orderLogPath, `END ${suite.filepath}\n`);
}
// Mirror the missing cleanup from Vitest isolate mode so shared workers do
// not carry file-scoped timers, stubs, spies, or stale module state
// forward into the next file.
if (vi.isFakeTimers()) {
vi.useRealTimers();
}
vi.restoreAllMocks();
vi.unstubAllGlobals();
vi.unstubAllEnvs();
vi.clearAllMocks();
vi.resetModules();
this.moduleRunner?.mocker?.reset?.();
resetEvaluatedModules(this.workerState.evaluatedModules as EvaluatedModules, true);
}
}

View File

@@ -75,6 +75,60 @@ const pickSendFn = (id: ChannelId, deps?: OutboundSendDeps) => {
return deps?.[id] as ((...args: unknown[]) => Promise<unknown>) | undefined;
};
type VitestEvaluatedModuleNode = {
promise?: unknown;
exports?: unknown;
evaluated?: boolean;
importers: Set<string>;
};
type VitestEvaluatedModules = {
idToModuleMap: Map<string, VitestEvaluatedModuleNode>;
};
const resetVitestWorkerModules = (resetMocks: boolean) => {
const workerState = (
globalThis as typeof globalThis & {
__vitest_worker__?: {
evaluatedModules?: VitestEvaluatedModules;
};
}
).__vitest_worker__;
const modules = workerState?.evaluatedModules;
if (!modules) {
return;
}
const skipPaths = [
/\/vitest\/dist\//,
/vitest-virtual-\w+\/dist/u,
/@vitest\/dist/u,
...(resetMocks ? [] : [/^mock:/u]),
];
modules.idToModuleMap.forEach((node, modulePath) => {
if (skipPaths.some((pattern) => pattern.test(modulePath))) {
return;
}
node.promise = undefined;
node.exports = undefined;
node.evaluated = false;
node.importers.clear();
});
};
const resetVitestWorkerFileState = () => {
const mocker = (
globalThis as typeof globalThis & {
__vitest_mocker__?: {
reset?: () => void;
};
}
).__vitest_mocker__;
mocker?.reset?.();
resetVitestWorkerModules(true);
};
const createStubOutbound = (
id: ChannelId,
deliveryMode: ChannelOutboundAdapter["deliveryMode"] = "direct",
@@ -274,8 +328,17 @@ afterEach(() => {
globalRegistryState.key = null;
globalRegistryState.version += 1;
}
// Guard against leaked fake timers across test files/workers.
if (vi.isFakeTimers()) {
vi.useRealTimers();
}
// Always normalize timer/date state. Some suites call `vi.setSystemTime()`
// without leaving fake timers enabled, which still leaks mocked time into
// later files under `--isolate=false`.
vi.useRealTimers();
// Non-isolated runs reuse the same module graph across files. Clear it so
// hoisted per-file mocks still apply when later files import the same modules.
vi.resetModules();
});
afterAll(() => {
// Mirror Vitest's isolate-mode file cleanup so `--isolate=false` does not
// carry hoisted mocks or stale module graphs into the next test file.
resetVitestWorkerFileState();
});

View File

@@ -41,6 +41,7 @@ export default defineConfig({
...base,
test: {
...baseTest,
runner: "./test/non-isolated-runner.ts",
include: loadIncludePatternsFromEnv() ?? unitTestIncludePatterns,
exclude: [
...new Set([