mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
test(boundary): route helper imports through bundled plugin surfaces
This commit is contained in:
134
scripts/check-test-helper-extension-import-boundary.mjs
Normal file
134
scripts/check-test-helper-extension-import-boundary.mjs
Normal 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);
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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, "/");
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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")>(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
28
test/test-helper-extension-import-boundary.test.ts
Normal file
28
test/test-helper-extension-import-boundary.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user