test(boundary): route helper imports through bundled plugin surfaces

This commit is contained in:
Vincent Koc
2026-04-10 08:03:42 +01:00
parent 50f5091979
commit 3cea11d3b6
18 changed files with 516 additions and 153 deletions

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env node
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import { BUNDLED_PLUGIN_PATH_PREFIX } from "./lib/bundled-plugin-paths.mjs";
import {
collectTypeScriptInventory,
normalizeRepoPath,
resolveRepoSpecifier,
visitModuleSpecifiers,
writeLine,
} from "./lib/guard-inventory-utils.mjs";
import {
collectTypeScriptFilesFromRoots,
resolveSourceRoots,
runAsScript,
toLine,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const scanRoots = resolveSourceRoots(repoRoot, ["test/helpers"]);
let cachedInventoryPromise = null;
function compareEntries(left, right) {
return (
left.file.localeCompare(right.file) ||
left.line - right.line ||
left.kind.localeCompare(right.kind) ||
left.specifier.localeCompare(right.specifier) ||
left.reason.localeCompare(right.reason)
);
}
function classifyResolvedExtensionReason(kind) {
const verb =
kind === "export"
? "re-exports"
: kind === "dynamic-import"
? "dynamically imports"
: "imports";
return `${verb} bundled plugin file from test helper boundary`;
}
function scanImportBoundaryViolations(sourceFile, filePath) {
const entries = [];
const relativeFile = normalizeRepoPath(repoRoot, filePath);
visitModuleSpecifiers(ts, sourceFile, ({ kind, specifier, specifierNode }) => {
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
if (!resolvedPath?.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
return;
}
entries.push({
file: relativeFile,
line: toLine(sourceFile, specifierNode),
kind,
specifier,
resolvedPath,
reason: classifyResolvedExtensionReason(kind),
});
});
return entries;
}
export async function collectTestHelperExtensionImportBoundaryInventory() {
if (cachedInventoryPromise) {
return cachedInventoryPromise;
}
cachedInventoryPromise = (async () => {
const files = (await collectTypeScriptFilesFromRoots(scanRoots)).toSorted((left, right) =>
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
);
return await collectTypeScriptInventory({
ts,
files,
compareEntries,
collectEntries(sourceFile, filePath) {
return scanImportBoundaryViolations(sourceFile, filePath);
},
});
})();
try {
return await cachedInventoryPromise;
} catch (error) {
cachedInventoryPromise = null;
throw error;
}
}
function formatInventoryHuman(inventory) {
if (inventory.length === 0) {
return "Rule: test/helpers/** must not import bundled plugin files directly\nNo test-helper import boundary violations found.";
}
const lines = [
"Rule: test/helpers/** must not import bundled plugin files directly",
"Test-helper extension import boundary inventory:",
];
let activeFile = "";
for (const entry of inventory) {
if (entry.file !== activeFile) {
activeFile = entry.file;
lines.push(activeFile);
}
lines.push(` - line ${entry.line} [${entry.kind}] ${entry.reason}`);
lines.push(` specifier: ${entry.specifier}`);
lines.push(` resolved: ${entry.resolvedPath}`);
}
return lines.join("\n");
}
export async function main(argv = process.argv.slice(2), io) {
const streams = io ?? { stdout: process.stdout, stderr: process.stderr };
const json = argv.includes("--json");
const inventory = await collectTestHelperExtensionImportBoundaryInventory();
if (json) {
writeLine(streams.stdout, JSON.stringify(inventory, null, 2));
} else {
writeLine(streams.stdout, formatInventoryHuman(inventory));
writeLine(
streams.stdout,
inventory.length === 0 ? "Boundary is clean." : "Boundary has violations.",
);
}
return inventory.length === 0 ? 0 : 1;
}
runAsScript(import.meta.url, main);

View File

@@ -24,8 +24,8 @@ function expectedBundledMusicProviderPluginIds(): string[] {
}
describe("bundled media-generation provider capabilities", () => {
it("declares explicit mode support for every bundled video-generation provider", () => {
const entries = loadBundledVideoGenerationProviders();
it("declares explicit mode support for every bundled video-generation provider", async () => {
const entries = await loadBundledVideoGenerationProviders();
expect(entries.map((entry) => entry.pluginId).toSorted()).toEqual(
expectedBundledVideoProviderPluginIds(),
);
@@ -66,8 +66,8 @@ describe("bundled media-generation provider capabilities", () => {
}
});
it("declares explicit generate/edit support for every bundled music-generation provider", () => {
const entries = loadBundledMusicGenerationProviders();
it("declares explicit generate/edit support for every bundled music-generation provider", async () => {
const entries = await loadBundledMusicGenerationProviders();
expect(entries.map((entry) => entry.pluginId).toSorted()).toEqual(
expectedBundledMusicProviderPluginIds(),
);

View File

@@ -33,6 +33,27 @@ export function loadBundledPluginPublicSurfaceSync<T extends object>(params: {
});
}
export function loadBundledPluginApiSync<T extends object>(pluginId: string): T {
return loadBundledPluginPublicSurfaceSync<T>({
pluginId,
artifactBasename: "api.js",
});
}
export function loadBundledPluginContractApiSync<T extends object>(pluginId: string): T {
return loadBundledPluginPublicSurfaceSync<T>({
pluginId,
artifactBasename: "contract-api.js",
});
}
export function loadBundledPluginRuntimeApiSync<T extends object>(pluginId: string): T {
return loadBundledPluginPublicSurfaceSync<T>({
pluginId,
artifactBasename: "runtime-api.js",
});
}
export function loadBundledPluginTestApiSync<T extends object>(pluginId: string): T {
return loadBundledPluginPublicSurfaceSync<T>({
pluginId,
@@ -40,20 +61,29 @@ export function loadBundledPluginTestApiSync<T extends object>(pluginId: string)
});
}
export function resolveBundledPluginPublicModulePath(params: {
pluginId: string;
artifactBasename: string;
}): string {
const metadata = findBundledPluginMetadata(params.pluginId);
return path.resolve(
OPENCLAW_PACKAGE_ROOT,
"extensions",
metadata.dirName,
normalizeBundledPluginArtifactSubpath(params.artifactBasename),
);
}
export function resolveRelativeBundledPluginPublicModuleId(params: {
fromModuleUrl: string;
pluginId: string;
artifactBasename: string;
}): string {
const metadata = findBundledPluginMetadata(params.pluginId);
const fromFilePath = fileURLToPath(params.fromModuleUrl);
const artifactBasename = normalizeBundledPluginArtifactSubpath(params.artifactBasename);
const targetPath = path.resolve(
OPENCLAW_PACKAGE_ROOT,
"extensions",
metadata.dirName,
artifactBasename,
);
const targetPath = resolveBundledPluginPublicModulePath({
pluginId: params.pluginId,
artifactBasename: params.artifactBasename,
});
const relativePath = path
.relative(path.dirname(fromFilePath), targetPath)
.replaceAll(path.sep, "/");

View File

@@ -1,5 +1,15 @@
import { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
type IMessageContractSurface = typeof import("@openclaw/imessage/contract-api.js");
const {
DEFAULT_IMESSAGE_ATTACHMENT_ROOTS,
resolveIMessageAttachmentRoots,
resolveIMessageRemoteAttachmentRoots,
} = loadBundledPluginContractApiSync<IMessageContractSurface>("imessage");
export {
DEFAULT_IMESSAGE_ATTACHMENT_ROOTS,
resolveIMessageAttachmentRoots,
resolveIMessageRemoteAttachmentRoots,
} from "../../../extensions/imessage/contract-api.js";
};

View File

@@ -1,6 +1,22 @@
export { buildTelegramModelsProviderChannelData } from "../../../extensions/telegram/contract-api.js";
export { whatsappCommandPolicy } from "../../../extensions/whatsapp/contract-api.js";
import {
loadBundledPluginApiSync,
loadBundledPluginContractApiSync,
} from "../../../src/test-utils/bundled-plugin-public-surface.js";
type TelegramContractSurface = typeof import("@openclaw/telegram/contract-api.js");
type WhatsAppApiSurface = Pick<
typeof import("@openclaw/whatsapp/api.js"),
"isWhatsAppGroupJid" | "normalizeWhatsAppTarget" | "whatsappCommandPolicy"
>;
const { buildTelegramModelsProviderChannelData } =
loadBundledPluginContractApiSync<TelegramContractSurface>("telegram");
const { isWhatsAppGroupJid, normalizeWhatsAppTarget, whatsappCommandPolicy } =
loadBundledPluginApiSync<WhatsAppApiSurface>("whatsapp");
export {
buildTelegramModelsProviderChannelData,
isWhatsAppGroupJid,
normalizeWhatsAppTarget,
} from "../../../extensions/whatsapp/contract-api.js";
whatsappCommandPolicy,
};

View File

@@ -1,4 +1,13 @@
export {
isSignalSenderAllowed,
type SignalSender,
} from "../../../extensions/signal/contract-api.js";
import type { SignalSender } from "@openclaw/signal/contract-api.js";
import { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
type SignalContractApiSurface = Pick<
typeof import("@openclaw/signal/contract-api.js"),
"isSignalSenderAllowed"
>;
const { isSignalSenderAllowed } =
loadBundledPluginContractApiSync<SignalContractApiSurface>("signal");
export { isSignalSenderAllowed };
export type { SignalSender };

View File

@@ -1,5 +1,15 @@
export { resolveWhatsAppRuntimeGroupPolicy } from "../../../extensions/whatsapp/test-api.js";
import { loadBundledPluginTestApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
type WhatsAppTestSurface = typeof import("@openclaw/whatsapp/test-api.js");
type ZaloTestSurface = typeof import("@openclaw/zalo/test-api.js");
const { resolveWhatsAppRuntimeGroupPolicy } =
loadBundledPluginTestApiSync<WhatsAppTestSurface>("whatsapp");
const { evaluateZaloGroupAccess, resolveZaloRuntimeGroupPolicy } =
loadBundledPluginTestApiSync<ZaloTestSurface>("zalo");
export {
evaluateZaloGroupAccess,
resolveWhatsAppRuntimeGroupPolicy,
resolveZaloRuntimeGroupPolicy,
} from "../../../extensions/zalo/test-api.js";
};

View File

@@ -1,11 +1,12 @@
import { expect, it } from "vitest";
import type { ResolvedSlackAccount } from "../../../extensions/slack/api.js";
import type { MsgContext } from "../../../src/auto-reply/templating.js";
import { expectChannelInboundContextContract } from "../../../src/channels/plugins/contracts/test-helpers.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { withTempHome } from "../temp-home.js";
type ResolvedSlackAccount = import("@openclaw/slack/api.js").ResolvedSlackAccount;
type SlackMessageEvent = {
channel: string;
channel_type?: string;

View File

@@ -1,12 +1,12 @@
export type {
DiscordInteractiveHandlerContext,
DiscordInteractiveHandlerRegistration,
} from "../../../extensions/discord/contract-api.js";
} from "@openclaw/discord/contract-api.js";
export type {
SlackInteractiveHandlerContext,
SlackInteractiveHandlerRegistration,
} from "../../../extensions/slack/contract-api.js";
} from "@openclaw/slack/contract-api.js";
export type {
TelegramInteractiveHandlerContext,
TelegramInteractiveHandlerRegistration,
} from "../../../extensions/telegram/contract-api.js";
} from "@openclaw/telegram/contract-api.js";

View File

@@ -1 +1,8 @@
export { matrixSetupAdapter, matrixSetupWizard } from "../../../extensions/matrix/contract-api.js";
import { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
type MatrixContractSurface = typeof import("@openclaw/matrix/contract-api.js");
const { matrixSetupAdapter, matrixSetupWizard } =
loadBundledPluginContractApiSync<MatrixContractSurface>("matrix");
export { matrixSetupAdapter, matrixSetupWizard };

View File

@@ -1,27 +1,4 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import {
listDiscordDirectoryGroupsFromConfig,
listDiscordDirectoryPeersFromConfig,
type DiscordProbe,
type DiscordTokenResolution,
} from "../../../extensions/discord/api.js";
import type { IMessageProbe } from "../../../extensions/imessage/runtime-api.js";
import type { SignalProbe } from "../../../extensions/signal/api.js";
import {
listSlackDirectoryGroupsFromConfig,
listSlackDirectoryPeersFromConfig,
type SlackProbe,
} from "../../../extensions/slack/api.js";
import {
listTelegramDirectoryGroupsFromConfig,
listTelegramDirectoryPeersFromConfig,
type TelegramProbe,
type TelegramTokenResolution,
} from "../../../extensions/telegram/api.js";
import {
listWhatsAppDirectoryGroupsFromConfig,
listWhatsAppDirectoryPeersFromConfig,
} from "../../../extensions/whatsapp/api.js";
import type {
BaseProbeResult,
BaseTokenResolution,
@@ -29,8 +6,30 @@ import type {
} from "../../../src/channels/plugins/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import type { LineProbeResult } from "../../../src/plugin-sdk/line.js";
import { loadBundledPluginApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { withEnvAsync } from "../../../src/test-utils/env.js";
type DiscordApiSurface = typeof import("@openclaw/discord/api.js");
type DiscordProbe = import("@openclaw/discord/api.js").DiscordProbe;
type DiscordTokenResolution = import("@openclaw/discord/api.js").DiscordTokenResolution;
type IMessageProbe = import("@openclaw/imessage/runtime-api.js").IMessageProbe;
type SignalProbe = import("@openclaw/signal/api.js").SignalProbe;
type SlackApiSurface = typeof import("@openclaw/slack/api.js");
type SlackProbe = import("@openclaw/slack/api.js").SlackProbe;
type TelegramApiSurface = typeof import("@openclaw/telegram/api.js");
type TelegramProbe = import("@openclaw/telegram/api.js").TelegramProbe;
type TelegramTokenResolution = import("@openclaw/telegram/api.js").TelegramTokenResolution;
type WhatsAppApiSurface = typeof import("@openclaw/whatsapp/api.js");
const { listDiscordDirectoryGroupsFromConfig, listDiscordDirectoryPeersFromConfig } =
loadBundledPluginApiSync<DiscordApiSurface>("discord");
const { listSlackDirectoryGroupsFromConfig, listSlackDirectoryPeersFromConfig } =
loadBundledPluginApiSync<SlackApiSurface>("slack");
const { listTelegramDirectoryGroupsFromConfig, listTelegramDirectoryPeersFromConfig } =
loadBundledPluginApiSync<TelegramApiSurface>("telegram");
const { listWhatsAppDirectoryGroupsFromConfig, listWhatsAppDirectoryPeersFromConfig } =
loadBundledPluginApiSync<WhatsAppApiSurface>("whatsapp");
type DirectoryListFn = (params: {
cfg: OpenClawConfig;
accountId?: string;

View File

@@ -1,14 +1,4 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { bluebubblesPlugin } from "../../../extensions/bluebubbles/api.js";
import {
discordPlugin,
discordThreadBindingTesting,
} from "../../../extensions/discord/test-api.js";
import { feishuPlugin, feishuThreadBindingTesting } from "../../../extensions/feishu/api.js";
import { imessagePlugin } from "../../../extensions/imessage/api.js";
import { matrixPlugin, setMatrixRuntime } from "../../../extensions/matrix/test-api.js";
import { telegramPlugin } from "../../../extensions/telegram/api.js";
import { resetTelegramThreadBindingsForTests } from "../../../extensions/telegram/test-api.js";
import type { ChannelPlugin } from "../../../src/channels/plugins/types.js";
import {
clearRuntimeConfigSnapshot,
@@ -22,9 +12,35 @@ import {
import { resetPluginRuntimeStateForTest } from "../../../src/plugins/runtime.js";
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
import type { PluginRuntime } from "../../../src/plugins/runtime/index.js";
import {
loadBundledPluginApiSync,
loadBundledPluginTestApiSync,
} from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { createTestRegistry } from "../../../src/test-utils/channel-plugins.js";
import { getSessionBindingContractRegistry } from "./registry-session-binding.js";
type BluebubblesApiSurface = typeof import("@openclaw/bluebubbles/api.js");
type DiscordTestApiSurface = typeof import("@openclaw/discord/test-api.js");
type FeishuApiSurface = typeof import("@openclaw/feishu/api.js");
type IMessageApiSurface = typeof import("@openclaw/imessage/api.js");
type MatrixApiSurface = typeof import("@openclaw/matrix/api.js");
type MatrixTestApiSurface = typeof import("@openclaw/matrix/test-api.js");
type TelegramApiSurface = typeof import("@openclaw/telegram/api.js");
type TelegramTestApiSurface = typeof import("@openclaw/telegram/test-api.js");
const { bluebubblesPlugin } = loadBundledPluginApiSync<BluebubblesApiSurface>("bluebubbles");
const { discordPlugin, discordThreadBindingTesting } =
loadBundledPluginTestApiSync<DiscordTestApiSurface>("discord");
const { feishuPlugin, feishuThreadBindingTesting } =
loadBundledPluginApiSync<FeishuApiSurface>("feishu");
const { imessagePlugin } = loadBundledPluginApiSync<IMessageApiSurface>("imessage");
const { resetMatrixThreadBindingsForTests } = loadBundledPluginApiSync<MatrixApiSurface>("matrix");
const { matrixPlugin, setMatrixRuntime } =
loadBundledPluginTestApiSync<MatrixTestApiSurface>("matrix");
const { telegramPlugin } = loadBundledPluginApiSync<TelegramApiSurface>("telegram");
const { resetTelegramThreadBindingsForTests } =
loadBundledPluginTestApiSync<TelegramTestApiSurface>("telegram");
type DiscordThreadBindingTesting = {
resetThreadBindingsForTests: () => void;
};
@@ -72,8 +88,7 @@ async function getFeishuThreadBindingTesting() {
}
async function getResetMatrixThreadBindingsForTests() {
const matrixApi = await import("../../../extensions/matrix/api.js");
return matrixApi.resetMatrixThreadBindingsForTests;
return resetMatrixThreadBindingsForTests;
}
function resolveSessionBindingContractRuntimeConfig(id: string) {

View File

@@ -1,52 +1,78 @@
import { buildAlibabaVideoGenerationProvider } from "../../../extensions/alibaba/video-generation-provider.js";
import { buildBytePlusVideoGenerationProvider } from "../../../extensions/byteplus/video-generation-provider.js";
import { buildComfyMusicGenerationProvider } from "../../../extensions/comfy/music-generation-provider.js";
import { buildComfyVideoGenerationProvider } from "../../../extensions/comfy/video-generation-provider.js";
import { buildFalVideoGenerationProvider } from "../../../extensions/fal/video-generation-provider.js";
import { buildGoogleMusicGenerationProvider } from "../../../extensions/google/music-generation-provider.js";
import { buildGoogleVideoGenerationProvider } from "../../../extensions/google/video-generation-provider.js";
import { buildMinimaxMusicGenerationProvider } from "../../../extensions/minimax/music-generation-provider.js";
import { buildMinimaxVideoGenerationProvider } from "../../../extensions/minimax/video-generation-provider.js";
import { buildOpenAIVideoGenerationProvider } from "../../../extensions/openai/video-generation-provider.js";
import { buildQwenVideoGenerationProvider } from "../../../extensions/qwen/video-generation-provider.js";
import { buildRunwayVideoGenerationProvider } from "../../../extensions/runway/video-generation-provider.js";
import { buildTogetherVideoGenerationProvider } from "../../../extensions/together/video-generation-provider.js";
import { buildVydraVideoGenerationProvider } from "../../../extensions/vydra/video-generation-provider.js";
import { buildXaiVideoGenerationProvider } from "../../../extensions/xai/video-generation-provider.js";
import type { MusicGenerationProvider } from "../../../src/music-generation/types.js";
import type { VideoGenerationProvider } from "../../../src/video-generation/types.js";
import type {
MusicGenerationProviderPlugin,
OpenClawPluginApi,
VideoGenerationProviderPlugin,
} from "../../../src/plugins/types.js";
import { loadBundledPluginPublicSurfaceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { registerProviderPlugin } from "../plugins/provider-registration.js";
type BundledPluginEntryModule = {
default: {
register(api: OpenClawPluginApi): void | Promise<void>;
};
};
export type BundledVideoProviderEntry = {
pluginId: string;
provider: VideoGenerationProvider;
provider: VideoGenerationProviderPlugin;
};
export type BundledMusicProviderEntry = {
pluginId: string;
provider: MusicGenerationProvider;
provider: MusicGenerationProviderPlugin;
};
export function loadBundledVideoGenerationProviders(): BundledVideoProviderEntry[] {
return [
{ pluginId: "alibaba", provider: buildAlibabaVideoGenerationProvider() },
{ pluginId: "byteplus", provider: buildBytePlusVideoGenerationProvider() },
{ pluginId: "comfy", provider: buildComfyVideoGenerationProvider() },
{ pluginId: "fal", provider: buildFalVideoGenerationProvider() },
{ pluginId: "google", provider: buildGoogleVideoGenerationProvider() },
{ pluginId: "minimax", provider: buildMinimaxVideoGenerationProvider() },
{ pluginId: "openai", provider: buildOpenAIVideoGenerationProvider() },
{ pluginId: "qwen", provider: buildQwenVideoGenerationProvider() },
{ pluginId: "runway", provider: buildRunwayVideoGenerationProvider() },
{ pluginId: "together", provider: buildTogetherVideoGenerationProvider() },
{ pluginId: "vydra", provider: buildVydraVideoGenerationProvider() },
{ pluginId: "xai", provider: buildXaiVideoGenerationProvider() },
];
const BUNDLED_VIDEO_PROVIDER_PLUGIN_IDS = [
"alibaba",
"byteplus",
"comfy",
"fal",
"google",
"minimax",
"openai",
"qwen",
"runway",
"together",
"vydra",
"xai",
] as const;
const BUNDLED_MUSIC_PROVIDER_PLUGIN_IDS = ["comfy", "google", "minimax"] as const;
function loadBundledPluginEntry(pluginId: string): BundledPluginEntryModule {
return loadBundledPluginPublicSurfaceSync<BundledPluginEntryModule>({
pluginId,
artifactBasename: "index.js",
});
}
export function loadBundledMusicGenerationProviders(): BundledMusicProviderEntry[] {
return [
{ pluginId: "comfy", provider: buildComfyMusicGenerationProvider() },
{ pluginId: "google", provider: buildGoogleMusicGenerationProvider() },
{ pluginId: "minimax", provider: buildMinimaxMusicGenerationProvider() },
];
async function registerBundledMediaPlugin(pluginId: string) {
const { default: plugin } = loadBundledPluginEntry(pluginId);
return await registerProviderPlugin({
plugin,
id: pluginId,
name: pluginId,
});
}
export async function loadBundledVideoGenerationProviders(): Promise<BundledVideoProviderEntry[]> {
return (
await Promise.all(
BUNDLED_VIDEO_PROVIDER_PLUGIN_IDS.map(async (pluginId) => {
const { videoProviders } = await registerBundledMediaPlugin(pluginId);
return videoProviders.map((provider) => ({ pluginId, provider }));
}),
)
).flat();
}
export async function loadBundledMusicGenerationProviders(): Promise<BundledMusicProviderEntry[]> {
return (
await Promise.all(
BUNDLED_MUSIC_PROVIDER_PLUGIN_IDS.map(async (pluginId) => {
const { musicProviders } = await registerBundledMediaPlugin(pluginId);
return musicProviders.map((provider) => ({ pluginId, provider }));
}),
)
).flat();
}

View File

@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { clearRuntimeAuthProfileStoreSnapshots } from "../../../src/agents/auth-profiles/store.js";
import type { AuthProfileStore } from "../../../src/agents/auth-profiles/types.js";
import { createNonExitingRuntime } from "../../../src/runtime.js";
import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import type {
WizardMultiSelectParams,
WizardPrompter,
@@ -25,13 +26,18 @@ const loginOpenAICodexOAuthMock = vi.hoisted(() => vi.fn<LoginOpenAICodexOAuth>(
const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn<GithubCopilotLoginCommand>());
const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn<EnsureAuthProfileStore>());
const listProfilesForProviderMock = vi.hoisted(() => vi.fn<ListProfilesForProvider>());
const providerAuthContractModules = vi.hoisted(() => ({
githubCopilotIndexModuleUrl: new URL(
"../../../extensions/github-copilot/index.ts",
import.meta.url,
).href,
openAIIndexModuleUrl: new URL("../../../extensions/openai/index.ts", import.meta.url).href,
}));
const providerAuthContractModules = {
githubCopilotIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "github-copilot",
artifactBasename: "index.js",
}),
openAIIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "openai",
artifactBasename: "index.js",
}),
};
vi.mock("openclaw/plugin-sdk/provider-auth-login", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/provider-auth-login")>(

View File

@@ -2,6 +2,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "../../../src/agents/auth-profiles/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import type { ModelDefinitionConfig } from "../../../src/config/types.models.js";
import {
resolveBundledPluginPublicModulePath,
resolveRelativeBundledPluginPublicModuleId,
} from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { registerProviders, requireProvider } from "./contracts-testkit.js";
const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn());
@@ -10,32 +14,59 @@ const buildVllmProviderMock = vi.hoisted(() => vi.fn());
const buildSglangProviderMock = vi.hoisted(() => vi.fn());
const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn());
const listProfilesForProviderMock = vi.hoisted(() => vi.fn());
const bundledProviderModules = vi.hoisted(() => ({
cloudflareAiGatewayIndexModuleUrl: new URL(
"../../../extensions/cloudflare-ai-gateway/index.ts",
import.meta.url,
).href,
cloudflareAiGatewayIndexModuleId: new URL(
"../../../extensions/cloudflare-ai-gateway/index.js",
import.meta.url,
).pathname,
githubCopilotIndexModuleUrl: new URL(
"../../../extensions/github-copilot/index.ts",
import.meta.url,
).href,
githubCopilotTokenModuleId: new URL(
"../../../extensions/github-copilot/token.js",
import.meta.url,
).pathname,
minimaxIndexModuleUrl: new URL("../../../extensions/minimax/index.ts", import.meta.url).href,
qwenIndexModuleUrl: new URL("../../../extensions/qwen/index.ts", import.meta.url).href,
ollamaApiModuleId: new URL("../../../extensions/ollama/api.js", import.meta.url).pathname,
ollamaIndexModuleUrl: new URL("../../../extensions/ollama/index.ts", import.meta.url).href,
sglangApiModuleId: new URL("../../../extensions/sglang/api.js", import.meta.url).pathname,
sglangIndexModuleUrl: new URL("../../../extensions/sglang/index.ts", import.meta.url).href,
vllmApiModuleId: new URL("../../../extensions/vllm/api.js", import.meta.url).pathname,
vllmIndexModuleUrl: new URL("../../../extensions/vllm/index.ts", import.meta.url).href,
}));
const bundledProviderModules = {
cloudflareAiGatewayIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "cloudflare-ai-gateway",
artifactBasename: "index.js",
}),
githubCopilotIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "github-copilot",
artifactBasename: "index.js",
}),
githubCopilotRegisterRuntimeModuleId: resolveBundledPluginPublicModulePath({
pluginId: "github-copilot",
artifactBasename: "register.runtime.js",
}),
minimaxIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "minimax",
artifactBasename: "index.js",
}),
qwenIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "qwen",
artifactBasename: "index.js",
}),
ollamaApiModuleId: resolveBundledPluginPublicModulePath({
pluginId: "ollama",
artifactBasename: "api.js",
}),
ollamaIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "ollama",
artifactBasename: "index.js",
}),
sglangApiModuleId: resolveBundledPluginPublicModulePath({
pluginId: "sglang",
artifactBasename: "api.js",
}),
sglangIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "sglang",
artifactBasename: "index.js",
}),
vllmApiModuleId: resolveBundledPluginPublicModulePath({
pluginId: "vllm",
artifactBasename: "api.js",
}),
vllmIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "vllm",
artifactBasename: "index.js",
}),
};
type ProviderHandle = Awaited<ReturnType<typeof requireProvider>>;
@@ -186,9 +217,9 @@ function installDiscoveryHooks(
validateApiKeyInput: () => undefined,
};
});
vi.doMock(bundledProviderModules.githubCopilotTokenModuleId, async () => {
vi.doMock(bundledProviderModules.githubCopilotRegisterRuntimeModuleId, async () => {
const actual = await vi.importActual<object>(
bundledProviderModules.githubCopilotTokenModuleId,
bundledProviderModules.githubCopilotRegisterRuntimeModuleId,
);
return {
...actual,

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ProviderPlugin, ProviderRuntimeModel } from "../../../src/plugins/types.js";
import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import {
createProviderUsageFetch,
makeResponse,
@@ -20,17 +21,48 @@ const getOAuthProvidersMock = vi.hoisted(() =>
{ id: "openai-codex", envApiKey: "OPENAI_API_KEY", oauthTokenEnv: "OPENAI_OAUTH_TOKEN" },
]),
);
const providerRuntimeContractModules = vi.hoisted(() => ({
anthropicIndexModuleId: "../../../extensions/anthropic/index.ts",
githubCopilotIndexModuleId: "../../../extensions/github-copilot/index.ts",
googleIndexModuleId: "../../../extensions/google/index.ts",
openAIIndexModuleId: "../../../extensions/openai/index.ts",
openAICodexProviderRuntimeModuleId: "../../../extensions/openai/openai-codex-provider.runtime.js",
openRouterIndexModuleId: "../../../extensions/openrouter/index.ts",
veniceIndexModuleId: "../../../extensions/venice/index.ts",
xAIIndexModuleId: "../../../extensions/xai/index.ts",
zaiIndexModuleId: "../../../extensions/zai/index.ts",
}));
const providerRuntimeContractModules = {
anthropicIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "anthropic",
artifactBasename: "index.js",
}),
githubCopilotIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "github-copilot",
artifactBasename: "index.js",
}),
googleIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "google",
artifactBasename: "index.js",
}),
openAIIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "openai",
artifactBasename: "index.js",
}),
openRouterIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "openrouter",
artifactBasename: "index.js",
}),
veniceIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "venice",
artifactBasename: "index.js",
}),
xAIIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "xai",
artifactBasename: "index.js",
}),
zaiIndexModuleId: resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "zai",
artifactBasename: "index.js",
}),
};
vi.mock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
@@ -43,10 +75,6 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => {
};
});
vi.mock(providerRuntimeContractModules.openAICodexProviderRuntimeModuleId, () => ({
refreshOpenAICodexToken: refreshOpenAICodexTokenMock,
}));
async function importBundledProviderPlugin<T>(moduleUrl: string): Promise<T> {
return (await import(moduleUrl)) as T;
}

View File

@@ -1,3 +1,16 @@
import { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
type AnthropicContractSurface = typeof import("@openclaw/anthropic/contract-api.js");
const {
createAnthropicBetaHeadersWrapper,
createAnthropicFastModeWrapper,
createAnthropicServiceTierWrapper,
resolveAnthropicBetas,
resolveAnthropicFastMode,
resolveAnthropicServiceTier,
} = loadBundledPluginContractApiSync<AnthropicContractSurface>("anthropic");
export {
createAnthropicBetaHeadersWrapper,
createAnthropicFastModeWrapper,
@@ -5,4 +18,4 @@ export {
resolveAnthropicBetas,
resolveAnthropicFastMode,
resolveAnthropicServiceTier,
} from "../../../extensions/anthropic/contract-api.js";
};

View File

@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import {
collectTestHelperExtensionImportBoundaryInventory,
main,
} from "../scripts/check-test-helper-extension-import-boundary.mjs";
import { createCapturedIo } from "./helpers/captured-io.js";
describe("test-helper extension import boundary inventory", () => {
it("stays empty", async () => {
expect(await collectTestHelperExtensionImportBoundaryInventory()).toEqual([]);
});
it("produces stable sorted output", async () => {
const first = await collectTestHelperExtensionImportBoundaryInventory();
const second = await collectTestHelperExtensionImportBoundaryInventory();
expect(second).toEqual(first);
});
it("script json output stays empty", async () => {
const captured = createCapturedIo();
const exitCode = await main(["--json"], captured.io);
expect(exitCode).toBe(0);
expect(captured.readStderr()).toBe("");
expect(JSON.parse(captured.readStdout())).toEqual([]);
});
});