test: harden shared-worker runtime setup

This commit is contained in:
Peter Steinberger
2026-04-03 18:17:59 +01:00
parent 2981cce130
commit e0580e6863
23 changed files with 292 additions and 117 deletions

View File

@@ -12,12 +12,19 @@ const hookRunnerMocks = vi.hoisted(() => ({
runBeforeReset: vi.fn<HookRunner["runBeforeReset"]>(),
}));
vi.mock("node:fs/promises", () => ({
default: {
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return {
...actual,
default: {
...actual,
readFile: fsMocks.readFile,
readdir: fsMocks.readdir,
},
readFile: fsMocks.readFile,
readdir: fsMocks.readdir,
},
}));
};
});
vi.mock("../../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: () =>

View File

@@ -3,9 +3,13 @@ import fs from "node:fs/promises";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { prepareRestartScript, runRestartScript } from "./restart-helper.js";
vi.mock("node:child_process", () => ({
spawn: vi.fn(),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: vi.fn(),
};
});
describe("restart-helper", () => {
const originalPlatform = process.platform;

View File

@@ -3,9 +3,13 @@ import { afterEach, describe, expect, it, vi } from "vitest";
const spawnMock = vi.hoisted(() => vi.fn());
const unrefMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({
spawn: (...args: unknown[]) => spawnMock(...args),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: (...args: unknown[]) => spawnMock(...args),
};
});
import { scheduleDetachedLaunchdRestartHandoff } from "./launchd-restart-handoff.js";

View File

@@ -10,15 +10,27 @@ const fsMocks = vi.hoisted(() => ({
realpath: vi.fn(),
}));
vi.mock("node:fs/promises", () => ({
default: { access: fsMocks.access, realpath: fsMocks.realpath },
access: fsMocks.access,
realpath: fsMocks.realpath,
}));
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return {
...actual,
default: {
...actual,
access: fsMocks.access,
realpath: fsMocks.realpath,
},
access: fsMocks.access,
realpath: fsMocks.realpath,
};
});
vi.mock("node:child_process", () => ({
execFileSync: childProcessMocks.execFileSync,
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
execFileSync: childProcessMocks.execFileSync,
};
});
import { resolveGatewayProgramArguments } from "./program-args.js";

View File

@@ -4,10 +4,17 @@ const fsMocks = vi.hoisted(() => ({
access: vi.fn(),
}));
vi.mock("node:fs/promises", () => ({
default: { access: fsMocks.access },
access: fsMocks.access,
}));
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return {
...actual,
default: {
...actual,
access: fsMocks.access,
},
access: fsMocks.access,
};
});
import {
renderSystemNodeWarning,

View File

@@ -4,9 +4,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const execFileMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({
execFile: execFileMock,
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
execFile: execFileMock,
};
});
import { splitArgsPreservingQuotes } from "./arg-split.js";
import { parseSystemdExecStart } from "./systemd-unit.js";

View File

@@ -3,9 +3,13 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { configHandlers, resolveConfigOpenCommand } from "./config.js";
import type { GatewayRequestHandlerOptions } from "./types.js";
vi.mock("node:child_process", () => ({
execFile: vi.fn(),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
execFile: vi.fn(),
};
});
function invokeExecFileCallback(args: unknown[], error: Error | null) {
const callback = args.at(-1);

View File

@@ -7,15 +7,25 @@ const parseProcCmdlineMock = vi.hoisted(() => vi.fn());
const isGatewayArgvMock = vi.hoisted(() => vi.fn());
const findGatewayPidsOnPortSyncMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({
spawnSync: (...args: unknown[]) => spawnSyncMock(...args),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawnSync: (...args: unknown[]) => spawnSyncMock(...args),
};
});
vi.mock("node:fs", () => ({
default: {
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return {
...actual,
default: {
...actual,
readFileSync: (...args: unknown[]) => readFileSyncMock(...args),
},
readFileSync: (...args: unknown[]) => readFileSyncMock(...args),
},
}));
};
});
vi.mock("../daemon/cmd-argv.js", () => ({
parseCmdScriptCommandLine: (...args: unknown[]) => parseCmdScriptCommandLineMock(...args),
@@ -30,6 +40,26 @@ vi.mock("./restart-stale-pids.js", () => ({
findGatewayPidsOnPortSync: (...args: unknown[]) => findGatewayPidsOnPortSyncMock(...args),
}));
vi.mock("../logging/subsystem.js", () => ({
createSubsystemLogger: vi.fn(() => ({
warn: vi.fn(),
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
trace: vi.fn(),
fatal: vi.fn(),
raw: vi.fn(),
child: vi.fn(),
isEnabled: vi.fn(() => false),
subsystem: "test",
})),
}));
vi.mock("../channels/chat-meta.js", () => ({
listChatChannels: vi.fn(() => []),
getChatChannelMeta: vi.fn(() => null),
}));
const {
findVerifiedGatewayListenerPidsOnPortSync,
formatGatewayPidList,

View File

@@ -3,7 +3,6 @@ import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js";
import type { ChannelOutboundAdapter } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import {
resolveAgentIdFromSessionKey,
@@ -11,9 +10,9 @@ import {
resolveMainSessionKey,
resolveStorePath,
} from "../config/sessions.js";
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../plugin-sdk/whatsapp-targets.js";
import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
import { buildAgentPeerSessionKey } from "../routing/session-key.js";
import { loadBundledPluginTestApiSync } from "../test-utils/bundled-plugin-public-surface.js";
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
import { typedCases } from "../test-utils/typed-cases.js";
import {
@@ -23,6 +22,7 @@ import {
resolveHeartbeatPrompt,
runHeartbeatOnce,
} from "./heartbeat-runner.js";
import { whatsappOutbound } from "./outbound/deliver.test-outbounds.js";
import {
resolveHeartbeatDeliveryTarget,
resolveHeartbeatSenderContext,
@@ -35,15 +35,40 @@ let testRegistry: ReturnType<typeof getActivePluginRegistry> | null = null;
let fixtureRoot = "";
let fixtureCount = 0;
let whatsappOutboundCache: ChannelOutboundAdapter | undefined;
function getWhatsAppOutbound(): ChannelOutboundAdapter {
if (!whatsappOutboundCache) {
({ whatsappOutbound: whatsappOutboundCache } = loadBundledPluginTestApiSync<{
whatsappOutbound: ChannelOutboundAdapter;
}>("whatsapp"));
function resolveWhatsAppTargetForTest(params: {
to: string | null | undefined;
allowFrom: Array<string | number> | null | undefined;
}) {
const trimmed = params.to?.trim() ?? "";
const allowListRaw = (params.allowFrom ?? [])
.map((entry) => String(entry).trim())
.filter(Boolean);
const hasWildcard = allowListRaw.includes("*");
const allowList = allowListRaw
.filter((entry) => entry !== "*")
.map((entry) => normalizeWhatsAppTarget(entry))
.filter((entry): entry is string => Boolean(entry));
const normalizedTarget = normalizeWhatsAppTarget(trimmed);
if (!normalizedTarget) {
return {
ok: false as const,
error: new Error('Missing target for WhatsApp; expected "<E.164|group JID>".'),
};
}
return whatsappOutboundCache;
if (isWhatsAppGroupJid(normalizedTarget)) {
return { ok: true as const, to: normalizedTarget };
}
if (hasWildcard || allowList.length === 0 || allowList.includes(normalizedTarget)) {
return { ok: true as const, to: normalizedTarget };
}
return {
ok: false as const,
error: new Error(
`Target "${normalizedTarget}" is not listed in the configured WhatsApp allowFrom policy.`,
),
};
}
const createCaseDir = async (prefix: string) => {
@@ -57,7 +82,14 @@ beforeAll(async () => {
const whatsappPlugin = createOutboundTestPlugin({
id: "whatsapp",
outbound: getWhatsAppOutbound(),
outbound: {
...whatsappOutbound,
resolveTarget: ({ to, allowFrom }) =>
resolveWhatsAppTargetForTest({
to,
allowFrom,
}),
},
});
whatsappPlugin.config = {
...whatsappPlugin.config,

View File

@@ -4,9 +4,13 @@ import { importFreshModule } from "../../test/helpers/import-fresh.js";
const execFileMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({
execFile: (...args: unknown[]) => execFileMock(...args),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
execFile: (...args: unknown[]) => execFileMock(...args),
};
});
const originalVitest = process.env.VITEST;
const originalNodeEnv = process.env.NODE_ENV;

View File

@@ -3,9 +3,13 @@ import { afterEach, describe, expect, it, vi } from "vitest";
const spawnSyncMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({
spawnSync: (...args: unknown[]) => spawnSyncMock(...args),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawnSync: (...args: unknown[]) => spawnSyncMock(...args),
};
});
import { resolveOsSummary } from "./os-summary.js";

View File

@@ -6,9 +6,13 @@ const spawnMock = vi.hoisted(() => vi.fn());
const triggerOpenClawRestartMock = vi.hoisted(() => vi.fn());
const scheduleDetachedLaunchdRestartHandoffMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({
spawn: (...args: unknown[]) => spawnMock(...args),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: (...args: unknown[]) => spawnMock(...args),
};
});
vi.mock("./restart.js", () => ({
triggerOpenClawRestart: (...args: unknown[]) => triggerOpenClawRestartMock(...args),
}));

View File

@@ -10,10 +10,14 @@ const mockSpawnSync = vi.hoisted(() => vi.fn());
const mockResolveGatewayPort = vi.hoisted(() => vi.fn(() => 18789));
const mockRestartWarn = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({
spawnSync: (...args: unknown[]) => mockSpawnSync(...args),
execFileSync: vi.fn(),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawnSync: (...args: unknown[]) => mockSpawnSync(...args),
execFileSync: vi.fn(),
};
});
vi.mock("../config/paths.js", () => ({
resolveGatewayPort: () => mockResolveGatewayPort(),

View File

@@ -16,7 +16,8 @@ function createMockSpawnChild() {
return { child, stdout };
}
vi.mock("node:child_process", () => {
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
const spawn = vi.fn(() => {
const { child, stdout } = createMockSpawnChild();
process.nextTick(() => {
@@ -35,7 +36,10 @@ vi.mock("node:child_process", () => {
});
return child;
});
return { spawn };
return {
...actual,
spawn,
};
});
const spawnMock = vi.mocked(spawn);

View File

@@ -7,9 +7,13 @@ import { captureFullEnv } from "../test-utils/env.js";
const spawnMock = vi.hoisted(() => vi.fn());
const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn(() => os.tmpdir()));
vi.mock("node:child_process", () => ({
spawn: (...args: unknown[]) => spawnMock(...args),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: (...args: unknown[]) => spawnMock(...args),
};
});
vi.mock("./tmp-openclaw-dir.js", () => ({
resolvePreferredOpenClawTmpDir: () => resolvePreferredOpenClawTmpDirMock(),
}));

View File

@@ -4,15 +4,25 @@ import { captureEnv } from "../test-utils/env.js";
const readFileSyncMock = vi.hoisted(() => vi.fn());
const readFileMock = vi.hoisted(() => vi.fn());
vi.mock("node:fs", () => ({
readFileSync: readFileSyncMock,
}));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return {
...actual,
readFileSync: readFileSyncMock,
};
});
vi.mock("node:fs/promises", () => ({
default: {
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return {
...actual,
default: {
...actual,
readFile: readFileMock,
},
readFile: readFileMock,
},
}));
};
});
let isWSLEnv: typeof import("./wsl.js").isWSLEnv;
let isWSLSync: typeof import("./wsl.js").isWSLSync;

View File

@@ -14,17 +14,33 @@ vi.mock("./facade-runtime.js", () => ({
tryLoadActivatedBundledPluginPublicSurfaceModuleSync,
}));
vi.mock("node:fs/promises", () => {
const mocked = { mkdir, access, rename };
return { ...mocked, default: mocked };
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return {
...actual,
default: {
...actual,
mkdir,
access,
rename,
},
mkdir,
access,
rename,
};
});
vi.mock("node:os", () => ({
default: {
vi.mock("node:os", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:os")>();
return {
...actual,
default: {
...actual,
homedir: () => "/home/test",
},
homedir: () => "/home/test",
},
homedir: () => "/home/test",
}));
};
});
describe("browser maintenance", () => {
beforeEach(() => {

View File

@@ -294,10 +294,13 @@ describe("command queue", () => {
const blocker2 = new Promise<void>((r) => {
resolve2 = r;
});
const firstStarted = createDeferred();
const first = enqueueCommandInLane(lane, async () => {
firstStarted.resolve();
await blocker1;
});
await firstStarted.promise;
const drainPromise = waitForActiveTasks(2000);
// Starts after waitForActiveTasks snapshot and should not block drain completion.

View File

@@ -4,9 +4,13 @@ const { spawnMock } = vi.hoisted(() => ({
spawnMock: vi.fn(),
}));
vi.mock("node:child_process", () => ({
spawn: (...args: unknown[]) => spawnMock(...args),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: (...args: unknown[]) => spawnMock(...args),
};
});
let killProcessTree: typeof import("./kill-tree.js").killProcessTree;