mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-30 01:06:11 +00:00
test: trim test startup overhead
This commit is contained in:
@@ -256,17 +256,27 @@ function formatInventoryHuman(inventory) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
function writeLine(stream, text) {
|
||||
stream.write(`${text}\n`);
|
||||
}
|
||||
|
||||
export async function runArchitectureSmellsCheck(argv = process.argv.slice(2), io) {
|
||||
const streams = io ?? { stdout: process.stdout, stderr: process.stderr };
|
||||
const json = argv.includes("--json");
|
||||
const inventory = await collectArchitectureSmells();
|
||||
|
||||
if (json) {
|
||||
process.stdout.write(`${JSON.stringify(inventory, null, 2)}\n`);
|
||||
return;
|
||||
writeLine(streams.stdout, JSON.stringify(inventory, null, 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(formatInventoryHuman(inventory));
|
||||
console.log(`${inventory.length} smell${inventory.length === 1 ? "" : "s"} found.`);
|
||||
writeLine(streams.stdout, formatInventoryHuman(inventory));
|
||||
writeLine(streams.stdout, `${inventory.length} smell${inventory.length === 1 ? "" : "s"} found.`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2), io) {
|
||||
return await runArchitectureSmellsCheck(argv, io);
|
||||
}
|
||||
|
||||
runAsScript(import.meta.url, main);
|
||||
|
||||
@@ -35,6 +35,9 @@ const baselinePathByMode = {
|
||||
),
|
||||
};
|
||||
|
||||
const inventoryPromiseByMode = new Map();
|
||||
let parsedExtensionSourceFilesPromise;
|
||||
|
||||
const ruleTextByMode = {
|
||||
"src-outside-plugin-sdk":
|
||||
"Rule: production extensions/** must not import src/** outside src/plugin-sdk/**",
|
||||
@@ -87,6 +90,34 @@ async function collectExtensionSourceFiles(rootDir) {
|
||||
return out.toSorted((left, right) => normalizePath(left).localeCompare(normalizePath(right)));
|
||||
}
|
||||
|
||||
async function collectParsedExtensionSourceFiles() {
|
||||
if (!parsedExtensionSourceFilesPromise) {
|
||||
parsedExtensionSourceFilesPromise = (async () => {
|
||||
const files = await collectExtensionSourceFiles(extensionsRoot);
|
||||
return await Promise.all(
|
||||
files.map(async (filePath) => {
|
||||
const source = await fs.readFile(filePath, "utf8");
|
||||
const scriptKind =
|
||||
filePath.endsWith(".tsx") || filePath.endsWith(".jsx")
|
||||
? ts.ScriptKind.TSX
|
||||
: ts.ScriptKind.TS;
|
||||
return {
|
||||
filePath,
|
||||
sourceFile: ts.createSourceFile(
|
||||
filePath,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
scriptKind,
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
})();
|
||||
}
|
||||
return await parsedExtensionSourceFilesPromise;
|
||||
}
|
||||
|
||||
function toLine(sourceFile, node) {
|
||||
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
||||
}
|
||||
@@ -216,22 +247,19 @@ export async function collectExtensionPluginSdkBoundaryInventory(mode) {
|
||||
if (!MODES.has(mode)) {
|
||||
throw new Error(`Unknown mode: ${mode}`);
|
||||
}
|
||||
const files = await collectExtensionSourceFiles(extensionsRoot);
|
||||
const inventory = [];
|
||||
for (const filePath of files) {
|
||||
const source = await fs.readFile(filePath, "utf8");
|
||||
const scriptKind =
|
||||
filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
scriptKind,
|
||||
);
|
||||
inventory.push(...collectFromSourceFile(mode, sourceFile, filePath));
|
||||
let pending = inventoryPromiseByMode.get(mode);
|
||||
if (!pending) {
|
||||
pending = (async () => {
|
||||
const files = await collectParsedExtensionSourceFiles();
|
||||
const inventory = [];
|
||||
for (const { filePath, sourceFile } of files) {
|
||||
inventory.push(...collectFromSourceFile(mode, sourceFile, filePath));
|
||||
}
|
||||
return inventory.toSorted(compareEntries);
|
||||
})();
|
||||
inventoryPromiseByMode.set(mode, pending);
|
||||
}
|
||||
return inventory.toSorted(compareEntries);
|
||||
return await pending;
|
||||
}
|
||||
|
||||
export async function readExpectedInventory(mode) {
|
||||
@@ -286,7 +314,12 @@ function formatInventoryHuman(mode, inventory) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
function writeLine(stream, text) {
|
||||
stream.write(`${text}\n`);
|
||||
}
|
||||
|
||||
export async function runExtensionPluginSdkBoundaryCheck(argv = process.argv.slice(2), io) {
|
||||
const streams = io ?? { stdout: process.stdout, stderr: process.stderr };
|
||||
const json = argv.includes("--json");
|
||||
const modeArg = argv.find((arg) => arg.startsWith("--mode="));
|
||||
const mode = modeArg?.slice("--mode=".length) ?? "src-outside-plugin-sdk";
|
||||
@@ -296,30 +329,38 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
|
||||
const actual = await collectExtensionPluginSdkBoundaryInventory(mode);
|
||||
if (json) {
|
||||
process.stdout.write(`${JSON.stringify(actual, null, 2)}\n`);
|
||||
return;
|
||||
writeLine(streams.stdout, JSON.stringify(actual, null, 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
const expected = await readExpectedInventory(mode);
|
||||
const diff = diffInventory(expected, actual);
|
||||
console.log(formatInventoryHuman(mode, actual));
|
||||
writeLine(streams.stdout, formatInventoryHuman(mode, actual));
|
||||
if (diff.missing.length === 0 && diff.unexpected.length === 0) {
|
||||
console.log(`Baseline matches (${actual.length} entries).`);
|
||||
return;
|
||||
writeLine(streams.stdout, `Baseline matches (${actual.length} entries).`);
|
||||
return 0;
|
||||
}
|
||||
if (diff.missing.length > 0) {
|
||||
console.error(`Missing baseline entries (${diff.missing.length}):`);
|
||||
writeLine(streams.stderr, `Missing baseline entries (${diff.missing.length}):`);
|
||||
for (const entry of diff.missing) {
|
||||
console.error(` - ${entry.file}:${entry.line} ${entry.reason}`);
|
||||
writeLine(streams.stderr, ` - ${entry.file}:${entry.line} ${entry.reason}`);
|
||||
}
|
||||
}
|
||||
if (diff.unexpected.length > 0) {
|
||||
console.error(`Unexpected inventory entries (${diff.unexpected.length}):`);
|
||||
writeLine(streams.stderr, `Unexpected inventory entries (${diff.unexpected.length}):`);
|
||||
for (const entry of diff.unexpected) {
|
||||
console.error(` - ${entry.file}:${entry.line} ${entry.reason}`);
|
||||
writeLine(streams.stderr, ` - ${entry.file}:${entry.line} ${entry.reason}`);
|
||||
}
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2), io) {
|
||||
const exitCode = await runExtensionPluginSdkBoundaryCheck(argv, io);
|
||||
if (!io) {
|
||||
process.exitCode = exitCode;
|
||||
}
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
if (path.resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) {
|
||||
|
||||
@@ -266,7 +266,12 @@ function formatEntry(entry) {
|
||||
return `${entry.file}:${entry.line} [${entry.kind}] ${entry.reason} (${entry.specifier} -> ${entry.resolvedPath})`;
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
function writeLine(stream, text) {
|
||||
stream.write(`${text}\n`);
|
||||
}
|
||||
|
||||
export async function runPluginExtensionImportBoundaryCheck(argv = process.argv.slice(2), io) {
|
||||
const streams = io ?? { stdout: process.stdout, stderr: process.stderr };
|
||||
const json = argv.includes("--json");
|
||||
const actual = await collectPluginExtensionImportBoundaryInventory();
|
||||
const expected = await readExpectedInventory();
|
||||
@@ -274,33 +279,43 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
const matchesBaseline = missing.length === 0 && unexpected.length === 0;
|
||||
|
||||
if (json) {
|
||||
process.stdout.write(`${JSON.stringify(actual, null, 2)}\n`);
|
||||
writeLine(streams.stdout, JSON.stringify(actual, null, 2));
|
||||
} else {
|
||||
console.log(formatInventoryHuman(actual));
|
||||
console.log(
|
||||
writeLine(streams.stdout, formatInventoryHuman(actual));
|
||||
writeLine(
|
||||
streams.stdout,
|
||||
matchesBaseline
|
||||
? `Baseline matches (${actual.length} entries).`
|
||||
: `Baseline mismatch (${unexpected.length} unexpected, ${missing.length} missing).`,
|
||||
);
|
||||
if (!matchesBaseline) {
|
||||
if (unexpected.length > 0) {
|
||||
console.error("Unexpected entries:");
|
||||
writeLine(streams.stderr, "Unexpected entries:");
|
||||
for (const entry of unexpected) {
|
||||
console.error(`- ${formatEntry(entry)}`);
|
||||
writeLine(streams.stderr, `- ${formatEntry(entry)}`);
|
||||
}
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
console.error("Missing baseline entries:");
|
||||
writeLine(streams.stderr, "Missing baseline entries:");
|
||||
for (const entry of missing) {
|
||||
console.error(`- ${formatEntry(entry)}`);
|
||||
writeLine(streams.stderr, `- ${formatEntry(entry)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchesBaseline) {
|
||||
process.exit(1);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2), io) {
|
||||
const exitCode = await runPluginExtensionImportBoundaryCheck(argv, io);
|
||||
if (!io && exitCode !== 0) {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
runAsScript(import.meta.url, main);
|
||||
|
||||
@@ -255,7 +255,12 @@ function formatEntry(entry) {
|
||||
return `${entry.provider} ${entry.file}:${entry.line} ${entry.reason}`;
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
function writeLine(stream, text) {
|
||||
stream.write(`${text}\n`);
|
||||
}
|
||||
|
||||
export async function runWebSearchProviderBoundaryCheck(argv = process.argv.slice(2), io) {
|
||||
const streams = io ?? { stdout: process.stdout, stderr: process.stderr };
|
||||
const json = argv.includes("--json");
|
||||
const actual = await collectWebSearchProviderBoundaryInventory();
|
||||
const expected = await readExpectedInventory();
|
||||
@@ -263,33 +268,43 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
const matchesBaseline = missing.length === 0 && unexpected.length === 0;
|
||||
|
||||
if (json) {
|
||||
process.stdout.write(`${JSON.stringify(actual, null, 2)}\n`);
|
||||
writeLine(streams.stdout, JSON.stringify(actual, null, 2));
|
||||
} else {
|
||||
console.log(formatInventoryHuman(actual));
|
||||
console.log(
|
||||
writeLine(streams.stdout, formatInventoryHuman(actual));
|
||||
writeLine(
|
||||
streams.stdout,
|
||||
matchesBaseline
|
||||
? `Baseline matches (${actual.length} entries).`
|
||||
: `Baseline mismatch (${unexpected.length} unexpected, ${missing.length} missing).`,
|
||||
);
|
||||
if (!matchesBaseline) {
|
||||
if (unexpected.length > 0) {
|
||||
console.error("Unexpected entries:");
|
||||
writeLine(streams.stderr, "Unexpected entries:");
|
||||
for (const entry of unexpected) {
|
||||
console.error(`- ${formatEntry(entry)}`);
|
||||
writeLine(streams.stderr, `- ${formatEntry(entry)}`);
|
||||
}
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
console.error("Missing baseline entries:");
|
||||
writeLine(streams.stderr, "Missing baseline entries:");
|
||||
for (const entry of missing) {
|
||||
console.error(`- ${formatEntry(entry)}`);
|
||||
writeLine(streams.stderr, `- ${formatEntry(entry)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchesBaseline) {
|
||||
process.exit(1);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2), io) {
|
||||
const exitCode = await runWebSearchProviderBoundaryCheck(argv, io);
|
||||
if (!io && exitCode !== 0) {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
runAsScript(import.meta.url, main);
|
||||
|
||||
@@ -13,11 +13,24 @@ import {
|
||||
const findVerifiedGatewayListenerPidsOnPortSync = vi.hoisted(() =>
|
||||
vi.fn<(port: number) => number[]>(() => []),
|
||||
);
|
||||
const timeState = vi.hoisted(() => ({ now: 0 }));
|
||||
const sleepMock = vi.hoisted(() =>
|
||||
vi.fn(async (ms: number) => {
|
||||
timeState.now += ms;
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("../infra/gateway-processes.js", () => ({
|
||||
findVerifiedGatewayListenerPidsOnPortSync: (port: number) =>
|
||||
findVerifiedGatewayListenerPidsOnPortSync(port),
|
||||
}));
|
||||
vi.mock("../utils.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../utils.js")>("../utils.js");
|
||||
return {
|
||||
...actual,
|
||||
sleep: (ms: number) => sleepMock(ms),
|
||||
};
|
||||
});
|
||||
|
||||
const { restartScheduledTask, stopScheduledTask } = await import("./schtasks.js");
|
||||
const GATEWAY_PORT = 18789;
|
||||
@@ -81,6 +94,12 @@ beforeEach(() => {
|
||||
resetSchtasksBaseMocks();
|
||||
findVerifiedGatewayListenerPidsOnPortSync.mockReset();
|
||||
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]);
|
||||
timeState.now = 0;
|
||||
vi.spyOn(Date, "now").mockImplementation(() => timeState.now);
|
||||
sleepMock.mockReset();
|
||||
sleepMock.mockImplementation(async (ms: number) => {
|
||||
timeState.now += ms;
|
||||
});
|
||||
inspectPortUsage.mockResolvedValue(freePortUsage());
|
||||
});
|
||||
|
||||
|
||||
@@ -13,14 +13,10 @@ vi.mock("./targets.js", async () => {
|
||||
});
|
||||
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
type AgentDeliveryModule = typeof import("./agent-delivery.js");
|
||||
import { resolveAgentDeliveryPlan, resolveAgentOutboundTarget } from "./agent-delivery.js";
|
||||
|
||||
let resolveAgentDeliveryPlan: AgentDeliveryModule["resolveAgentDeliveryPlan"];
|
||||
let resolveAgentOutboundTarget: AgentDeliveryModule["resolveAgentOutboundTarget"];
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ resolveAgentDeliveryPlan, resolveAgentOutboundTarget } = await import("./agent-delivery.js"));
|
||||
beforeEach(() => {
|
||||
mocks.resolveOutboundTarget.mockClear();
|
||||
});
|
||||
|
||||
describe("agent delivery helpers", () => {
|
||||
|
||||
@@ -46,13 +46,10 @@ vi.mock("./deliver.js", () => ({
|
||||
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
|
||||
let sendMessage: typeof import("./message.js").sendMessage;
|
||||
import { sendMessage } from "./message.js";
|
||||
|
||||
describe("sendMessage", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ sendMessage } = await import("./message.js"));
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
mocks.getChannelPlugin.mockClear();
|
||||
mocks.resolveOutboundTarget.mockClear();
|
||||
|
||||
@@ -16,25 +16,31 @@ vi.mock("../config/paths.js", () => ({
|
||||
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
|
||||
}));
|
||||
|
||||
let __testing: typeof import("./restart-stale-pids.js").__testing;
|
||||
let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync;
|
||||
let findGatewayPidsOnPortSync: typeof import("./restart-stale-pids.js").findGatewayPidsOnPortSync;
|
||||
import {
|
||||
__testing,
|
||||
cleanStaleGatewayProcessesSync,
|
||||
findGatewayPidsOnPortSync,
|
||||
} from "./restart-stale-pids.js";
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } =
|
||||
await import("./restart-stale-pids.js"));
|
||||
let currentTimeMs = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
spawnSyncMock.mockReset();
|
||||
resolveLsofCommandSyncMock.mockReset();
|
||||
resolveGatewayPortMock.mockReset();
|
||||
|
||||
currentTimeMs = 0;
|
||||
resolveLsofCommandSyncMock.mockReturnValue("/usr/sbin/lsof");
|
||||
resolveGatewayPortMock.mockReturnValue(18789);
|
||||
__testing.setSleepSyncOverride(() => {});
|
||||
__testing.setSleepSyncOverride((ms) => {
|
||||
currentTimeMs += ms;
|
||||
});
|
||||
__testing.setDateNowOverride(() => currentTimeMs);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__testing.setSleepSyncOverride(null);
|
||||
__testing.setDateNowOverride(null);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -79,11 +85,17 @@ describe.runIf(process.platform !== "win32")("findGatewayPidsOnPortSync", () =>
|
||||
|
||||
describe.runIf(process.platform !== "win32")("cleanStaleGatewayProcessesSync", () => {
|
||||
it("kills stale gateway pids discovered on the gateway port", () => {
|
||||
spawnSyncMock.mockReturnValue({
|
||||
error: undefined,
|
||||
status: 0,
|
||||
stdout: ["p6001", "copenclaw", "p6002", "copenclaw-gateway"].join("\n"),
|
||||
});
|
||||
spawnSyncMock
|
||||
.mockReturnValueOnce({
|
||||
error: undefined,
|
||||
status: 0,
|
||||
stdout: ["p6001", "copenclaw", "p6002", "copenclaw-gateway"].join("\n"),
|
||||
})
|
||||
.mockReturnValue({
|
||||
error: undefined,
|
||||
status: 1,
|
||||
stdout: "",
|
||||
});
|
||||
const killSpy = vi.spyOn(process, "kill").mockImplementation(() => true);
|
||||
|
||||
const killed = cleanStaleGatewayProcessesSync();
|
||||
@@ -97,11 +109,17 @@ describe.runIf(process.platform !== "win32")("cleanStaleGatewayProcessesSync", (
|
||||
});
|
||||
|
||||
it("uses explicit port override when provided", () => {
|
||||
spawnSyncMock.mockReturnValue({
|
||||
error: undefined,
|
||||
status: 0,
|
||||
stdout: ["p7001", "copenclaw"].join("\n"),
|
||||
});
|
||||
spawnSyncMock
|
||||
.mockReturnValueOnce({
|
||||
error: undefined,
|
||||
status: 0,
|
||||
stdout: ["p7001", "copenclaw"].join("\n"),
|
||||
})
|
||||
.mockReturnValue({
|
||||
error: undefined,
|
||||
status: 1,
|
||||
stdout: "",
|
||||
});
|
||||
const killSpy = vi.spyOn(process, "kill").mockImplementation(() => true);
|
||||
|
||||
const killed = cleanStaleGatewayProcessesSync(19999);
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
buildProviderRegistry,
|
||||
createMediaAttachmentCache,
|
||||
normalizeMediaAttachments,
|
||||
runCapability,
|
||||
} from "./runner.js";
|
||||
|
||||
const catalog = [
|
||||
{
|
||||
@@ -23,20 +29,9 @@ vi.mock("../agents/model-catalog.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
let buildProviderRegistry: typeof import("./runner.js").buildProviderRegistry;
|
||||
let createMediaAttachmentCache: typeof import("./runner.js").createMediaAttachmentCache;
|
||||
let normalizeMediaAttachments: typeof import("./runner.js").normalizeMediaAttachments;
|
||||
let runCapability: typeof import("./runner.js").runCapability;
|
||||
|
||||
describe("runCapability image skip", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({
|
||||
buildProviderRegistry,
|
||||
createMediaAttachmentCache,
|
||||
normalizeMediaAttachments,
|
||||
runCapability,
|
||||
} = await import("./runner.js"));
|
||||
beforeEach(() => {
|
||||
loadModelCatalog.mockClear();
|
||||
});
|
||||
|
||||
it("skips image understanding when the active model supports vision", async () => {
|
||||
|
||||
@@ -4,21 +4,27 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPluginSdkEntrySources,
|
||||
buildPluginSdkPackageExports,
|
||||
buildPluginSdkSpecifiers,
|
||||
pluginSdkEntrypoints,
|
||||
} from "./entrypoints.js";
|
||||
import { buildPluginSdkEntrySources, pluginSdkEntrypoints } from "./entrypoints.js";
|
||||
|
||||
const pluginSdkSpecifiers = buildPluginSdkSpecifiers();
|
||||
const require = createRequire(import.meta.url);
|
||||
const tsdownModuleUrl = pathToFileURL(require.resolve("tsdown")).href;
|
||||
const bundledSmokeEntrypoints = [
|
||||
"index",
|
||||
"core",
|
||||
"runtime",
|
||||
"channel-runtime",
|
||||
"provider-setup",
|
||||
"setup",
|
||||
"matrix-runtime-heavy",
|
||||
"windows-spawn",
|
||||
"gateway-runtime",
|
||||
"plugin-runtime",
|
||||
"testing",
|
||||
] as const;
|
||||
|
||||
describe("plugin-sdk bundled exports", () => {
|
||||
it("emits importable bundled subpath entries", { timeout: 240_000 }, async () => {
|
||||
const outDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-build-"));
|
||||
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-consumer-"));
|
||||
|
||||
try {
|
||||
const { build } = await import(tsdownModuleUrl);
|
||||
@@ -45,52 +51,20 @@ describe("plugin-sdk bundled exports", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const packageDir = path.join(fixtureDir, "openclaw");
|
||||
const consumerDir = path.join(fixtureDir, "consumer");
|
||||
const consumerEntry = path.join(consumerDir, "import-plugin-sdk.mjs");
|
||||
|
||||
await fs.mkdir(path.join(packageDir, "dist"), { recursive: true });
|
||||
await fs.symlink(outDir, path.join(packageDir, "dist", "plugin-sdk"), "dir");
|
||||
// Mirror the installed package layout so subpaths can resolve root deps.
|
||||
await fs.symlink(
|
||||
path.join(process.cwd(), "node_modules"),
|
||||
path.join(packageDir, "node_modules"),
|
||||
"dir",
|
||||
// Export list and package-specifier coverage already live in
|
||||
// package-contract-guardrails.test.ts and subpaths.test.ts. Keep this file
|
||||
// focused on the expensive part: can tsdown emit working bundle artifacts?
|
||||
const importResults = await Promise.all(
|
||||
bundledSmokeEntrypoints.map(async (entry) => [
|
||||
entry,
|
||||
typeof (await import(pathToFileURL(path.join(outDir, `${entry}.js`)).href)),
|
||||
]),
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(packageDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
exports: buildPluginSdkPackageExports(),
|
||||
name: "openclaw",
|
||||
type: "module",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
await fs.mkdir(path.join(consumerDir, "node_modules"), { recursive: true });
|
||||
await fs.symlink(packageDir, path.join(consumerDir, "node_modules", "openclaw"), "dir");
|
||||
await fs.writeFile(
|
||||
consumerEntry,
|
||||
[
|
||||
`const specifiers = ${JSON.stringify(pluginSdkSpecifiers)};`,
|
||||
"const results = {};",
|
||||
"for (const specifier of specifiers) {",
|
||||
" results[specifier] = typeof (await import(specifier));",
|
||||
"}",
|
||||
"export default results;",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const { default: importResults } = await import(pathToFileURL(consumerEntry).href);
|
||||
expect(importResults).toEqual(
|
||||
Object.fromEntries(pluginSdkSpecifiers.map((specifier: string) => [specifier, "object"])),
|
||||
expect(Object.fromEntries(importResults)).toEqual(
|
||||
Object.fromEntries(bundledSmokeEntrypoints.map((entry) => [entry, "object"])),
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(outDir, { recursive: true, force: true });
|
||||
await fs.rm(fixtureDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { emitDiagnosticEvent, resetDiagnosticEventsForTest } from "../infra/diagnostic-events.js";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import { clearPluginCommands, getPluginCommandSpecs } from "./commands.js";
|
||||
|
||||
async function importFreshPluginTestModules() {
|
||||
vi.resetModules();
|
||||
@@ -1009,8 +1010,6 @@ module.exports = { id: "skipped-scoped-only", register() { throw new Error("skip
|
||||
},
|
||||
};`,
|
||||
});
|
||||
const { clearPluginCommands, getPluginCommandSpecs } = await import("./commands.js");
|
||||
|
||||
clearPluginCommands();
|
||||
|
||||
const scoped = loadOpenClawPlugins({
|
||||
@@ -1395,6 +1394,8 @@ module.exports = { id: "skipped-scoped-only", register() { throw new Error("skip
|
||||
filename: "cache-eviction.cjs",
|
||||
body: `module.exports = { id: "cache-eviction", register() {} };`,
|
||||
});
|
||||
const previousCacheCap = __testing.maxPluginRegistryCacheEntries;
|
||||
__testing.setMaxPluginRegistryCacheEntriesForTest(4);
|
||||
const stateDirs = Array.from({ length: __testing.maxPluginRegistryCacheEntries + 1 }, () =>
|
||||
makeTempDir(),
|
||||
);
|
||||
@@ -1416,17 +1417,21 @@ module.exports = { id: "skipped-scoped-only", register() { throw new Error("skip
|
||||
},
|
||||
});
|
||||
|
||||
const first = loadWithStateDir(stateDirs[0] ?? makeTempDir());
|
||||
const second = loadWithStateDir(stateDirs[1] ?? makeTempDir());
|
||||
try {
|
||||
const first = loadWithStateDir(stateDirs[0] ?? makeTempDir());
|
||||
const second = loadWithStateDir(stateDirs[1] ?? makeTempDir());
|
||||
|
||||
expect(loadWithStateDir(stateDirs[0] ?? makeTempDir())).toBe(first);
|
||||
expect(loadWithStateDir(stateDirs[0] ?? makeTempDir())).toBe(first);
|
||||
|
||||
for (const stateDir of stateDirs.slice(2)) {
|
||||
loadWithStateDir(stateDir);
|
||||
for (const stateDir of stateDirs.slice(2)) {
|
||||
loadWithStateDir(stateDir);
|
||||
}
|
||||
|
||||
expect(loadWithStateDir(stateDirs[0] ?? makeTempDir())).toBe(first);
|
||||
expect(loadWithStateDir(stateDirs[1] ?? makeTempDir())).not.toBe(second);
|
||||
} finally {
|
||||
__testing.setMaxPluginRegistryCacheEntriesForTest(previousCacheCap);
|
||||
}
|
||||
|
||||
expect(loadWithStateDir(stateDirs[0] ?? makeTempDir())).toBe(first);
|
||||
expect(loadWithStateDir(stateDirs[1] ?? makeTempDir())).not.toBe(second);
|
||||
});
|
||||
|
||||
it("normalizes bundled plugin env overrides against the provided env", () => {
|
||||
|
||||
@@ -101,6 +101,7 @@ type CachedPluginState = {
|
||||
};
|
||||
|
||||
const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 128;
|
||||
let pluginRegistryCacheEntryCap = MAX_PLUGIN_REGISTRY_CACHE_ENTRIES;
|
||||
const registryCache = new Map<string, CachedPluginState>();
|
||||
const openAllowlistWarningCache = new Set<string>();
|
||||
const LAZY_RUNTIME_REFLECTION_KEYS = [
|
||||
@@ -139,7 +140,15 @@ export const __testing = {
|
||||
resolvePluginSdkAliasFile,
|
||||
resolvePluginRuntimeModulePath,
|
||||
shouldPreferNativeJiti,
|
||||
maxPluginRegistryCacheEntries: MAX_PLUGIN_REGISTRY_CACHE_ENTRIES,
|
||||
get maxPluginRegistryCacheEntries() {
|
||||
return pluginRegistryCacheEntryCap;
|
||||
},
|
||||
setMaxPluginRegistryCacheEntriesForTest(value?: number) {
|
||||
pluginRegistryCacheEntryCap =
|
||||
typeof value === "number" && Number.isFinite(value) && value > 0
|
||||
? Math.max(1, Math.floor(value))
|
||||
: MAX_PLUGIN_REGISTRY_CACHE_ENTRIES;
|
||||
},
|
||||
};
|
||||
|
||||
function getCachedPluginRegistry(cacheKey: string): CachedPluginState | undefined {
|
||||
@@ -158,7 +167,7 @@ function setCachedPluginRegistry(cacheKey: string, state: CachedPluginState): vo
|
||||
registryCache.delete(cacheKey);
|
||||
}
|
||||
registryCache.set(cacheKey, state);
|
||||
while (registryCache.size > MAX_PLUGIN_REGISTRY_CACHE_ENTRIES) {
|
||||
while (registryCache.size > pluginRegistryCacheEntryCap) {
|
||||
const oldestKey = registryCache.keys().next().value;
|
||||
if (!oldestKey) {
|
||||
break;
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectArchitectureSmells } from "../scripts/check-architecture-smells.mjs";
|
||||
import { collectArchitectureSmells, main } from "../scripts/check-architecture-smells.mjs";
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const scriptPath = path.join(repoRoot, "scripts", "check-architecture-smells.mjs");
|
||||
function createCapturedIo() {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
return {
|
||||
io: {
|
||||
stdout: {
|
||||
write(chunk) {
|
||||
stdout += String(chunk);
|
||||
},
|
||||
},
|
||||
stderr: {
|
||||
write(chunk) {
|
||||
stderr += String(chunk);
|
||||
},
|
||||
},
|
||||
},
|
||||
readStdout: () => stdout,
|
||||
readStderr: () => stderr,
|
||||
};
|
||||
}
|
||||
|
||||
describe("architecture smell inventory", () => {
|
||||
it("produces stable sorted output", async () => {
|
||||
@@ -26,11 +42,11 @@ describe("architecture smell inventory", () => {
|
||||
});
|
||||
|
||||
it("script json output matches the collector", async () => {
|
||||
const stdout = execFileSync(process.execPath, [scriptPath, "--json"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
});
|
||||
const captured = createCapturedIo();
|
||||
const exitCode = await main(["--json"], captured.io);
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual(await collectArchitectureSmells());
|
||||
expect(exitCode).toBe(0);
|
||||
expect(captured.readStderr()).toBe("");
|
||||
expect(JSON.parse(captured.readStdout())).toEqual(await collectArchitectureSmells());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectExtensionPluginSdkBoundaryInventory } from "../scripts/check-extension-plugin-sdk-boundary.mjs";
|
||||
import {
|
||||
collectExtensionPluginSdkBoundaryInventory,
|
||||
main,
|
||||
} from "../scripts/check-extension-plugin-sdk-boundary.mjs";
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const scriptPath = path.join(repoRoot, "scripts", "check-extension-plugin-sdk-boundary.mjs");
|
||||
const relativeOutsidePackageBaselinePath = path.join(
|
||||
repoRoot,
|
||||
"test",
|
||||
@@ -13,6 +14,27 @@ const relativeOutsidePackageBaselinePath = path.join(
|
||||
"extension-relative-outside-package-inventory.json",
|
||||
);
|
||||
|
||||
function createCapturedIo() {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
return {
|
||||
io: {
|
||||
stdout: {
|
||||
write(chunk) {
|
||||
stdout += String(chunk);
|
||||
},
|
||||
},
|
||||
stderr: {
|
||||
write(chunk) {
|
||||
stderr += String(chunk);
|
||||
},
|
||||
},
|
||||
},
|
||||
readStdout: () => stdout,
|
||||
readStderr: () => stderr,
|
||||
};
|
||||
}
|
||||
|
||||
describe("extension src outside plugin-sdk boundary inventory", () => {
|
||||
it("is currently empty", async () => {
|
||||
const inventory = await collectExtensionPluginSdkBoundaryInventory("src-outside-plugin-sdk");
|
||||
@@ -38,17 +60,13 @@ describe("extension src outside plugin-sdk boundary inventory", () => {
|
||||
).toEqual(first);
|
||||
});
|
||||
|
||||
it("script json output is empty", () => {
|
||||
const stdout = execFileSync(
|
||||
process.execPath,
|
||||
[scriptPath, "--mode=src-outside-plugin-sdk", "--json"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
it("script json output is empty", async () => {
|
||||
const captured = createCapturedIo();
|
||||
const exitCode = await main(["--mode=src-outside-plugin-sdk", "--json"], captured.io);
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual([]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(captured.readStderr()).toBe("");
|
||||
expect(JSON.parse(captured.readStdout())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,17 +77,13 @@ describe("extension plugin-sdk-internal boundary inventory", () => {
|
||||
expect(inventory).toEqual([]);
|
||||
});
|
||||
|
||||
it("script json output is empty", () => {
|
||||
const stdout = execFileSync(
|
||||
process.execPath,
|
||||
[scriptPath, "--mode=plugin-sdk-internal", "--json"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
it("script json output is empty", async () => {
|
||||
const captured = createCapturedIo();
|
||||
const exitCode = await main(["--mode=plugin-sdk-internal", "--json"], captured.io);
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual([]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(captured.readStderr()).toBe("");
|
||||
expect(JSON.parse(captured.readStdout())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,17 +95,13 @@ describe("extension relative-outside-package boundary inventory", () => {
|
||||
expect(inventory).toEqual(expected);
|
||||
});
|
||||
|
||||
it("script json output matches the checked-in baseline", () => {
|
||||
const stdout = execFileSync(
|
||||
process.execPath,
|
||||
[scriptPath, "--mode=relative-outside-package", "--json"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
it("script json output matches the checked-in baseline", async () => {
|
||||
const captured = createCapturedIo();
|
||||
const exitCode = await main(["--mode=relative-outside-package", "--json"], captured.io);
|
||||
const expected = JSON.parse(fs.readFileSync(relativeOutsidePackageBaselinePath, "utf8"));
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual(expected);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(captured.readStderr()).toBe("");
|
||||
expect(JSON.parse(captured.readStdout())).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
1662
test/fixtures/test-timings.unit.json
vendored
1662
test/fixtures/test-timings.unit.json
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,13 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectPluginExtensionImportBoundaryInventory,
|
||||
diffInventory,
|
||||
main,
|
||||
} from "../scripts/check-plugin-extension-import-boundary.mjs";
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const scriptPath = path.join(repoRoot, "scripts", "check-plugin-extension-import-boundary.mjs");
|
||||
const baselinePath = path.join(
|
||||
repoRoot,
|
||||
"test",
|
||||
@@ -20,6 +19,27 @@ function readBaseline() {
|
||||
return JSON.parse(readFileSync(baselinePath, "utf8"));
|
||||
}
|
||||
|
||||
function createCapturedIo() {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
return {
|
||||
io: {
|
||||
stdout: {
|
||||
write(chunk) {
|
||||
stdout += String(chunk);
|
||||
},
|
||||
},
|
||||
stderr: {
|
||||
write(chunk) {
|
||||
stderr += String(chunk);
|
||||
},
|
||||
},
|
||||
},
|
||||
readStdout: () => stdout,
|
||||
readStderr: () => stderr,
|
||||
};
|
||||
}
|
||||
|
||||
describe("plugin extension import boundary inventory", () => {
|
||||
it("keeps dedicated web-search registry shims out of the remaining inventory", async () => {
|
||||
const inventory = await collectPluginExtensionImportBoundaryInventory();
|
||||
@@ -65,12 +85,12 @@ describe("plugin extension import boundary inventory", () => {
|
||||
expect(diffInventory(expected, actual)).toEqual({ missing: [], unexpected: [] });
|
||||
});
|
||||
|
||||
it("script json output matches the baseline exactly", () => {
|
||||
const stdout = execFileSync(process.execPath, [scriptPath, "--json"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
});
|
||||
it("script json output matches the baseline exactly", async () => {
|
||||
const captured = createCapturedIo();
|
||||
const exitCode = await main(["--json"], captured.io);
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual(readBaseline());
|
||||
expect(exitCode).toBe(0);
|
||||
expect(captured.readStderr()).toBe("");
|
||||
expect(JSON.parse(captured.readStdout())).toEqual(readBaseline());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectWebSearchProviderBoundaryInventory } from "../scripts/check-web-search-provider-boundaries.mjs";
|
||||
import {
|
||||
collectWebSearchProviderBoundaryInventory,
|
||||
main,
|
||||
} from "../scripts/check-web-search-provider-boundaries.mjs";
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const scriptPath = path.join(repoRoot, "scripts", "check-web-search-provider-boundaries.mjs");
|
||||
function createCapturedIo() {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
return {
|
||||
io: {
|
||||
stdout: {
|
||||
write(chunk) {
|
||||
stdout += String(chunk);
|
||||
},
|
||||
},
|
||||
stderr: {
|
||||
write(chunk) {
|
||||
stderr += String(chunk);
|
||||
},
|
||||
},
|
||||
},
|
||||
readStdout: () => stdout,
|
||||
readStderr: () => stderr,
|
||||
};
|
||||
}
|
||||
|
||||
describe("web search provider boundary inventory", () => {
|
||||
it("has no remaining production inventory in core", async () => {
|
||||
@@ -35,12 +54,12 @@ describe("web search provider boundary inventory", () => {
|
||||
).toEqual(first);
|
||||
});
|
||||
|
||||
it("script json output is empty", () => {
|
||||
const stdout = execFileSync(process.execPath, [scriptPath, "--json"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
});
|
||||
it("script json output is empty", async () => {
|
||||
const captured = createCapturedIo();
|
||||
const exitCode = await main(["--json"], captured.io);
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual([]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(captured.readStderr()).toBe("");
|
||||
expect(JSON.parse(captured.readStdout())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user