Files
moltbot/src/plugin-sdk/channel-import-guardrails.test.ts
2026-03-22 02:42:57 +00:00

494 lines
15 KiB
TypeScript

import { readdirSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
const ALLOWED_EXTENSION_PUBLIC_SURFACES = new Set([
"action-runtime.runtime.js",
"action-runtime-api.js",
"allow-from.js",
"api.js",
"auth-presence.js",
"index.js",
"light-runtime-api.js",
"login-qr-api.js",
"onboard.js",
"openai-codex-catalog.js",
"provider-catalog.js",
"runtime-api.js",
"session-key-api.js",
"setup-api.js",
"setup-entry.js",
"timeouts.js",
]);
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: "extensions/discord/src/shared.ts",
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/discord["']/, /plugin-sdk-internal\/discord/],
},
{
path: "extensions/slack/src/shared.ts",
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/slack["']/, /plugin-sdk-internal\/slack/],
},
{
path: "extensions/telegram/src/shared.ts",
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/telegram["']/, /plugin-sdk-internal\/telegram/],
},
{
path: "extensions/imessage/src/shared.ts",
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/imessage["']/, /plugin-sdk-internal\/imessage/],
},
{
path: "extensions/whatsapp/src/shared.ts",
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/whatsapp["']/, /plugin-sdk-internal\/whatsapp/],
},
{
path: "extensions/signal/src/shared.ts",
forbiddenPatterns: [/["']openclaw\/plugin-sdk\/signal["']/, /plugin-sdk-internal\/signal/],
},
];
const SETUP_BARREL_GUARDS: GuardedSource[] = [
{
path: "extensions/signal/src/setup-core.ts",
forbiddenPatterns: [/\bformatCliCommand\b/, /\bformatDocsLink\b/],
},
{
path: "extensions/signal/src/setup-surface.ts",
forbiddenPatterns: [
/\bdetectBinary\b/,
/\binstallSignalCli\b/,
/\bformatCliCommand\b/,
/\bformatDocsLink\b/,
],
},
{
path: "extensions/slack/src/setup-core.ts",
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: "extensions/slack/src/setup-surface.ts",
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: "extensions/discord/src/setup-core.ts",
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: "extensions/discord/src/setup-surface.ts",
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: "extensions/imessage/src/setup-core.ts",
forbiddenPatterns: [/\bformatDocsLink\b/],
},
{
path: "extensions/imessage/src/setup-surface.ts",
forbiddenPatterns: [/\bdetectBinary\b/, /\bformatDocsLink\b/],
},
{
path: "extensions/telegram/src/setup-core.ts",
forbiddenPatterns: [/\bformatCliCommand\b/, /\bformatDocsLink\b/],
},
{
path: "extensions/whatsapp/src/setup-surface.ts",
forbiddenPatterns: [/\bformatCliCommand\b/, /\bformatDocsLink\b/],
},
];
const LOCAL_EXTENSION_API_BARREL_GUARDS = [
"acpx",
"bluebubbles",
"device-pair",
"diagnostics-otel",
"discord",
"diffs",
"feishu",
"google",
"irc",
"llm-task",
"line",
"lobster",
"matrix",
"mattermost",
"memory-lancedb",
"msteams",
"nextcloud-talk",
"nostr",
"open-prose",
"phone-control",
"copilot-proxy",
"zai",
"qwen-portal-auth",
"signal",
"synology-chat",
"talk-voice",
"telegram",
"thread-ownership",
"tlon",
"voice-call",
"whatsapp",
"twitch",
"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 -> extensions/matrix/api.ts -> accounts.ts
"extensions/matrix/src/matrix/accounts.ts",
] as const;
const sourceTextCache = new Map<string, string>();
let extensionSourceFilesCache: string[] | null = null;
let coreSourceFilesCache: string[] | null = null;
const extensionFilesCache = new Map<string, string[]>();
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 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[] {
if (extensionSourceFilesCache) {
return extensionSourceFilesCache;
}
const extensionsDir = normalizePath(resolve(ROOT_DIR, "..", "extensions"));
const sharedExtensionsDir = normalizePath(resolve(extensionsDir, "shared"));
const files: string[] = [];
const stack = [resolve(ROOT_DIR, "..", "extensions")];
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;
}
stack.push(fullPath);
continue;
}
if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) {
continue;
}
if (entry.name.endsWith(".d.ts") || normalizedFullPath.includes(sharedExtensionsDir)) {
continue;
}
if (normalizedFullPath.includes(`${extensionsDir}/shared/`)) {
continue;
}
if (
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".test-") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
normalizedFullPath.includes("test-support") ||
entry.name === "api.ts" ||
entry.name === "runtime-api.ts"
) {
continue;
}
files.push(fullPath);
}
}
extensionSourceFilesCache = files;
return files;
}
function collectCoreSourceFiles(): string[] {
if (coreSourceFilesCache) {
return coreSourceFilesCache;
}
const srcDir = resolve(ROOT_DIR, "..", "src");
const normalizedPluginSdkDir = normalizePath(resolve(ROOT_DIR, "plugin-sdk"));
const files: string[] = [];
const stack = [srcDir];
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;
}
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 (
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".mock-harness.") ||
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}/`)
) {
continue;
}
files.push(fullPath);
}
}
coreSourceFilesCache = files;
return files;
}
function collectExtensionFiles(extensionId: string): string[] {
const cached = extensionFilesCache.get(extensionId);
if (cached) {
return cached;
}
const extensionDir = resolve(ROOT_DIR, "..", "extensions", extensionId);
const files: string[] = [];
const stack = [extensionDir];
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;
}
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 (
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".test-") ||
normalizedFullPath.includes(".spec.") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
entry.name === "runtime-api.ts"
) {
continue;
}
files.push(fullPath);
}
}
extensionFilesCache.set(extensionId, files);
return files;
}
function collectExtensionImports(text: string): string[] {
return [...text.matchAll(/["']([^"']*extensions\/[^"']+\.(?:[cm]?[jt]sx?))["']/g)].map(
(match) => match[1] ?? "",
);
}
function collectImportSpecifiers(text: string): string[] {
return [...text.matchAll(/["']([^"']+\.(?:[cm]?[jt]sx?))["']/g)].map((match) => match[1] ?? "");
}
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(/extensions\/([^/]+)\//)?.[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(/\/extensions\/([^/]+)\//)?.[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}`);
}
}
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 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 extension private src imports", () => {
for (const file of collectCoreSourceFiles()) {
const text = readSource(file);
expect(text, `${file} should not import extensions/*/src`).not.toMatch(
/["'][^"']*extensions\/[^/"']+\/src\//,
);
}
});
it("keeps extension production files off other extensions' private src imports", () => {
for (const file of collectExtensionSourceFiles()) {
const text = readSource(file);
expectNoSiblingExtensionPrivateSrcImports(file, collectImportSpecifiers(text));
}
});
it("keeps core extension imports limited to approved public surfaces", () => {
for (const file of collectCoreSourceFiles()) {
expectOnlyApprovedExtensionSeams(file, collectExtensionImports(readSource(file)));
}
});
it("keeps extension-to-extension imports limited to approved public surfaces", () => {
for (const file of collectExtensionSourceFiles()) {
expectOnlyApprovedExtensionSeams(file, collectExtensionImports(readSource(file)));
}
});
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"));
}
}
});
});