mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
test: extract node builtin mock helpers
This commit is contained in:
@@ -1,33 +1,25 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
return {
|
||||
...actual,
|
||||
const { mockNodeBuiltinModule } = await import("../../../../test/helpers/node-builtin-mocks.js");
|
||||
return mockNodeBuiltinModule(importOriginal, {
|
||||
execFileSync: vi.fn(),
|
||||
};
|
||||
});
|
||||
});
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
const { mockNodeBuiltinModule } = await import("../../../../test/helpers/node-builtin-mocks.js");
|
||||
const existsSync = vi.fn();
|
||||
const readFileSync = vi.fn();
|
||||
const module = { existsSync, readFileSync };
|
||||
return {
|
||||
...actual,
|
||||
...module,
|
||||
default: {
|
||||
...actual,
|
||||
...module,
|
||||
},
|
||||
};
|
||||
return mockNodeBuiltinModule(
|
||||
importOriginal,
|
||||
{ existsSync, readFileSync },
|
||||
{ mirrorToDefault: true },
|
||||
);
|
||||
});
|
||||
vi.mock("node:os", () => {
|
||||
vi.mock("node:os", async (importOriginal) => {
|
||||
const { mockNodeBuiltinModule } = await import("../../../../test/helpers/node-builtin-mocks.js");
|
||||
const homedir = vi.fn();
|
||||
const module = { homedir };
|
||||
return {
|
||||
...module,
|
||||
default: module,
|
||||
};
|
||||
return mockNodeBuiltinModule(importOriginal, { homedir }, { mirrorToDefault: true });
|
||||
});
|
||||
import { execFileSync } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
@@ -3,6 +3,8 @@ import { acquireLocalHeavyCheckLockSync } from "./lib/local-heavy-check-runtime.
|
||||
import { spawnPnpmRunner } from "./pnpm-runner.mjs";
|
||||
import { createVitestRunSpecs, writeVitestIncludeFile } from "./test-projects.test-support.mjs";
|
||||
|
||||
// Keep this shim so `pnpm test -- src/foo.test.ts` still forwards filters
|
||||
// cleanly instead of leaking pnpm's passthrough sentinel to Vitest.
|
||||
const releaseLock = acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
|
||||
@@ -8,23 +8,21 @@ const isGatewayArgvMock = vi.hoisted(() => vi.fn());
|
||||
const findGatewayPidsOnPortSyncMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
return {
|
||||
...actual,
|
||||
const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
|
||||
return mockNodeBuiltinModule(importOriginal, {
|
||||
spawnSync: (...args: unknown[]) => spawnSyncMock(...args),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...actual,
|
||||
const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
|
||||
return mockNodeBuiltinModule(
|
||||
importOriginal,
|
||||
{
|
||||
readFileSync: (...args: unknown[]) => readFileSyncMock(...args),
|
||||
},
|
||||
readFileSync: (...args: unknown[]) => readFileSyncMock(...args),
|
||||
};
|
||||
{ mirrorToDefault: true },
|
||||
);
|
||||
});
|
||||
|
||||
vi.mock("../daemon/cmd-argv.js", () => ({
|
||||
|
||||
@@ -17,7 +17,7 @@ function createMockSpawnChild() {
|
||||
}
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
|
||||
const spawn = vi.fn(() => {
|
||||
const { child, stdout } = createMockSpawnChild();
|
||||
process.nextTick(() => {
|
||||
@@ -36,10 +36,9 @@ vi.mock("node:child_process", async (importOriginal) => {
|
||||
});
|
||||
return child;
|
||||
});
|
||||
return {
|
||||
...actual,
|
||||
return mockNodeBuiltinModule(importOriginal, {
|
||||
spawn,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const spawnMock = vi.mocked(spawn);
|
||||
|
||||
@@ -8,11 +8,10 @@ const spawnMock = vi.hoisted(() => vi.fn());
|
||||
const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn(() => os.tmpdir()));
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
return {
|
||||
...actual,
|
||||
const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
|
||||
return mockNodeBuiltinModule(importOriginal, {
|
||||
spawn: (...args: unknown[]) => spawnMock(...args),
|
||||
};
|
||||
});
|
||||
});
|
||||
vi.mock("./tmp-openclaw-dir.js", () => ({
|
||||
resolvePreferredOpenClawTmpDir: () => resolvePreferredOpenClawTmpDirMock(),
|
||||
|
||||
@@ -15,31 +15,21 @@ vi.mock("./facade-runtime.js", () => ({
|
||||
}));
|
||||
|
||||
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,
|
||||
};
|
||||
const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
|
||||
return mockNodeBuiltinModule(
|
||||
importOriginal,
|
||||
{ mkdir, access, rename },
|
||||
{ mirrorToDefault: true },
|
||||
);
|
||||
});
|
||||
|
||||
vi.mock("node:os", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:os")>();
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...actual,
|
||||
homedir: () => "/home/test",
|
||||
},
|
||||
homedir: () => "/home/test",
|
||||
};
|
||||
const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
|
||||
return mockNodeBuiltinModule(
|
||||
importOriginal,
|
||||
{ homedir: () => "/home/test" },
|
||||
{ mirrorToDefault: true },
|
||||
);
|
||||
});
|
||||
|
||||
describe("browser maintenance", () => {
|
||||
|
||||
@@ -7,12 +7,11 @@ const spawnMock = vi.hoisted(() => vi.fn());
|
||||
const execFileMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
return {
|
||||
...actual,
|
||||
const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
|
||||
return mockNodeBuiltinModule(importOriginal, {
|
||||
spawn: spawnMock,
|
||||
execFile: execFileMock,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
let runCommandWithTimeout: typeof import("./exec.js").runCommandWithTimeout;
|
||||
|
||||
@@ -4,16 +4,12 @@ import type { WindowsAclEntry, WindowsAclSummary } from "./windows-acl.js";
|
||||
const MOCK_USERNAME = "MockUser";
|
||||
|
||||
vi.mock("node:os", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:os")>();
|
||||
const base = ("default" in actual ? actual.default : actual) as Record<string, unknown>;
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...base,
|
||||
userInfo: () => ({ username: MOCK_USERNAME }),
|
||||
},
|
||||
userInfo: () => ({ username: MOCK_USERNAME }),
|
||||
};
|
||||
const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
|
||||
return mockNodeBuiltinModule(
|
||||
importOriginal,
|
||||
{ userInfo: () => ({ username: MOCK_USERNAME }) },
|
||||
{ mirrorToDefault: true },
|
||||
);
|
||||
});
|
||||
|
||||
let createIcaclsResetCommand: typeof import("./windows-acl.js").createIcaclsResetCommand;
|
||||
|
||||
@@ -34,6 +34,24 @@ function walk(dir: string, entries: string[] = []): string[] {
|
||||
return entries;
|
||||
}
|
||||
|
||||
function walkCode(dir: string, entries: string[] = []): string[] {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
|
||||
continue;
|
||||
}
|
||||
walkCode(fullPath, entries);
|
||||
continue;
|
||||
}
|
||||
if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".tsx")) {
|
||||
continue;
|
||||
}
|
||||
entries.push(path.relative(repoRoot, fullPath).replaceAll(path.sep, "/"));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function findExtensionImports(source: string): string[] {
|
||||
return [
|
||||
...source.matchAll(/from\s+["']((?:\.\.\/)+extensions\/[^"']+)["']/g),
|
||||
@@ -124,4 +142,21 @@ describe("non-extension test boundaries", () => {
|
||||
|
||||
expect(imports).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps bundled plugin sync test-api loaders out of core tests", () => {
|
||||
const files = [
|
||||
...walkCode(path.join(repoRoot, "src")),
|
||||
...walkCode(path.join(repoRoot, "test")),
|
||||
]
|
||||
.filter((file) => !file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX))
|
||||
.filter((file) => !file.startsWith("test/helpers/"))
|
||||
.filter((file) => file !== "test/extension-test-boundary.test.ts");
|
||||
|
||||
const offenders = files.filter((file) => {
|
||||
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
|
||||
return source.includes("loadBundledPluginTestApiSync(");
|
||||
});
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
54
test/helpers/node-builtin-mocks.test.ts
Normal file
54
test/helpers/node-builtin-mocks.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mockNodeBuiltinModule } from "./node-builtin-mocks.js";
|
||||
|
||||
describe("mockNodeBuiltinModule", () => {
|
||||
it("merges partial overrides into the original module", async () => {
|
||||
const actual = { readFileSync: () => "actual", watch: () => "watch" };
|
||||
const readFileSync = () => "mock";
|
||||
|
||||
const mocked = await mockNodeBuiltinModule(async () => actual, {
|
||||
readFileSync,
|
||||
});
|
||||
|
||||
expect(mocked.readFileSync).toBe(readFileSync);
|
||||
expect(mocked.watch).toBe(actual.watch);
|
||||
expect("default" in mocked).toBe(false);
|
||||
});
|
||||
|
||||
it("mirrors overrides into the default export when requested", async () => {
|
||||
const homedir = () => "/tmp/home";
|
||||
|
||||
const mocked = await mockNodeBuiltinModule(
|
||||
async () => ({ tmpdir: () => "/tmp" }),
|
||||
{ homedir },
|
||||
{ mirrorToDefault: true },
|
||||
);
|
||||
|
||||
expect(mocked.default).toMatchObject({
|
||||
homedir,
|
||||
tmpdir: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves existing default exports while overriding members", async () => {
|
||||
const actual = {
|
||||
readFileSync: () => "actual",
|
||||
default: {
|
||||
readFileSync: () => "actual",
|
||||
statSync: () => "stat",
|
||||
},
|
||||
};
|
||||
const readFileSync = () => "mock";
|
||||
|
||||
const mocked = await mockNodeBuiltinModule(
|
||||
async () => actual,
|
||||
{ readFileSync },
|
||||
{ mirrorToDefault: true },
|
||||
);
|
||||
|
||||
expect(mocked.default).toMatchObject({
|
||||
readFileSync,
|
||||
statSync: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
43
test/helpers/node-builtin-mocks.ts
Normal file
43
test/helpers/node-builtin-mocks.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
type MockFactory<TModule extends object> =
|
||||
| Partial<TModule>
|
||||
| ((actual: TModule) => Partial<TModule>);
|
||||
|
||||
function resolveMockOverrides<TModule extends object>(
|
||||
actual: TModule,
|
||||
factory: MockFactory<TModule>,
|
||||
): Partial<TModule> {
|
||||
return typeof factory === "function" ? factory(actual) : factory;
|
||||
}
|
||||
|
||||
function resolveDefaultBase<TModule extends object>(actual: TModule): Record<string, unknown> {
|
||||
const defaultExport = (actual as TModule & { default?: unknown }).default;
|
||||
if (defaultExport && typeof defaultExport === "object") {
|
||||
return defaultExport as Record<string, unknown>;
|
||||
}
|
||||
return actual as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export async function mockNodeBuiltinModule<TModule extends object>(
|
||||
importOriginal: () => Promise<TModule>,
|
||||
factory: MockFactory<TModule>,
|
||||
options?: { mirrorToDefault?: boolean },
|
||||
): Promise<TModule> {
|
||||
const actual = await importOriginal();
|
||||
const overrides = resolveMockOverrides(actual, factory);
|
||||
const mocked = {
|
||||
...actual,
|
||||
...overrides,
|
||||
} as TModule & { default?: Record<string, unknown> };
|
||||
|
||||
if (!options?.mirrorToDefault) {
|
||||
return mocked;
|
||||
}
|
||||
|
||||
return {
|
||||
...mocked,
|
||||
default: {
|
||||
...resolveDefaultBase(actual),
|
||||
...overrides,
|
||||
},
|
||||
} as TModule;
|
||||
}
|
||||
Reference in New Issue
Block a user