import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { buildPluginSdkPackageExports } from "./entrypoints.js"; async function collectRuntimeExports(filePath: string, seen = new Set()) { const normalizedPath = path.resolve(filePath); if (seen.has(normalizedPath)) { return new Set(); } seen.add(normalizedPath); const source = await fs.readFile(normalizedPath, "utf8"); const exportNames = new Set(); for (const match of source.matchAll(/export\s+(?!type\b)\{([\s\S]*?)\}\s+from\s+"([^"]+)";/g)) { const names = match[1] .split(",") .map((part) => part.trim()) .filter(Boolean) .map((part) => part.split(/\s+as\s+/).at(-1) ?? part); for (const name of names) { exportNames.add(name); } } for (const match of source.matchAll(/export\s+\*\s+from\s+"([^"]+)";/g)) { const specifier = match[1]; if (!specifier.startsWith(".")) { continue; } const nestedPath = path.resolve( path.dirname(normalizedPath), specifier.replace(/\.js$/, ".ts"), ); const nestedExports = await collectRuntimeExports(nestedPath, seen); for (const name of nestedExports) { exportNames.add(name); } } return exportNames; } describe("plugin-sdk exports", () => { it("does not expose runtime modules", async () => { const runtimeExports = await collectRuntimeExports(path.join(import.meta.dirname, "index.ts")); const forbidden = [ "chunkMarkdownText", "chunkText", "hasControlCommand", "isControlCommandMessage", "shouldComputeCommandAuthorized", "shouldHandleTextCommands", "buildMentionRegexes", "matchesMentionPatterns", "resolveStateDir", "writeConfigFile", "enqueueSystemEvent", "fetchRemoteMedia", "saveMediaBuffer", "formatAgentEnvelope", "buildPairingReply", "resolveAgentRoute", "dispatchReplyFromConfig", "createReplyDispatcherWithTyping", "dispatchReplyWithBufferedBlockDispatcher", "resolveCommandAuthorizedFromAuthorizers", "monitorSlackProvider", "monitorTelegramProvider", "monitorIMessageProvider", "monitorSignalProvider", "sendMessageSlack", "sendMessageTelegram", "sendMessageIMessage", "sendMessageSignal", "sendMessageWhatsApp", "probeSlack", "probeTelegram", "probeIMessage", "probeSignal", ]; for (const key of forbidden) { expect(runtimeExports.has(key)).toBe(false); } }); it("keeps the root runtime surface intentionally small", async () => { const runtimeExports = await collectRuntimeExports(path.join(import.meta.dirname, "index.ts")); expect([...runtimeExports].toSorted()).toEqual([ "buildFalImageGenerationProvider", "buildGoogleImageGenerationProvider", "buildOpenAIImageGenerationProvider", "delegateCompactionToRuntime", "emptyPluginConfigSchema", "onDiagnosticEvent", "registerContextEngine", ]); }); it("keeps package.json plugin-sdk exports synced with the manifest", async () => { const packageJsonPath = path.join(process.cwd(), "package.json"); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as { exports?: Record; }; const currentPluginSdkExports = Object.fromEntries( Object.entries(packageJson.exports ?? {}).filter(([key]) => key.startsWith("./plugin-sdk")), ); expect(currentPluginSdkExports).toEqual(buildPluginSdkPackageExports()); }); });