test: move remaining plugin-sdk guardrails to contracts

This commit is contained in:
Peter Steinberger
2026-04-01 02:46:50 +01:00
parent 7e02005ca9
commit 016f065d7e
6 changed files with 12 additions and 12 deletions

View File

@@ -1,599 +0,0 @@
import { readdirSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
BUNDLED_PLUGIN_PATH_PREFIX,
BUNDLED_PLUGIN_ROOT_DIR,
bundledPluginFile,
} from "../../test/helpers/bundled-plugin-paths.js";
import { GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES } from "../plugins/public-artifacts.js";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
const ALLOWED_EXTENSION_PUBLIC_SURFACES = new Set(GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES);
ALLOWED_EXTENSION_PUBLIC_SURFACES.add("test-api.js");
const BUNDLED_EXTENSION_IDS = readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== "shared")
.map((entry) => entry.name)
.toSorted((left, right) => right.length - left.length);
const GUARDED_CHANNEL_EXTENSIONS = new Set([
"bluebubbles",
"discord",
"feishu",
"googlechat",
"imessage",
"irc",
"line",
"matrix",
"mattermost",
"msteams",
"nostr",
"nextcloud-talk",
"signal",
"slack",
"synology-chat",
"telegram",
"tlon",
"twitch",
"whatsapp",
"zalo",
"zalouser",
]);
type GuardedSource = {
path: string;
forbiddenPatterns: RegExp[];
};
const SAME_CHANNEL_SDK_GUARDS: GuardedSource[] = [
{
path: bundledPluginFile("discord", "src/shared.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/discord["']/, /plugin-sdk-internal\/discord/],
},
{
path: bundledPluginFile("slack", "src/shared.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/slack["']/, /plugin-sdk-internal\/slack/],
},
{
path: bundledPluginFile("telegram", "src/shared.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/telegram["']/, /plugin-sdk-internal\/telegram/],
},
{
path: bundledPluginFile("telegram", "src/account-inspect.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/account-resolution["']/],
},
{
path: bundledPluginFile("telegram", "src/accounts.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/account-resolution["']/],
},
{
path: bundledPluginFile("telegram", "src/token.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/account-resolution["']/],
},
{
path: bundledPluginFile("telegram", "src/channel.ts"),
forbiddenPatterns: [/["']\.\.\/runtime-api\.js["']/],
},
{
path: bundledPluginFile("telegram", "src/action-runtime.ts"),
forbiddenPatterns: [/["']\.\.\/runtime-api\.js["']/],
},
{
path: bundledPluginFile("telegram", "src/accounts.ts"),
forbiddenPatterns: [/["']\.\.\/runtime-api\.js["']/],
},
{
path: bundledPluginFile("telegram", "src/account-inspect.ts"),
forbiddenPatterns: [/["']\.\.\/runtime-api\.js["']/],
},
{
path: bundledPluginFile("telegram", "src/api-fetch.ts"),
forbiddenPatterns: [/["']\.\.\/runtime-api\.js["']/],
},
{
path: bundledPluginFile("telegram", "src/channel.setup.ts"),
forbiddenPatterns: [/["']\.\.\/runtime-api\.js["']/],
},
{
path: bundledPluginFile("telegram", "src/probe.ts"),
forbiddenPatterns: [/["']\.\.\/runtime-api\.js["']/],
},
{
path: bundledPluginFile("telegram", "src/setup-core.ts"),
forbiddenPatterns: [/["']\.\.\/runtime-api\.js["']/],
},
{
path: bundledPluginFile("telegram", "src/token.ts"),
forbiddenPatterns: [/["']\.\.\/runtime-api\.js["']/],
},
{
path: bundledPluginFile("imessage", "src/shared.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/imessage["']/, /plugin-sdk-internal\/imessage/],
},
{
path: bundledPluginFile("whatsapp", "src/shared.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/whatsapp["']/, /plugin-sdk-internal\/whatsapp/],
},
{
path: bundledPluginFile("signal", "src/shared.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/signal["']/, /plugin-sdk-internal\/signal/],
},
{
path: bundledPluginFile("signal", "src/runtime-api.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/signal["']/, /plugin-sdk-internal\/signal/],
},
];
const SETUP_BARREL_GUARDS: GuardedSource[] = [
{
path: bundledPluginFile("signal", "src/setup-core.ts"),
forbiddenPatterns: [/\bformatCliCommand\b/, /\bformatDocsLink\b/],
},
{
path: bundledPluginFile("signal", "src/setup-surface.ts"),
forbiddenPatterns: [
/\bdetectBinary\b/,
/\binstallSignalCli\b/,
/\bformatCliCommand\b/,
/\bformatDocsLink\b/,
],
},
{
path: bundledPluginFile("slack", "src/setup-core.ts"),
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: bundledPluginFile("slack", "src/setup-surface.ts"),
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: bundledPluginFile("discord", "src/setup-core.ts"),
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: bundledPluginFile("discord", "src/setup-surface.ts"),
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: bundledPluginFile("imessage", "src/setup-core.ts"),
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: bundledPluginFile("imessage", "src/setup-surface.ts"),
forbiddenPatterns: [/\bdetectBinary\b/, /\bformatDocsLink\b/],
},
{
path: bundledPluginFile("telegram", "src/setup-core.ts"),
forbiddenPatterns: [/\bformatCliCommand\b/, /\bformatDocsLink\b/],
},
{
path: bundledPluginFile("whatsapp", "src/setup-surface.ts"),
forbiddenPatterns: [/\bformatCliCommand\b/, /\bformatDocsLink\b/],
},
];
const CHANNEL_CONFIG_SCHEMA_GUARDS: GuardedSource[] = [
{
path: bundledPluginFile("tlon", "src/config-schema.ts"),
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/core["']/],
},
];
const LOCAL_EXTENSION_API_BARREL_GUARDS = [
"acpx",
"bluebubbles",
"device-pair",
"diagnostics-otel",
"discord",
"diffs",
"feishu",
"google",
"imessage",
"irc",
"llm-task",
"line",
"lobster",
"matrix",
"mattermost",
"memory-lancedb",
"msteams",
"nextcloud-talk",
"nostr",
"ollama",
"open-prose",
"phone-control",
"copilot-proxy",
"sglang",
"zai",
"signal",
"synology-chat",
"talk-voice",
"telegram",
"thread-ownership",
"tlon",
"voice-call",
"vllm",
"whatsapp",
"twitch",
"xai",
"zalo",
"zalouser",
] as const;
const LOCAL_EXTENSION_API_BARREL_EXCEPTIONS = [
// Direct import avoids a circular init path:
// accounts.ts -> runtime-api.ts -> src/plugin-sdk/matrix -> plugin api barrel -> accounts.ts
bundledPluginFile("matrix", "src/matrix/accounts.ts"),
] as const;
const sourceTextCache = new Map<string, string>();
type SourceAnalysis = {
text: string;
importSpecifiers: string[];
extensionImports: string[];
};
const sourceAnalysisCache = new Map<string, SourceAnalysis>();
let extensionSourceFilesCache: string[] | null = null;
let coreSourceFilesCache: string[] | null = null;
const extensionFilesCache = new Map<string, string[]>();
type SourceFileCollectorOptions = {
rootDir: string;
shouldSkipPath?: (normalizedFullPath: string) => boolean;
shouldSkipEntry?: (params: { entryName: string; normalizedFullPath: string }) => boolean;
};
function readSource(path: string): string {
const fullPath = resolve(REPO_ROOT, path);
const cached = sourceTextCache.get(fullPath);
if (cached !== undefined) {
return cached;
}
const text = readFileSync(fullPath, "utf8");
sourceTextCache.set(fullPath, text);
return text;
}
function normalizePath(path: string): string {
return path.replaceAll("\\", "/");
}
function collectSourceFiles(
cached: string[] | undefined | null,
options: SourceFileCollectorOptions,
): string[] {
if (cached) {
return cached;
}
const files: string[] = [];
const stack = [options.rootDir];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
for (const entry of readdirSync(current, { withFileTypes: true })) {
const fullPath = resolve(current, entry.name);
const normalizedFullPath = normalizePath(fullPath);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
continue;
}
if (options.shouldSkipPath?.(normalizedFullPath)) {
continue;
}
stack.push(fullPath);
continue;
}
if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) {
continue;
}
if (entry.name.endsWith(".d.ts")) {
continue;
}
if (
options.shouldSkipPath?.(normalizedFullPath) ||
options.shouldSkipEntry?.({ entryName: entry.name, normalizedFullPath })
) {
continue;
}
files.push(fullPath);
}
}
return files;
}
function readSetupBarrelImportBlock(path: string): string {
const lines = readSource(path).split("\n");
const targetLineIndex = lines.findIndex((line) =>
/from\s*"[^"]*plugin-sdk(?:-internal)?\/setup(?:\.js)?";/.test(line),
);
if (targetLineIndex === -1) {
return "";
}
let startLineIndex = targetLineIndex;
while (startLineIndex >= 0 && !lines[startLineIndex].includes("import")) {
startLineIndex -= 1;
}
return lines.slice(startLineIndex, targetLineIndex + 1).join("\n");
}
function collectExtensionSourceFiles(): string[] {
const extensionsDir = normalizePath(resolve(ROOT_DIR, "..", "extensions"));
const sharedExtensionsDir = normalizePath(resolve(extensionsDir, "shared"));
extensionSourceFilesCache = collectSourceFiles(extensionSourceFilesCache, {
rootDir: resolve(ROOT_DIR, "..", "extensions"),
shouldSkipPath: (normalizedFullPath) =>
normalizedFullPath.includes(sharedExtensionsDir) ||
normalizedFullPath.includes(`${extensionsDir}/shared/`),
shouldSkipEntry: ({ entryName, normalizedFullPath }) =>
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".test-") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
normalizedFullPath.includes("test-support") ||
entryName === "api.ts" ||
entryName === "runtime-api.ts",
});
return extensionSourceFilesCache;
}
function collectCoreSourceFiles(): string[] {
const srcDir = resolve(ROOT_DIR, "..", "src");
const normalizedPluginSdkDir = normalizePath(resolve(ROOT_DIR, "plugin-sdk"));
coreSourceFilesCache = collectSourceFiles(coreSourceFilesCache, {
rootDir: srcDir,
shouldSkipEntry: ({ entryName, normalizedFullPath }) =>
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".test-utils.") ||
normalizedFullPath.includes(".test-harness.") ||
normalizedFullPath.includes(".test-helpers.") ||
entryName.endsWith("-test-helpers.ts") ||
entryName === "test-manager-helpers.ts" ||
normalizedFullPath.includes(".mock-harness.") ||
normalizedFullPath.includes(".suite.") ||
normalizedFullPath.includes(".spec.") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
// src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated
// plugin-sdk guardrails instead of the generic "core should not touch extensions" rule.
normalizedFullPath.includes(`${normalizedPluginSdkDir}/`),
});
return coreSourceFilesCache;
}
function collectExtensionFiles(extensionId: string): string[] {
const cached = extensionFilesCache.get(extensionId);
const files = collectSourceFiles(cached, {
rootDir: resolve(ROOT_DIR, "..", "extensions", extensionId),
shouldSkipEntry: ({ entryName, normalizedFullPath }) =>
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".test-") ||
normalizedFullPath.includes(".spec.") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
normalizedFullPath.includes("test-support") ||
entryName === "test-support.ts" ||
entryName === "runtime-api.ts",
});
extensionFilesCache.set(extensionId, files);
return files;
}
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,
] 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) {
specifiers.add(specifier);
}
}
}
return [...specifiers];
}
function collectImportSpecifiers(text: string): string[] {
return collectModuleSpecifiers(text);
}
function getSourceAnalysis(path: string): SourceAnalysis {
const fullPath = resolve(REPO_ROOT, path);
const cached = sourceAnalysisCache.get(fullPath);
if (cached) {
return cached;
}
const text = readSource(path);
const importSpecifiers = collectImportSpecifiers(text);
const analysis = {
text,
importSpecifiers,
extensionImports: importSpecifiers.filter((specifier) =>
specifier.includes(BUNDLED_PLUGIN_PATH_PREFIX),
),
} satisfies SourceAnalysis;
sourceAnalysisCache.set(fullPath, analysis);
return analysis;
}
function expectOnlyApprovedExtensionSeams(file: string, imports: string[]): void {
for (const specifier of imports) {
const normalized = specifier.replaceAll("\\", "/");
const resolved = specifier.startsWith(".")
? resolve(dirname(file), specifier).replaceAll("\\", "/")
: normalized;
const extensionId =
resolved.match(new RegExp(`${BUNDLED_PLUGIN_ROOT_DIR}/([^/]+)/`))?.[1] ?? null;
if (!extensionId || !GUARDED_CHANNEL_EXTENSIONS.has(extensionId)) {
continue;
}
const basename = resolved.split("/").at(-1) ?? "";
expect(
ALLOWED_EXTENSION_PUBLIC_SURFACES.has(basename),
`${file} should only import approved extension surfaces, got ${specifier}`,
).toBe(true);
}
}
function expectNoSiblingExtensionPrivateSrcImports(file: string, imports: string[]): void {
const normalizedFile = file.replaceAll("\\", "/");
const currentExtensionId =
normalizedFile.match(new RegExp(`/${BUNDLED_PLUGIN_ROOT_DIR}/([^/]+)/`))?.[1] ?? null;
if (!currentExtensionId) {
return;
}
for (const specifier of imports) {
if (!specifier.startsWith(".")) {
continue;
}
const resolvedImport = resolve(dirname(file), specifier).replaceAll("\\", "/");
const targetExtensionId = resolvedImport.match(/\/extensions\/([^/]+)\/src\//)?.[1] ?? null;
if (!targetExtensionId || targetExtensionId === currentExtensionId) {
continue;
}
expect.fail(`${file} should not import another extension's private src, got ${specifier}`);
}
}
function expectNoCrossPluginSdkFacadeImports(file: string, imports: string[]): void {
const normalizedFile = file.replaceAll("\\", "/");
const currentExtensionId =
normalizedFile.match(new RegExp(`/${BUNDLED_PLUGIN_ROOT_DIR}/([^/]+)/`))?.[1] ?? null;
if (!currentExtensionId) {
return;
}
for (const specifier of imports) {
if (!specifier.startsWith("openclaw/plugin-sdk/")) {
continue;
}
const targetSubpath = specifier.slice("openclaw/plugin-sdk/".length);
const targetExtensionId =
BUNDLED_EXTENSION_IDS.find(
(extensionId) =>
targetSubpath === extensionId || targetSubpath.startsWith(`${extensionId}-`),
) ?? null;
if (!targetExtensionId || targetExtensionId === currentExtensionId) {
continue;
}
expect.fail(
`${file} should not import another bundled plugin facade, got ${specifier}. Promote shared helpers to a neutral plugin-sdk subpath instead.`,
);
}
}
describe("channel import guardrails", () => {
it("keeps channel helper modules off their own SDK barrels", () => {
for (const source of SAME_CHANNEL_SDK_GUARDS) {
const text = readSource(source.path);
for (const pattern of source.forbiddenPatterns) {
expect(text, `${source.path} should not match ${pattern}`).not.toMatch(pattern);
}
}
});
it("keeps setup barrels limited to setup primitives", () => {
for (const source of SETUP_BARREL_GUARDS) {
const importBlock = readSetupBarrelImportBlock(source.path);
for (const pattern of source.forbiddenPatterns) {
expect(importBlock, `${source.path} setup import should not match ${pattern}`).not.toMatch(
pattern,
);
}
}
});
it("keeps channel config schemas off the broad core sdk barrel", () => {
for (const source of CHANNEL_CONFIG_SCHEMA_GUARDS) {
const text = readSource(source.path);
for (const pattern of source.forbiddenPatterns) {
expect(text, `${source.path} should not match ${pattern}`).not.toMatch(pattern);
}
}
});
it("keeps bundled extension source files off root and compat plugin-sdk imports", () => {
for (const file of collectExtensionSourceFiles()) {
const text = readSource(file);
expect(text, `${file} should not import openclaw/plugin-sdk root`).not.toMatch(
/["']openclaw\/plugin-sdk["']/,
);
expect(text, `${file} should not import openclaw/plugin-sdk/compat`).not.toMatch(
/["']openclaw\/plugin-sdk\/compat["']/,
);
}
});
it("keeps bundled extension source files off legacy core send-deps src imports", () => {
const legacyCoreSendDepsImport = /["'][^"']*src\/infra\/outbound\/send-deps\.[cm]?[jt]s["']/;
for (const file of collectExtensionSourceFiles()) {
const text = readSource(file);
expect(text, `${file} should not import src/infra/outbound/send-deps.*`).not.toMatch(
legacyCoreSendDepsImport,
);
}
});
it("keeps core production files off plugin-private src imports", () => {
for (const file of collectCoreSourceFiles()) {
const text = readSource(file);
expect(text, `${file} should not import plugin-private src paths`).not.toMatch(
/["'][^"']*extensions\/[^/"']+\/src\//,
);
}
});
it("keeps extension production files off other extensions' private src imports", () => {
for (const file of collectExtensionSourceFiles()) {
expectNoSiblingExtensionPrivateSrcImports(file, getSourceAnalysis(file).importSpecifiers);
}
});
it("keeps extension production files off other bundled plugin sdk facades", () => {
for (const file of collectExtensionSourceFiles()) {
expectNoCrossPluginSdkFacadeImports(file, getSourceAnalysis(file).importSpecifiers);
}
});
it("keeps core extension imports limited to approved public surfaces", () => {
for (const file of collectCoreSourceFiles()) {
expectOnlyApprovedExtensionSeams(file, getSourceAnalysis(file).extensionImports);
}
});
it("keeps extension-to-extension imports limited to approved public surfaces", () => {
for (const file of collectExtensionSourceFiles()) {
expectOnlyApprovedExtensionSeams(file, getSourceAnalysis(file).extensionImports);
}
});
it("keeps internalized extension helper surfaces behind local api barrels", () => {
for (const extensionId of LOCAL_EXTENSION_API_BARREL_GUARDS) {
for (const file of collectExtensionFiles(extensionId)) {
const normalized = file.replaceAll("\\", "/");
if (
LOCAL_EXTENSION_API_BARREL_EXCEPTIONS.some((suffix) => normalized.endsWith(suffix)) ||
normalized.endsWith("/api.ts") ||
normalized.endsWith("/test-runtime.ts") ||
normalized.includes(".test.") ||
normalized.includes(".spec.") ||
normalized.includes(".fixture.") ||
normalized.includes(".snap")
) {
continue;
}
const text = readSource(file);
expect(
text,
`${normalized} should import ${extensionId} helpers via the local api barrel`,
).not.toMatch(new RegExp(`["']openclaw/plugin-sdk/${extensionId}(?:["'/])`, "u"));
}
}
});
});

View File

@@ -1,66 +0,0 @@
import { readdirSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
BUNDLED_PLUGIN_ROOT_DIR,
bundledPluginFile,
} from "../../test/helpers/bundled-plugin-paths.js";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
const EXTENSIONS_DIR = resolve(REPO_ROOT, BUNDLED_PLUGIN_ROOT_DIR);
const CORE_PLUGIN_ENTRY_IMPORT_RE =
/import\s*\{[^}]*\bdefinePluginEntry\b[^}]*\}\s*from\s*"openclaw\/plugin-sdk\/core"/;
const RUNTIME_ENTRY_HELPER_RE = /(^|\/)plugin-entry\.runtime\.[cm]?[jt]s$/;
describe("plugin entry guardrails", () => {
it("keeps bundled extension entry modules off direct definePluginEntry imports from core", () => {
const failures: string[] = [];
for (const entry of readdirSync(EXTENSIONS_DIR, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const indexPath = resolve(EXTENSIONS_DIR, entry.name, "index.ts");
try {
const source = readFileSync(indexPath, "utf8");
if (CORE_PLUGIN_ENTRY_IMPORT_RE.test(source)) {
failures.push(bundledPluginFile(entry.name, "index.ts"));
}
} catch {
// Skip extensions without index.ts entry modules.
}
}
expect(failures).toEqual([]);
});
it("does not advertise runtime helper sidecars as bundled plugin entry extensions", () => {
const failures: string[] = [];
for (const entry of readdirSync(EXTENSIONS_DIR, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const packageJsonPath = resolve(EXTENSIONS_DIR, entry.name, "package.json");
try {
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
openclaw?: { extensions?: unknown };
};
const extensions = Array.isArray(pkg.openclaw?.extensions) ? pkg.openclaw.extensions : [];
if (
extensions.some(
(candidate) => typeof candidate === "string" && RUNTIME_ENTRY_HELPER_RE.test(candidate),
)
) {
failures.push(bundledPluginFile(entry.name, "package.json"));
}
} catch {
// Skip directories without package metadata.
}
}
expect(failures).toEqual([]);
});
});

View File

@@ -1,193 +0,0 @@
import { readdirSync, readFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import { describe, expect, it } from "vitest";
import { bundledPluginFile } from "../../test/helpers/bundled-plugin-paths.js";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
[bundledPluginFile("discord", "runtime-api.ts")]: [
'export * from "./src/audit.js";',
'export * from "./src/actions/runtime.js";',
'export * from "./src/actions/runtime.moderation-shared.js";',
'export * from "./src/actions/runtime.shared.js";',
'export * from "./src/channel-actions.js";',
'export * from "./src/directory-live.js";',
'export * from "./src/monitor.js";',
'export * from "./src/monitor/gateway-plugin.js";',
'export * from "./src/monitor/gateway-registry.js";',
'export * from "./src/monitor/presence-cache.js";',
'export * from "./src/monitor/thread-bindings.js";',
'export * from "./src/monitor/thread-bindings.manager.js";',
'export * from "./src/monitor/timeouts.js";',
'export * from "./src/probe.js";',
'export * from "./src/resolve-channels.js";',
'export * from "./src/resolve-users.js";',
'export * from "./src/outbound-session-route.js";',
'export * from "./src/send.js";',
],
[bundledPluginFile("imessage", "runtime-api.ts")]: [
'export { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, getChatChannelMeta, type ChannelPlugin, type OpenClawConfig } from "openclaw/plugin-sdk/core";',
'export { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";',
'export { buildComputedAccountStatusSnapshot, collectStatusIssuesFromLastError } from "openclaw/plugin-sdk/status-helpers";',
'export { formatTrimmedAllowFromEntries, resolveIMessageConfigAllowFrom, resolveIMessageConfigDefaultTo } from "openclaw/plugin-sdk/channel-config-helpers";',
'export { looksLikeIMessageTargetId, normalizeIMessageMessagingTarget } from "./src/normalize.js";',
'export { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/media-runtime";',
'export { IMessageConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";',
'export { resolveIMessageGroupRequireMention, resolveIMessageGroupToolPolicy } from "./src/group-policy.js";',
'export { monitorIMessageProvider } from "./src/monitor.js";',
'export type { MonitorIMessageOpts } from "./src/monitor.js";',
'export { probeIMessage } from "./src/probe.js";',
'export type { IMessageProbe } from "./src/probe.js";',
'export { sendMessageIMessage } from "./src/send.js";',
'export type IMessageAccountConfig = Omit< NonNullable<NonNullable<RuntimeApiOpenClawConfig["channels"]>["imessage"]>, "accounts" | "defaultAccount" >;',
'export function chunkTextForOutbound(text: string, limit: number): string[] { const chunks: string[] = []; let remaining = text; while (remaining.length > limit) { const window = remaining.slice(0, limit); const splitAt = Math.max(window.lastIndexOf("\\n"), window.lastIndexOf(" ")); const breakAt = splitAt > 0 ? splitAt : limit; chunks.push(remaining.slice(0, breakAt).trimEnd()); remaining = remaining.slice(breakAt).trimStart(); } if (remaining.length > 0 || text.length === 0) { chunks.push(remaining); } return chunks; }',
],
[bundledPluginFile("googlechat", "runtime-api.ts")]: [
'export * from "openclaw/plugin-sdk/googlechat";',
],
[bundledPluginFile("matrix", "runtime-api.ts")]: [
'export * from "./src/auth-precedence.js";',
'export { requiresExplicitMatrixDefaultAccount, resolveMatrixDefaultOrOnlyAccountId } from "./src/account-selection.js";',
'export * from "./src/account-selection.js";',
'export * from "./src/env-vars.js";',
'export * from "./src/storage-paths.js";',
'export { assertHttpUrlTargetsPrivateNetwork, closeDispatcher, createPinnedDispatcher, resolvePinnedHostnameWithPolicy, ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";',
'export { setMatrixThreadBindingIdleTimeoutBySessionKey, setMatrixThreadBindingMaxAgeBySessionKey } from "./src/matrix/thread-bindings-shared.js";',
'export { setMatrixRuntime } from "./src/runtime.js";',
'export { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";',
'export type { ChannelDirectoryEntry, ChannelMessageActionContext, OpenClawConfig, PluginRuntime, RuntimeLogger, RuntimeEnv, WizardPrompter } from "openclaw/plugin-sdk/matrix-runtime-shared";',
'export { formatZonedTimestamp } from "openclaw/plugin-sdk/matrix-runtime-shared";',
'export function chunkTextForOutbound(text: string, limit: number): string[] { const chunks: string[] = []; let remaining = text; while (remaining.length > limit) { const window = remaining.slice(0, limit); const splitAt = Math.max(window.lastIndexOf("\\n"), window.lastIndexOf(" ")); const breakAt = splitAt > 0 ? splitAt : limit; chunks.push(remaining.slice(0, breakAt).trimEnd()); remaining = remaining.slice(breakAt).trimStart(); } if (remaining.length > 0 || text.length === 0) { chunks.push(remaining); } return chunks; }',
],
[bundledPluginFile("nextcloud-talk", "runtime-api.ts")]: [
'export * from "openclaw/plugin-sdk/nextcloud-talk";',
],
[bundledPluginFile("signal", "runtime-api.ts")]: ['export * from "./src/runtime-api.js";'],
[bundledPluginFile("slack", "runtime-api.ts")]: [
'export * from "./src/action-runtime.js";',
'export * from "./src/directory-live.js";',
'export * from "./src/index.js";',
'export * from "./src/resolve-channels.js";',
'export * from "./src/resolve-users.js";',
],
[bundledPluginFile("telegram", "runtime-api.ts")]: [
'export type { ChannelMessageActionAdapter, ChannelPlugin, OpenClawConfig, OpenClawPluginApi, PluginRuntime, TelegramAccountConfig, TelegramActionConfig, TelegramNetworkConfig } from "openclaw/plugin-sdk/telegram-core";',
'export type { TelegramApiOverride } from "./src/send.js";',
'export type { OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger } from "openclaw/plugin-sdk/core";',
'export type { AcpRuntime, AcpRuntimeCapabilities, AcpRuntimeDoctorReport, AcpRuntimeEnsureInput, AcpRuntimeEvent, AcpRuntimeHandle, AcpRuntimeStatus, AcpRuntimeTurnInput, AcpRuntimeErrorCode, AcpSessionUpdateTag } from "openclaw/plugin-sdk/acp-runtime";',
'export { AcpRuntimeError } from "openclaw/plugin-sdk/acp-runtime";',
'export { buildTokenChannelStatusSummary, clearAccountEntryFields, DEFAULT_ACCOUNT_ID, normalizeAccountId, PAIRING_APPROVED_MESSAGE, parseTelegramTopicConversation, projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses, resolveTelegramPollVisibility } from "openclaw/plugin-sdk/telegram-core";',
'export { buildChannelConfigSchema, getChatChannelMeta, jsonResult, readNumberParam, readReactionParams, readStringArrayParam, readStringOrNumberParam, readStringParam, resolvePollMaxSelections, TelegramConfigSchema } from "openclaw/plugin-sdk/telegram-core";',
'export type { TelegramProbe } from "./src/probe.js";',
'export { auditTelegramGroupMembership, collectTelegramUnmentionedGroupIds } from "./src/audit.js";',
'export { resolveTelegramRuntimeGroupPolicy } from "./src/group-access.js";',
'export { buildTelegramExecApprovalPendingPayload, shouldSuppressTelegramExecApprovalForwardingFallback } from "./src/exec-approval-forwarding.js";',
'export { telegramMessageActions } from "./src/channel-actions.js";',
'export { monitorTelegramProvider } from "./src/monitor.js";',
'export { probeTelegram } from "./src/probe.js";',
'export { resolveTelegramFetch, resolveTelegramTransport, shouldRetryTelegramTransportFallback } from "./src/fetch.js";',
'export { makeProxyFetch } from "./src/proxy.js";',
'export { createForumTopicTelegram, deleteMessageTelegram, editForumTopicTelegram, editMessageReplyMarkupTelegram, editMessageTelegram, pinMessageTelegram, reactMessageTelegram, renameForumTopicTelegram, sendMessageTelegram, sendPollTelegram, sendStickerTelegram, sendTypingTelegram, unpinMessageTelegram } from "./src/send.js";',
'export { createTelegramThreadBindingManager, getTelegramThreadBindingManager, resetTelegramThreadBindingsForTests, setTelegramThreadBindingIdleTimeoutBySessionKey, setTelegramThreadBindingMaxAgeBySessionKey } from "./src/thread-bindings.js";',
'export { resolveTelegramToken } from "./src/token.js";',
],
[bundledPluginFile("whatsapp", "runtime-api.ts")]: [
'export * from "./src/active-listener.js";',
'export * from "./src/action-runtime.js";',
'export * from "./src/agent-tools-login.js";',
'export * from "./src/auth-store.js";',
'export * from "./src/auto-reply.js";',
'export * from "./src/inbound.js";',
'export * from "./src/login.js";',
'export * from "./src/media.js";',
'export * from "./src/send.js";',
'export * from "./src/session.js";',
"export async function startWebLoginWithQr( ...args: Parameters<StartWebLoginWithQr> ): ReturnType<StartWebLoginWithQr> { const { startWebLoginWithQr } = await loadLoginQrModule(); return await startWebLoginWithQr(...args); }",
"export async function waitForWebLogin( ...args: Parameters<WaitForWebLogin> ): ReturnType<WaitForWebLogin> { const { waitForWebLogin } = await loadLoginQrModule(); return await waitForWebLogin(...args); }",
],
} as const;
function collectRuntimeApiFiles(): string[] {
const extensionsDir = resolve(ROOT_DIR, "..", "extensions");
const files: string[] = [];
const stack = [extensionsDir];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
for (const entry of readdirSync(current, { withFileTypes: true })) {
const fullPath = resolve(current, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
continue;
}
stack.push(fullPath);
continue;
}
if (!entry.isFile() || entry.name !== "runtime-api.ts") {
continue;
}
files.push(relative(resolve(ROOT_DIR, ".."), fullPath).replaceAll("\\", "/"));
}
}
return files;
}
function readExportStatements(path: string): string[] {
const sourceText = readFileSync(resolve(ROOT_DIR, "..", path), "utf8");
const sourceFile = ts.createSourceFile(path, sourceText, ts.ScriptTarget.Latest, true);
return sourceFile.statements.flatMap((statement) => {
if (!ts.isExportDeclaration(statement)) {
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
if (!modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
return [];
}
return [statement.getText(sourceFile).replaceAll(/\s+/g, " ").trim()];
}
const moduleSpecifier = statement.moduleSpecifier;
if (!moduleSpecifier || !ts.isStringLiteral(moduleSpecifier)) {
return [statement.getText(sourceFile).replaceAll(/\s+/g, " ").trim()];
}
if (!statement.exportClause) {
const prefix = statement.isTypeOnly ? "export type *" : "export *";
return [`${prefix} from ${moduleSpecifier.getText(sourceFile)};`];
}
if (!ts.isNamedExports(statement.exportClause)) {
return [statement.getText(sourceFile).replaceAll(/\s+/g, " ").trim()];
}
const specifiers = statement.exportClause.elements.map((element) => {
const imported = element.propertyName?.text;
const exported = element.name.text;
const alias = imported ? `${imported} as ${exported}` : exported;
return element.isTypeOnly ? `type ${alias}` : alias;
});
const exportPrefix = statement.isTypeOnly ? "export type" : "export";
return [
`${exportPrefix} { ${specifiers.join(", ")} } from ${moduleSpecifier.getText(sourceFile)};`,
];
});
}
describe("runtime api guardrails", () => {
it("keeps runtime api surfaces on an explicit export allowlist", () => {
const runtimeApiFiles = collectRuntimeApiFiles();
expect(runtimeApiFiles).toEqual(
expect.arrayContaining(Object.keys(RUNTIME_API_EXPORT_GUARDS).toSorted()),
);
for (const file of Object.keys(RUNTIME_API_EXPORT_GUARDS).toSorted()) {
expect(readExportStatements(file), `${file} runtime api exports changed`).toEqual(
RUNTIME_API_EXPORT_GUARDS[file],
);
}
});
});

View File

@@ -1,812 +0,0 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type {
BaseProbeResult as ContractBaseProbeResult,
BaseTokenResolution as ContractBaseTokenResolution,
ChannelAgentTool as ContractChannelAgentTool,
ChannelAccountSnapshot as ContractChannelAccountSnapshot,
ChannelGroupContext as ContractChannelGroupContext,
ChannelMessageActionAdapter as ContractChannelMessageActionAdapter,
ChannelMessageActionContext as ContractChannelMessageActionContext,
ChannelMessageActionName as ContractChannelMessageActionName,
ChannelMessageToolDiscovery as ContractChannelMessageToolDiscovery,
ChannelStatusIssue as ContractChannelStatusIssue,
ChannelThreadingContext as ContractChannelThreadingContext,
ChannelThreadingToolContext as ContractChannelThreadingToolContext,
} from "openclaw/plugin-sdk/channel-contract";
import type {
ChannelMessageActionContext as CoreChannelMessageActionContext,
OpenClawPluginApi as CoreOpenClawPluginApi,
PluginRuntime as CorePluginRuntime,
} from "openclaw/plugin-sdk/core";
import * as providerEntrySdk from "openclaw/plugin-sdk/provider-entry";
import { describe, expect, expectTypeOf, it } from "vitest";
import type { ChannelMessageActionContext } from "../channels/plugins/types.js";
import type {
BaseProbeResult,
BaseTokenResolution,
ChannelAgentTool,
ChannelAccountSnapshot,
ChannelGroupContext,
ChannelMessageActionAdapter,
ChannelMessageActionName,
ChannelMessageToolDiscovery,
ChannelStatusIssue,
ChannelThreadingContext,
ChannelThreadingToolContext,
} from "../channels/plugins/types.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import type { OpenClawPluginApi } from "../plugins/types.js";
import type {
ChannelMessageActionContext as SharedChannelMessageActionContext,
OpenClawPluginApi as SharedOpenClawPluginApi,
PluginRuntime as SharedPluginRuntime,
} from "./channel-plugin-common.js";
import { pluginSdkSubpaths } from "./entrypoints.js";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const PLUGIN_SDK_DIR = resolve(ROOT_DIR, "plugin-sdk");
const sourceCache = new Map<string, string>();
const representativeRuntimeSmokeSubpaths = ["channel-runtime", "conversation-runtime"] as const;
const importResolvedPluginSdkSubpath = async (specifier: string) => import(specifier);
function readPluginSdkSource(subpath: string): string {
const file = resolve(PLUGIN_SDK_DIR, `${subpath}.ts`);
const cached = sourceCache.get(file);
if (cached !== undefined) {
return cached;
}
const text = readFileSync(file, "utf8");
sourceCache.set(file, text);
return text;
}
function isIdentifierCode(code: number): boolean {
return (
(code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
code === 36 ||
code === 95
);
}
function sourceMentionsIdentifier(source: string, name: string): boolean {
let fromIndex = 0;
while (true) {
const matchIndex = source.indexOf(name, fromIndex);
if (matchIndex === -1) {
return false;
}
const beforeCode = matchIndex === 0 ? -1 : source.charCodeAt(matchIndex - 1);
const afterIndex = matchIndex + name.length;
const afterCode = afterIndex >= source.length ? -1 : source.charCodeAt(afterIndex);
if (!isIdentifierCode(beforeCode) && !isIdentifierCode(afterCode)) {
return true;
}
fromIndex = matchIndex + 1;
}
}
function expectSourceMentions(subpath: string, names: readonly string[]) {
const source = readPluginSdkSource(subpath);
const missing = names.filter((name) => !sourceMentionsIdentifier(source, name));
expect(missing, `${subpath} missing exports`).toEqual([]);
}
function expectSourceOmits(subpath: string, names: readonly string[]) {
const source = readPluginSdkSource(subpath);
const present = names.filter((name) => sourceMentionsIdentifier(source, name));
expect(present, `${subpath} leaked exports`).toEqual([]);
}
function expectSourceContract(
subpath: string,
params: { mentions?: readonly string[]; omits?: readonly string[] },
) {
const source = readPluginSdkSource(subpath);
const missing = (params.mentions ?? []).filter((name) => !sourceMentionsIdentifier(source, name));
const present = (params.omits ?? []).filter((name) => sourceMentionsIdentifier(source, name));
expect(missing, `${subpath} missing exports`).toEqual([]);
expect(present, `${subpath} leaked exports`).toEqual([]);
}
function expectSourceContains(subpath: string, snippet: string) {
expect(readPluginSdkSource(subpath)).toContain(snippet);
}
function expectSourceOmitsSnippet(subpath: string, snippet: string) {
expect(readPluginSdkSource(subpath)).not.toContain(snippet);
}
function expectSourceOmitsImportPattern(subpath: string, specifier: string) {
const escapedSpecifier = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const source = readPluginSdkSource(subpath);
expect(source).not.toMatch(new RegExp(`\\bfrom\\s+["']${escapedSpecifier}["']`, "u"));
expect(source).not.toMatch(new RegExp(`\\bimport\\(\\s*["']${escapedSpecifier}["']\\s*\\)`, "u"));
}
describe("plugin-sdk subpath exports", () => {
it("keeps the curated public list free of internal implementation subpaths", () => {
for (const deniedSubpath of [
"acpx",
"device-pair",
"lobster",
"pairing-access",
"provider-model-definitions",
"reply-prefix",
"secret-input-runtime",
"secret-input-schema",
"signal-core",
"synology-chat",
"typing",
"whatsapp",
"whatsapp-action-runtime",
"whatsapp-login-qr",
"zai",
]) {
expect(pluginSdkSubpaths).not.toContain(deniedSubpath);
}
});
it("keeps helper subpaths aligned", () => {
expectSourceMentions("core", [
"emptyPluginConfigSchema",
"definePluginEntry",
"defineChannelPluginEntry",
"defineSetupPluginEntry",
"createChatChannelPlugin",
"createChannelPluginBase",
"isSecretRef",
"optionalStringEnum",
]);
expectSourceOmits("core", [
"runPassiveAccountLifecycle",
"createLoggerBackedRuntime",
"registerSandboxBackend",
]);
expectSourceContract("routing", {
mentions: [
"buildAgentSessionKey",
"resolveThreadSessionKeys",
"normalizeMessageChannel",
"resolveGatewayMessageChannel",
],
});
expectSourceMentions("reply-payload", [
"buildMediaPayload",
"deliverTextOrMediaReply",
"resolveOutboundMediaUrls",
"resolvePayloadMediaUrls",
"sendPayloadMediaSequenceAndFinalize",
"sendPayloadMediaSequenceOrFallback",
"sendTextMediaPayload",
"sendPayloadWithChunkedTextAndMedia",
]);
expectSourceMentions("media-runtime", [
"createDirectTextMediaOutbound",
"createScopedChannelMediaMaxBytesResolver",
]);
expectSourceMentions("telegram-core", [
"ChannelMessageActionAdapter",
"TelegramAccountConfig",
"buildChannelConfigSchema",
"buildTokenChannelStatusSummary",
"resolveConfiguredFromCredentialStatuses",
]);
expectSourceMentions("bluebubbles", [
"normalizeBlueBubblesAcpConversationId",
"matchBlueBubblesAcpConversation",
"resolveBlueBubblesConversationIdFromTarget",
"resolveAckReaction",
"resolveChannelMediaMaxBytes",
"collectBlueBubblesStatusIssues",
"createChannelPairingController",
"createChannelReplyPipeline",
"resolveRequestUrl",
"buildProbeChannelStatusSummary",
"extractToolSend",
"createFixedWindowRateLimiter",
"withResolvedWebhookRequestPipeline",
]);
expectSourceMentions("irc", [
"createChannelReplyPipeline",
"chunkTextForOutbound",
"createChannelPairingController",
"createLoggerBackedRuntime",
"ircSetupAdapter",
"ircSetupWizard",
]);
expectSourceMentions("bluebubbles-policy", [
"isAllowedBlueBubblesSender",
"resolveBlueBubblesGroupRequireMention",
"resolveBlueBubblesGroupToolPolicy",
]);
for (const subpath of [
"feishu",
"googlechat",
"matrix",
"mattermost",
"msteams",
"zalo",
"zalouser",
]) {
expectSourceMentions(subpath, ["chunkTextForOutbound"]);
}
expectSourceMentions("signal", ["chunkText"]);
expectSourceMentions("reply-history", [
"buildPendingHistoryContextFromMap",
"clearHistoryEntriesIfEnabled",
"recordPendingHistoryEntryIfEnabled",
]);
expectSourceContract("reply-runtime", {
omits: [
"buildPendingHistoryContextFromMap",
"clearHistoryEntriesIfEnabled",
"recordPendingHistoryEntryIfEnabled",
"DEFAULT_GROUP_HISTORY_LIMIT",
],
});
expectSourceMentions("account-helpers", ["createAccountListHelpers"]);
expectSourceMentions("channel-actions", ["optionalStringEnum", "stringEnum"]);
expectSourceMentions("compat", [
"createPluginRuntimeStore",
"createScopedChannelConfigAdapter",
"resolveControlCommandGate",
"delegateCompactionToRuntime",
]);
expectSourceMentions("device-bootstrap", [
"approveDevicePairing",
"issueDeviceBootstrapToken",
"listDevicePairing",
]);
expectSourceMentions("allowlist-config-edit", [
"buildDmGroupAccountAllowlistAdapter",
"createNestedAllowlistOverrideResolver",
]);
expectSourceContract("allow-from", {
mentions: [
"addAllowlistUserEntriesFromConfigEntry",
"buildAllowlistResolutionSummary",
"canonicalizeAllowlistWithResolvedIds",
"mapAllowlistResolutionInputs",
"mergeAllowlist",
"patchAllowlistUsersInConfigEntries",
"summarizeMapping",
"compileAllowlist",
"firstDefined",
"formatAllowlistMatchMeta",
"isSenderIdAllowed",
"mergeDmAllowFromSources",
"resolveAllowlistMatchSimple",
],
});
expectSourceMentions("runtime", ["createLoggerBackedRuntime"]);
expectSourceMentions("discord", [
"buildDiscordComponentMessage",
"editDiscordComponentMessage",
"registerBuiltDiscordComponentMessage",
"resolveDiscordAccount",
]);
expectSourceMentions("huggingface", [
"buildHuggingfaceModelDefinition",
"buildHuggingfaceProvider",
"discoverHuggingfaceModels",
"HUGGINGFACE_MODEL_CATALOG",
"isHuggingfacePolicyLocked",
]);
expectSourceMentions("conversation-runtime", [
"recordInboundSession",
"recordInboundSessionMetaSafe",
"resolveConversationLabel",
]);
expectSourceMentions("directory-runtime", [
"createChannelDirectoryAdapter",
"createRuntimeDirectoryLiveAdapter",
"listDirectoryEntriesFromSources",
"listResolvedDirectoryEntriesFromSources",
]);
expectSourceContains(
"memory-core-host-runtime-core",
'export * from "../../packages/memory-host-sdk/src/runtime-core.js";',
);
expectSourceContains(
"memory-core-host-runtime-cli",
'export * from "../../packages/memory-host-sdk/src/runtime-cli.js";',
);
expectSourceContains(
"memory-core-host-runtime-files",
'export * from "../../packages/memory-host-sdk/src/runtime-files.js";',
);
});
it("exports channel runtime helpers from the dedicated subpath", () => {
expectSourceOmits("channel-runtime", [
"applyChannelMatchMeta",
"createChannelDirectoryAdapter",
"createEmptyChannelDirectoryAdapter",
"createArmableStallWatchdog",
"createDraftStreamLoop",
"createLoggedPairingApprovalNotifier",
"createPairingPrefixStripper",
"createRunStateMachine",
"createRuntimeDirectoryLiveAdapter",
"createRuntimeOutboundDelegates",
"createStatusReactionController",
"createTextPairingAdapter",
"createFinalizableDraftLifecycle",
"DEFAULT_EMOJIS",
"logAckFailure",
"logTypingFailure",
"logInboundDrop",
"normalizeMessageChannel",
"removeAckReactionAfterReply",
"recordInboundSession",
"recordInboundSessionMetaSafe",
"resolveInboundSessionEnvelopeContext",
"resolveMentionGating",
"resolveMentionGatingWithBypass",
"resolveOutboundSendDep",
"resolveConversationLabel",
"shouldDebounceTextInbound",
"shouldAckReaction",
"shouldAckReactionForWhatsApp",
"toLocationContext",
"resolveThreadBindingConversationIdFromBindingId",
"resolveThreadBindingEffectiveExpiresAt",
"resolveThreadBindingFarewellText",
"resolveThreadBindingIdleTimeoutMs",
"resolveThreadBindingIdleTimeoutMsForChannel",
"resolveThreadBindingIntroText",
"resolveThreadBindingLifecycle",
"resolveThreadBindingMaxAgeMs",
"resolveThreadBindingMaxAgeMsForChannel",
"resolveThreadBindingSpawnPolicy",
"resolveThreadBindingThreadName",
"resolveThreadBindingsEnabled",
"formatThreadBindingDisabledError",
"DISCORD_THREAD_BINDING_CHANNEL",
"MATRIX_THREAD_BINDING_CHANNEL",
"resolveControlCommandGate",
"resolveCommandAuthorizedFromAuthorizers",
"resolveDualTextControlCommandGate",
"resolveNativeCommandSessionTargets",
"attachChannelToResult",
"buildComputedAccountStatusSnapshot",
"buildMediaPayload",
"createActionGate",
"jsonResult",
"normalizeInteractiveReply",
"PAIRING_APPROVED_MESSAGE",
"projectCredentialSnapshotFields",
"readStringParam",
"compileAllowlist",
"formatAllowlistMatchMeta",
"firstDefined",
"isSenderIdAllowed",
"mergeDmAllowFromSources",
"addAllowlistUserEntriesFromConfigEntry",
"buildAllowlistResolutionSummary",
"canonicalizeAllowlistWithResolvedIds",
"mergeAllowlist",
"patchAllowlistUsersInConfigEntries",
"resolvePayloadMediaUrls",
"resolveScopedChannelMediaMaxBytes",
"sendPayloadMediaSequenceAndFinalize",
"sendPayloadMediaSequenceOrFallback",
"sendTextMediaPayload",
"createScopedChannelMediaMaxBytesResolver",
"runPassiveAccountLifecycle",
"buildChannelKeyCandidates",
"buildMessagingTarget",
"createDirectTextMediaOutbound",
"createMessageToolButtonsSchema",
"createMessageToolCardSchema",
"createScopedAccountReplyToModeResolver",
"createStaticReplyToModeResolver",
"createTopLevelChannelReplyToModeResolver",
"createUnionActionGate",
"ensureTargetId",
"listTokenSourcedAccounts",
"parseMentionPrefixOrAtUserTarget",
"requireTargetKind",
"resolveChannelEntryMatchWithFallback",
"resolveChannelMatchConfig",
"resolveReactionMessageId",
"resolveTargetsWithOptionalToken",
"appendMatchMetadata",
"asString",
"collectIssuesForEnabledAccounts",
"isRecord",
"resolveEnabledConfiguredAccountId",
]);
expectSourceMentions("channel-inbound", [
"buildMentionRegexes",
"createDirectDmPreCryptoGuardPolicy",
"createChannelInboundDebouncer",
"createInboundDebouncer",
"dispatchInboundDirectDmWithRuntime",
"formatInboundEnvelope",
"formatInboundFromLabel",
"formatLocationText",
"logInboundDrop",
"matchesMentionPatterns",
"matchesMentionWithExplicit",
"normalizeMentionText",
"resolveInboundDebounceMs",
"resolveEnvelopeFormatOptions",
"resolveInboundSessionEnvelopeContext",
"resolveMentionGating",
"resolveMentionGatingWithBypass",
"shouldDebounceTextInbound",
"toLocationContext",
]);
expectSourceContract("reply-runtime", {
omits: [
"buildMentionRegexes",
"formatInboundEnvelope",
"formatInboundFromLabel",
"matchesMentionPatterns",
"matchesMentionWithExplicit",
"normalizeMentionText",
"resolveEnvelopeFormatOptions",
"hasControlCommand",
"buildCommandTextFromArgs",
"buildCommandsPaginationKeyboard",
"buildModelsProviderData",
"listNativeCommandSpecsForConfig",
"listSkillCommandsForAgents",
"normalizeCommandBody",
"resolveCommandAuthorization",
"resolveStoredModelOverride",
"shouldComputeCommandAuthorized",
"shouldHandleTextCommands",
],
});
expectSourceMentions("channel-setup", [
"createOptionalChannelSetupSurface",
"createTopLevelChannelDmPolicy",
]);
expectSourceContract("channel-actions", {
mentions: [
"createUnionActionGate",
"listTokenSourcedAccounts",
"resolveReactionMessageId",
"createMessageToolButtonsSchema",
"createMessageToolCardSchema",
],
});
expectSourceMentions("channel-targets", [
"applyChannelMatchMeta",
"buildChannelKeyCandidates",
"buildMessagingTarget",
"createAllowedChatSenderMatcher",
"ensureTargetId",
"parseChatAllowTargetPrefixes",
"parseMentionPrefixOrAtUserTarget",
"parseChatTargetPrefixesOrThrow",
"requireTargetKind",
"resolveChannelEntryMatchWithFallback",
"resolveChannelMatchConfig",
"resolveServicePrefixedAllowTarget",
"resolveServicePrefixedChatTarget",
"resolveServicePrefixedOrChatAllowTarget",
"resolveServicePrefixedTarget",
"resolveTargetsWithOptionalToken",
]);
expectSourceMentions("channel-config-writes", [
"authorizeConfigWrite",
"canBypassConfigWritePolicy",
"formatConfigWriteDeniedMessage",
"resolveChannelConfigWrites",
]);
expectSourceMentions("channel-feedback", [
"createStatusReactionController",
"logAckFailure",
"logTypingFailure",
"removeAckReactionAfterReply",
"shouldAckReaction",
"shouldAckReactionForWhatsApp",
"DEFAULT_EMOJIS",
]);
expectSourceMentions("status-helpers", [
"appendMatchMetadata",
"asString",
"collectIssuesForEnabledAccounts",
"isRecord",
"resolveEnabledConfiguredAccountId",
]);
expectSourceMentions("outbound-runtime", [
"createRuntimeOutboundDelegates",
"resolveOutboundSendDep",
"resolveAgentOutboundIdentity",
]);
expectSourceMentions("command-auth", [
"buildCommandTextFromArgs",
"buildCommandsPaginationKeyboard",
"buildModelsProviderData",
"hasControlCommand",
"listNativeCommandSpecsForConfig",
"listSkillCommandsForAgents",
"normalizeCommandBody",
"createPreCryptoDirectDmAuthorizer",
"resolveCommandAuthorization",
"resolveCommandAuthorizedFromAuthorizers",
"resolveInboundDirectDmAccessWithRuntime",
"resolveControlCommandGate",
"resolveDualTextControlCommandGate",
"resolveNativeCommandSessionTargets",
"resolveStoredModelOverride",
"shouldComputeCommandAuthorized",
"shouldHandleTextCommands",
]);
expectSourceMentions("channel-send-result", [
"attachChannelToResult",
"buildChannelSendResult",
]);
expectSourceMentions("direct-dm", [
"createDirectDmPreCryptoGuardPolicy",
"createPreCryptoDirectDmAuthorizer",
"dispatchInboundDirectDmWithRuntime",
"resolveInboundDirectDmAccessWithRuntime",
]);
expectSourceMentions("conversation-runtime", [
"DISCORD_THREAD_BINDING_CHANNEL",
"MATRIX_THREAD_BINDING_CHANNEL",
"formatThreadBindingDisabledError",
"resolveThreadBindingFarewellText",
"resolveThreadBindingConversationIdFromBindingId",
"resolveThreadBindingEffectiveExpiresAt",
"resolveThreadBindingIdleTimeoutMs",
"resolveThreadBindingIdleTimeoutMsForChannel",
"resolveThreadBindingIntroText",
"resolveThreadBindingLifecycle",
"resolveThreadBindingMaxAgeMs",
"resolveThreadBindingMaxAgeMsForChannel",
"resolveThreadBindingSpawnPolicy",
"resolveThreadBindingThreadName",
"resolveThreadBindingsEnabled",
"formatThreadBindingDurationLabel",
"createScopedAccountReplyToModeResolver",
"createStaticReplyToModeResolver",
"createTopLevelChannelReplyToModeResolver",
]);
expectSourceMentions("thread-bindings-runtime", [
"resolveThreadBindingFarewellText",
"resolveThreadBindingLifecycle",
"registerSessionBindingAdapter",
"unregisterSessionBindingAdapter",
"SessionBindingAdapter",
]);
expectSourceMentions("matrix-runtime-shared", ["formatZonedTimestamp"]);
expectSourceMentions("ssrf-runtime", [
"closeDispatcher",
"createPinnedDispatcher",
"resolvePinnedHostnameWithPolicy",
"formatErrorMessage",
"assertHttpUrlTargetsPrivateNetwork",
"ssrfPolicyFromAllowPrivateNetwork",
]);
expectSourceContract("provider-setup", {
mentions: [
"applyProviderDefaultModel",
"discoverOpenAICompatibleLocalModels",
"discoverOpenAICompatibleSelfHostedProvider",
],
omits: [
"buildOllamaProvider",
"configureOllamaNonInteractive",
"ensureOllamaModelPulled",
"promptAndConfigureOllama",
"promptAndConfigureVllm",
"buildVllmProvider",
"buildSglangProvider",
"OLLAMA_DEFAULT_BASE_URL",
"OLLAMA_DEFAULT_MODEL",
"VLLM_DEFAULT_BASE_URL",
],
});
expectSourceOmitsSnippet("provider-setup", "./ollama-surface.js");
expectSourceOmitsImportPattern("provider-setup", "./vllm.js");
expectSourceOmitsImportPattern("provider-setup", "./sglang.js");
expectSourceMentions("provider-auth", [
"buildOauthProviderAuthResult",
"generatePkceVerifierChallenge",
"readClaudeCliCredentialsCached",
"toFormUrlEncoded",
]);
expectSourceOmits("core", ["buildOauthProviderAuthResult"]);
expectSourceContract("provider-model-shared", {
mentions: ["DEFAULT_CONTEXT_TOKENS", "normalizeModelCompat", "cloneFirstTemplateModel"],
omits: ["applyOpenAIConfig", "buildKilocodeModelDefinition", "discoverHuggingfaceModels"],
});
expectSourceContract("provider-catalog-shared", {
mentions: ["buildSingleProviderApiKeyCatalog", "buildPairedProviderApiKeyCatalog"],
omits: ["buildDeepSeekProvider", "buildOpenAICodexProvider", "buildVeniceProvider"],
});
expectSourceMentions("setup", [
"DEFAULT_ACCOUNT_ID",
"createAllowFromSection",
"createDelegatedSetupWizardProxy",
"createTopLevelChannelDmPolicy",
"mergeAllowFromEntries",
]);
expectSourceMentions("setup-tools", [
"formatCliCommand",
"detectBinary",
"installSignalCli",
"formatDocsLink",
]);
expectSourceMentions("lazy-runtime", ["createLazyRuntimeSurface", "createLazyRuntimeModule"]);
expectSourceContract("self-hosted-provider-setup", {
mentions: [
"applyProviderDefaultModel",
"discoverOpenAICompatibleLocalModels",
"discoverOpenAICompatibleSelfHostedProvider",
"configureOpenAICompatibleSelfHostedProviderNonInteractive",
],
omits: ["buildVllmProvider", "buildSglangProvider"],
});
expectSourceOmitsImportPattern("self-hosted-provider-setup", "./vllm.js");
expectSourceOmitsImportPattern("self-hosted-provider-setup", "./sglang.js");
expectSourceOmitsSnippet("agent-runtime", "./sglang.js");
expectSourceOmitsSnippet("agent-runtime", "./vllm.js");
expectSourceOmitsSnippet("agent-runtime", "../../extensions/");
expectSourceOmitsSnippet("xai-model-id", "./xai.js");
expectSourceOmitsSnippet("xai-model-id", "../../extensions/");
expectSourceMentions("sandbox", ["registerSandboxBackend", "runPluginCommandWithTimeout"]);
expectSourceMentions("secret-input", [
"buildSecretInputSchema",
"buildOptionalSecretInputSchema",
"normalizeSecretInputString",
]);
expectSourceMentions("provider-http", [
"assertOkOrThrowHttpError",
"normalizeBaseUrl",
"postJsonRequest",
"postTranscriptionRequest",
"requireTranscriptionText",
]);
expectSourceOmits("speech", [
"buildElevenLabsSpeechProvider",
"buildMicrosoftSpeechProvider",
"buildOpenAISpeechProvider",
"edgeTTS",
"elevenLabsTTS",
"inferEdgeExtension",
"openaiTTS",
"OPENAI_TTS_MODELS",
"OPENAI_TTS_VOICES",
]);
expectSourceOmits("media-understanding", [
"deepgramMediaUnderstandingProvider",
"groqMediaUnderstandingProvider",
"assertOkOrThrowHttpError",
"postJsonRequest",
"postTranscriptionRequest",
]);
expectSourceOmits("image-generation", [
"buildFalImageGenerationProvider",
"buildGoogleImageGenerationProvider",
"buildOpenAIImageGenerationProvider",
]);
expectSourceOmits("config-runtime", [
"hasConfiguredSecretInput",
"normalizeResolvedSecretInputString",
"normalizeSecretInputString",
]);
expectSourceMentions("webhook-ingress", [
"registerPluginHttpRoute",
"resolveWebhookPath",
"readRequestBodyWithLimit",
"readJsonWebhookBodyOrReject",
"requestBodyErrorToText",
"withResolvedWebhookRequestPipeline",
]);
expectSourceMentions("testing", ["removeAckReactionAfterReply", "shouldAckReaction"]);
});
it("keeps shared plugin-sdk types aligned", () => {
expectTypeOf<ContractBaseProbeResult>().toMatchTypeOf<BaseProbeResult>();
expectTypeOf<ContractBaseTokenResolution>().toMatchTypeOf<BaseTokenResolution>();
expectTypeOf<ContractChannelAgentTool>().toMatchTypeOf<ChannelAgentTool>();
expectTypeOf<ContractChannelAccountSnapshot>().toMatchTypeOf<ChannelAccountSnapshot>();
expectTypeOf<ContractChannelGroupContext>().toMatchTypeOf<ChannelGroupContext>();
expectTypeOf<ContractChannelMessageActionAdapter>().toMatchTypeOf<ChannelMessageActionAdapter>();
expectTypeOf<ContractChannelMessageActionContext>().toMatchTypeOf<ChannelMessageActionContext>();
expectTypeOf<ContractChannelMessageActionName>().toMatchTypeOf<ChannelMessageActionName>();
expectTypeOf<ContractChannelMessageToolDiscovery>().toMatchTypeOf<ChannelMessageToolDiscovery>();
expectTypeOf<ContractChannelStatusIssue>().toMatchTypeOf<ChannelStatusIssue>();
expectTypeOf<ContractChannelThreadingContext>().toMatchTypeOf<ChannelThreadingContext>();
expectTypeOf<ContractChannelThreadingToolContext>().toMatchTypeOf<ChannelThreadingToolContext>();
expectTypeOf<CoreOpenClawPluginApi>().toMatchTypeOf<OpenClawPluginApi>();
expectTypeOf<CorePluginRuntime>().toMatchTypeOf<PluginRuntime>();
expectTypeOf<CoreChannelMessageActionContext>().toMatchTypeOf<ChannelMessageActionContext>();
expectTypeOf<CoreOpenClawPluginApi>().toMatchTypeOf<SharedOpenClawPluginApi>();
expectTypeOf<CorePluginRuntime>().toMatchTypeOf<SharedPluginRuntime>();
expectTypeOf<CoreChannelMessageActionContext>().toMatchTypeOf<SharedChannelMessageActionContext>();
});
it("keeps runtime entry subpaths importable", async () => {
const [
coreSdk,
channelActionsSdk,
globalSingletonSdk,
textRuntimeSdk,
huggingfaceSdk,
pluginEntrySdk,
channelLifecycleSdk,
channelPairingSdk,
channelReplyPipelineSdk,
...representativeModules
] = await Promise.all([
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/core"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/channel-actions"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/global-singleton"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/text-runtime"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/huggingface"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/plugin-entry"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/channel-lifecycle"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/channel-pairing"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/channel-reply-pipeline"),
...representativeRuntimeSmokeSubpaths.map((id) =>
importResolvedPluginSdkSubpath(`openclaw/plugin-sdk/${id}`),
),
]);
expect(coreSdk.definePluginEntry).toBe(pluginEntrySdk.definePluginEntry);
expect(typeof coreSdk.optionalStringEnum).toBe("function");
expect(typeof channelActionsSdk.optionalStringEnum).toBe("function");
expect(typeof channelActionsSdk.stringEnum).toBe("function");
expect(typeof globalSingletonSdk.resolveGlobalMap).toBe("function");
expect(typeof globalSingletonSdk.resolveGlobalSingleton).toBe("function");
expect(typeof globalSingletonSdk.createScopedExpiringIdCache).toBe("function");
expect(typeof textRuntimeSdk.createScopedExpiringIdCache).toBe("function");
expect(typeof textRuntimeSdk.resolveGlobalMap).toBe("function");
expect(typeof textRuntimeSdk.resolveGlobalSingleton).toBe("function");
expect(typeof huggingfaceSdk.buildHuggingfaceProvider).toBe("function");
expect(typeof huggingfaceSdk.discoverHuggingfaceModels).toBe("function");
expect(Array.isArray(huggingfaceSdk.HUGGINGFACE_MODEL_CATALOG)).toBe(true);
expectSourceMentions("infra-runtime", ["createRuntimeOutboundDelegates"]);
expectSourceContains("infra-runtime", "../infra/outbound/send-deps.js");
expect(typeof channelLifecycleSdk.createDraftStreamLoop).toBe("function");
expect(typeof channelLifecycleSdk.createFinalizableDraftLifecycle).toBe("function");
expect(typeof channelLifecycleSdk.runPassiveAccountLifecycle).toBe("function");
expect(typeof channelLifecycleSdk.createRunStateMachine).toBe("function");
expect(typeof channelLifecycleSdk.createArmableStallWatchdog).toBe("function");
expectSourceMentions("channel-pairing", [
"createChannelPairingController",
"createChannelPairingChallengeIssuer",
"createLoggedPairingApprovalNotifier",
"createPairingPrefixStripper",
"createTextPairingAdapter",
]);
expect("createScopedPairingAccess" in channelPairingSdk).toBe(false);
expectSourceMentions("channel-reply-pipeline", ["createChannelReplyPipeline"]);
expect("createTypingCallbacks" in channelReplyPipelineSdk).toBe(false);
expect("createReplyPrefixContext" in channelReplyPipelineSdk).toBe(false);
expect("createReplyPrefixOptions" in channelReplyPipelineSdk).toBe(false);
expect(pluginSdkSubpaths.length).toBeGreaterThan(representativeRuntimeSmokeSubpaths.length);
for (const [index, id] of representativeRuntimeSmokeSubpaths.entries()) {
const mod = representativeModules[index];
expect(typeof mod).toBe("object");
expect(mod, `subpath ${id} should resolve`).toBeTruthy();
}
});
it("exports single-provider plugin entry helpers from the dedicated subpath", () => {
expect(typeof providerEntrySdk.defineSingleProviderPluginEntry).toBe("function");
});
});