test: slim channel contract hotspots

This commit is contained in:
Peter Steinberger
2026-04-18 01:15:18 +01:00
parent 569247cff8
commit 3abb5fd291
22 changed files with 99 additions and 224 deletions

View File

@@ -0,0 +1,2 @@
export { prepareSlackMessage } from "./src/monitor/message-handler/prepare.js";
export { createInboundSlackTestContext } from "./src/monitor/message-handler/prepare.test-helpers.js";

View File

@@ -0,0 +1 @@
export { createSlackOutboundPayloadHarness } from "./src/outbound-payload.test-harness.js";

View File

@@ -0,0 +1 @@
export { whatsappOutbound } from "./src/outbound-adapter.js";

View File

@@ -11,8 +11,7 @@ import {
import { chunkText } from "openclaw/plugin-sdk/reply-runtime";
import { shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
import { WHATSAPP_LEGACY_OUTBOUND_SEND_DEP_KEYS } from "./outbound-send-deps.js";
import { resolveWhatsAppOutboundTarget } from "./runtime-api.js";
import { sendPollWhatsApp } from "./send.js";
import { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js";
function trimLeadingWhitespace(text: string | undefined): string {
return text?.trimStart() ?? "";
@@ -90,7 +89,9 @@ export const whatsappOutbound: ChannelOutboundAdapter = {
});
},
sendPoll: async ({ cfg, to, poll, accountId }) =>
await sendPollWhatsApp(to, poll, {
await (
await import("./send.js")
).sendPollWhatsApp(to, poll, {
verbose: shouldLogVerbose(),
accountId: accountId ?? undefined,
cfg,

View File

@@ -248,6 +248,14 @@ const sourceAnalysisCache = new Map<string, SourceAnalysis>();
let extensionSourceFilesCache: string[] | null = null;
let coreSourceFilesCache: string[] | null = null;
const extensionFilesCache = new Map<string, string[]>();
const STATIC_FROM_IMPORT_RE =
/^\s*import(?:\s+type)?\s+(?!["'])(?:[\s\S]*?)\s+from\s*["']([^"']+)["']/gmu;
const STATIC_SIDE_EFFECT_IMPORT_RE = /^\s*import\s*["']([^"']+)["']/gmu;
const RE_EXPORT_STAR_RE =
/^\s*export\s+(?:type\s+)?\*\s*(?:as\s+\w+\s+)?from\s*["']([^"']+)["']/gmu;
const RE_EXPORT_NAMED_RE = /^\s*export\s+(?:type\s+)?\{[^}]*\}\s+from\s*["']([^"']+)["']/gmu;
const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/gmu;
const REQUIRE_RE = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/gmu;
type SourceFileCollectorOptions = {
rootDir: string;
@@ -388,16 +396,18 @@ function collectExtensionFiles(extensionId: string): string[] {
function collectModuleSpecifiers(text: string): string[] {
const patterns = [
/\bimport\s*\(\s*["']([^"']+\.(?:[cm]?[jt]sx?))["']\s*\)/g,
/\brequire\s*\(\s*["']([^"']+\.(?:[cm]?[jt]sx?))["']\s*\)/g,
/\b(?:import|export)\b[\s\S]*?\bfrom\s*["']([^"']+\.(?:[cm]?[jt]sx?))["']/g,
/\bimport\s*["']([^"']+\.(?:[cm]?[jt]sx?))["']/g,
DYNAMIC_IMPORT_RE,
REQUIRE_RE,
STATIC_FROM_IMPORT_RE,
STATIC_SIDE_EFFECT_IMPORT_RE,
RE_EXPORT_STAR_RE,
RE_EXPORT_NAMED_RE,
] as const;
const specifiers = new Set<string>();
for (const pattern of patterns) {
for (const match of text.matchAll(pattern)) {
const specifier = match[1]?.trim();
if (specifier) {
if (specifier && /\.(?:[cm]?[jt]sx?)$/u.test(specifier)) {
specifiers.add(specifier);
}
}

View File

@@ -0,0 +1,28 @@
import { describe } from "vitest";
import { installDiscordInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.discord.js";
import { installSignalInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.signal.js";
import { installSlackInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.slack.js";
import { installTelegramInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.telegram.js";
import { installWhatsAppInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.whatsapp.js";
describe("inbound channel contracts", () => {
describe("discord", () => {
installDiscordInboundContractSuite();
});
describe("signal", () => {
installSignalInboundContractSuite();
});
describe("slack", () => {
installSlackInboundContractSuite();
});
describe("telegram", () => {
installTelegramInboundContractSuite();
});
describe("whatsapp", () => {
installWhatsAppInboundContractSuite();
});
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installDiscordInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.discord.js";
describe("discord inbound contract", () => {
installDiscordInboundContractSuite();
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installSignalInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.signal.js";
describe("signal inbound contract", () => {
installSignalInboundContractSuite();
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installSlackInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.slack.js";
describe("slack inbound contract", () => {
installSlackInboundContractSuite();
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installTelegramInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.telegram.js";
describe("telegram inbound contract", () => {
installTelegramInboundContractSuite();
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installWhatsAppInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.whatsapp.js";
describe("whatsapp inbound contract", () => {
installWhatsAppInboundContractSuite();
});

View File

@@ -0,0 +1,35 @@
import { describe } from "vitest";
import {
installDirectTextMediaOutboundPayloadContractSuite,
installDiscordOutboundPayloadContractSuite,
installSlackOutboundPayloadContractSuite,
installWhatsAppOutboundPayloadContractSuite,
installZaloOutboundPayloadContractSuite,
installZalouserOutboundPayloadContractSuite,
} from "../../../../test/helpers/channels/outbound-payload-contract.js";
describe("outbound payload contracts", () => {
describe("discord", () => {
installDiscordOutboundPayloadContractSuite();
});
describe("imessage", () => {
installDirectTextMediaOutboundPayloadContractSuite();
});
describe("slack", () => {
installSlackOutboundPayloadContractSuite();
});
describe("whatsapp", () => {
installWhatsAppOutboundPayloadContractSuite();
});
describe("zalo", () => {
installZaloOutboundPayloadContractSuite();
});
describe("zalouser", () => {
installZalouserOutboundPayloadContractSuite();
});
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installDiscordOutboundPayloadContractSuite } from "../../../../test/helpers/channels/outbound-payload-contract.js";
describe("discord outbound payload contract", () => {
installDiscordOutboundPayloadContractSuite();
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installDirectTextMediaOutboundPayloadContractSuite } from "../../../../test/helpers/channels/outbound-payload-contract.js";
describe("imessage outbound payload contract", () => {
installDirectTextMediaOutboundPayloadContractSuite();
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installSlackOutboundPayloadContractSuite } from "../../../../test/helpers/channels/outbound-payload-contract.js";
describe("slack outbound payload contract", () => {
installSlackOutboundPayloadContractSuite();
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installWhatsAppOutboundPayloadContractSuite } from "../../../../test/helpers/channels/outbound-payload-contract.js";
describe("whatsapp outbound payload contract", () => {
installWhatsAppOutboundPayloadContractSuite();
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installZaloOutboundPayloadContractSuite } from "../../../../test/helpers/channels/outbound-payload-contract.js";
describe("zalo outbound payload contract", () => {
installZaloOutboundPayloadContractSuite();
});

View File

@@ -1,6 +0,0 @@
import { describe } from "vitest";
import { installZalouserOutboundPayloadContractSuite } from "../../../../test/helpers/channels/outbound-payload-contract.js";
describe("zalouser outbound payload contract", () => {
installZalouserOutboundPayloadContractSuite();
});

View File

@@ -1,12 +1,9 @@
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
import { readdirSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import * as tar from "tar";
import { afterEach, describe, expect, it } from "vitest";
import { describe, expect, it } from "vitest";
import { pluginSdkEntrypoints } from "../../plugin-sdk/entrypoints.js";
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "../test-helpers/fs-fixtures.js";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
@@ -15,9 +12,6 @@ const PUBLIC_CONTRACT_REFERENCE_FILES = [
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
] as const;
const PLUGIN_SDK_SUBPATH_PATTERN = /openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)\b/g;
const NPM_PACK_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/;
const tempDirs: string[] = [];
function collectPluginSdkPackageExports(): string[] {
const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
@@ -87,124 +81,6 @@ function createRootPackageRequire() {
return createRequire(pathToFileURL(resolve(REPO_ROOT, "package.json")).href);
}
function isNpmExecPath(value: string): boolean {
return /^npm(?:-cli)?(?:\.(?:c?js|cmd|exe))?$/.test(
value.split(/[\\/]/).at(-1)?.toLowerCase() ?? "",
);
}
function escapeForCmdExe(arg: string): string {
if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(arg)) {
throw new Error(`unsafe Windows cmd.exe argument detected: ${JSON.stringify(arg)}`);
}
if (!arg.includes(" ") && !arg.includes('"')) {
return arg;
}
return `"${arg.replace(/"/g, '""')}"`;
}
function buildCmdExeCommandLine(command: string, args: string[]): string {
return [escapeForCmdExe(command), ...args.map(escapeForCmdExe)].join(" ");
}
type NpmCommandInvocation = {
command: string;
args: string[];
env?: NodeJS.ProcessEnv;
windowsVerbatimArguments?: boolean;
};
function resolveNpmCommandInvocation(npmArgs: string[]): NpmCommandInvocation {
const npmExecPath = process.env.npm_execpath;
if (typeof npmExecPath === "string" && npmExecPath.length > 0 && isNpmExecPath(npmExecPath)) {
return { command: process.execPath, args: [npmExecPath, ...npmArgs] };
}
if (process.platform !== "win32") {
return { command: "npm", args: npmArgs };
}
const nodeDir = dirname(process.execPath);
const npmCliCandidates = [
resolve(nodeDir, "../lib/node_modules/npm/bin/npm-cli.js"),
resolve(nodeDir, "node_modules/npm/bin/npm-cli.js"),
];
const npmCliPath = npmCliCandidates.find((candidate) => existsSync(candidate));
if (npmCliPath) {
return { command: process.execPath, args: [npmCliPath, ...npmArgs] };
}
const npmExePath = resolve(nodeDir, "npm.exe");
if (existsSync(npmExePath)) {
return { command: npmExePath, args: npmArgs };
}
const npmCmdPath = resolve(nodeDir, "npm.cmd");
if (existsSync(npmCmdPath)) {
return {
command: process.env.ComSpec ?? "cmd.exe",
args: ["/d", "/s", "/c", buildCmdExeCommandLine(npmCmdPath, npmArgs)],
windowsVerbatimArguments: true,
};
}
return {
command: process.env.ComSpec ?? "cmd.exe",
args: ["/d", "/s", "/c", buildCmdExeCommandLine("npm.cmd", npmArgs)],
windowsVerbatimArguments: true,
};
}
function packOpenClawToTempDir(packDir: string): string {
const invocation = resolveNpmCommandInvocation([
"pack",
"--ignore-scripts",
"--json",
"--pack-destination",
packDir,
]);
const result = spawnSync(invocation.command, invocation.args, {
cwd: REPO_ROOT,
encoding: "utf8",
env: {
...process.env,
...invocation.env,
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
},
maxBuffer: NPM_PACK_MAX_BUFFER_BYTES,
stdio: ["ignore", "pipe", "pipe"],
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error((result.stderr || result.stdout || "npm pack failed").trim());
}
const raw = result.stdout;
const parsed = JSON.parse(raw) as Array<{ filename?: string }>;
const filename = parsed[0]?.filename?.trim();
if (!filename) {
throw new Error(`npm pack did not return a filename: ${raw}`);
}
return join(packDir, filename);
}
async function readPackedRootPackageJson(archivePath: string): Promise<{
dependencies?: Record<string, string>;
}> {
const extractDir = makeTrackedTempDir("openclaw-packed-root-package-json", tempDirs);
await tar.x({
file: archivePath,
cwd: extractDir,
filter: (entryPath) => entryPath === "package/package.json",
strict: true,
});
return JSON.parse(readFileSync(join(extractDir, "package", "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
};
}
function collectExtensionFiles(dir: string): string[] {
const entries = readdirSync(dir, { withFileTypes: true });
const files: string[] = [];
@@ -259,10 +135,6 @@ function collectExtensionCoreImportLeaks(): Array<{ file: string; specifier: str
}
describe("plugin-sdk package contract guardrails", () => {
afterEach(() => {
cleanupTrackedTempDirs(tempDirs);
});
it("keeps package.json exports aligned with built plugin-sdk entrypoints", () => {
expect(collectPluginSdkPackageExports()).toEqual([...pluginSdkEntrypoints].toSorted());
});
@@ -291,7 +163,7 @@ describe("plugin-sdk package contract guardrails", () => {
expect(failures).toEqual([]);
});
it("mirrors matrix runtime deps needed by the bundled host graph", () => {
it("mirrors package runtime deps needed by bundled host graphs", () => {
const rootRuntimeDeps = collectRuntimeDependencySpecs(readRootPackageJson());
const matrixPackageJson = readMatrixPackageJson();
const matrixRuntimeDeps = collectRuntimeDependencySpecs(matrixPackageJson);
@@ -304,6 +176,7 @@ describe("plugin-sdk package contract guardrails", () => {
]) {
expect(rootRuntimeDeps.get(dep)).toBe(matrixRuntimeDeps.get(dep));
}
expect(rootRuntimeDeps.has("@openclaw/plugin-package-contract")).toBe(false);
});
it("resolves matrix crypto WASM from the root runtime surface", () => {
@@ -316,20 +189,6 @@ describe("plugin-sdk package contract guardrails", () => {
expect(resolvedPath).toContain("@matrix-org/matrix-sdk-crypto-wasm");
});
it("keeps matrix crypto WASM in the packed artifact manifest", async () => {
const tempRoot = makeTrackedTempDir("openclaw-matrix-wasm-pack", tempDirs);
const packDir = join(tempRoot, "pack");
mkdirSync(packDir, { recursive: true });
const archivePath = packOpenClawToTempDir(packDir);
const packedPackageJson = await readPackedRootPackageJson(archivePath);
const matrixPackageJson = readMatrixPackageJson();
expect(packedPackageJson.dependencies?.["@matrix-org/matrix-sdk-crypto-wasm"]).toBe(
matrixPackageJson.dependencies?.["@matrix-org/matrix-sdk-crypto-wasm"],
);
expect(packedPackageJson.dependencies?.["@openclaw/plugin-package-contract"]).toBeUndefined();
});
it("keeps extension sources on public sdk or local package seams", () => {
expect(collectExtensionCoreImportLeaks()).toEqual([]);
});

View File

@@ -34,7 +34,7 @@ type SlackTestApi = {
const slackPrepareTestApiModuleId = resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "slack",
artifactBasename: "test-api.js",
artifactBasename: "inbound-contract-test-api.js",
});
let slackTestApiPromise: Promise<SlackTestApi> | undefined;

View File

@@ -21,12 +21,12 @@ const discordOutboundAdapterModuleId = resolveRelativeBundledPluginPublicModuleI
const slackTestApiModuleId = resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "slack",
artifactBasename: "test-api.js",
artifactBasename: "outbound-payload-test-api.js",
});
const whatsappTestApiModuleId = resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "whatsapp",
artifactBasename: "test-api.js",
artifactBasename: "outbound-payload-test-api.js",
});
let discordOutboundCache: Promise<ChannelOutboundAdapter> | undefined;

View File

@@ -1,4 +1,4 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "../../../src/agents/auth-profiles/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import type { ModelDefinitionConfig } from "../../../src/config/types.models.js";
@@ -179,6 +179,7 @@ function installDiscoveryHooks(
providerIds: readonly BundledProviderUnderTest[],
) {
beforeAll(async () => {
vi.resetModules();
vi.doMock("openclaw/plugin-sdk/agent-runtime", () => {
return {
ensureAuthProfileStore: ensureAuthProfileStoreMock,
@@ -311,10 +312,13 @@ function installDiscoveryHooks(
"cloudflare-ai-gateway",
);
}
setRuntimeAuthStore();
});
beforeEach(() => {
setRuntimeAuthStore();
});
afterEach(() => {
vi.restoreAllMocks();
resolveCopilotApiTokenMock.mockReset();
buildOllamaProviderMock.mockReset();