mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 06:11:24 +00:00
test: move remaining plugin-sdk guardrails to contracts
This commit is contained in:
66
src/plugins/contracts/plugin-entry-guardrails.test.ts
Normal file
66
src/plugins/contracts/plugin-entry-guardrails.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
@@ -55,7 +55,7 @@ describe("plugin-sdk bundled exports", () => {
|
||||
neverBundle: ["@lancedb/lancedb", "@matrix-org/matrix-sdk-crypto-nodejs", "matrix-js-sdk"],
|
||||
},
|
||||
// Full plugin-sdk coverage belongs to `pnpm build`, package contract
|
||||
// guardrails, and `subpaths.test.ts`. This file only keeps the expensive
|
||||
// guardrails, and `plugin-sdk-subpaths.test.ts`. This file only keeps the expensive
|
||||
// bundler path honest across representative entrypoint families plus the
|
||||
// Matrix SDK runtime import surface that historically crashed plugin
|
||||
// loading when bare and deep SDK entrypoints mixed.
|
||||
@@ -90,7 +90,7 @@ describe("plugin-sdk bundled exports", () => {
|
||||
expect(filesWithBareMatrixSdkImports).toEqual([]);
|
||||
|
||||
// Export list and package-specifier coverage already live in
|
||||
// package-contract-guardrails.test.ts and subpaths.test.ts. Keep this file
|
||||
// plugin-sdk-package-contract-guardrails.test.ts and plugin-sdk-subpaths.test.ts. Keep this file
|
||||
// focused on the expensive part: can tsdown emit working bundle artifacts?
|
||||
const importResults = await Promise.all(
|
||||
bundledRepresentativeEntrypoints.map(async (entry) => [
|
||||
|
||||
@@ -12,7 +12,7 @@ const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const REPO_ROOT = resolve(ROOT_DIR, "..");
|
||||
const PUBLIC_CONTRACT_REFERENCE_FILES = [
|
||||
"docs/plugins/architecture.md",
|
||||
"src/plugin-sdk/subpaths.test.ts",
|
||||
"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;
|
||||
|
||||
193
src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts
Normal file
193
src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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],
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
812
src/plugins/contracts/plugin-sdk-subpaths.test.ts
Normal file
812
src/plugins/contracts/plugin-sdk-subpaths.test.ts
Normal file
@@ -0,0 +1,812 @@
|
||||
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 { pluginSdkSubpaths } from "../../plugin-sdk/entrypoints.js";
|
||||
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";
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user