test(ci): reduce channel contract import cost

This commit is contained in:
Peter Steinberger
2026-04-21 00:39:21 +01:00
parent 92191d37e6
commit 982b1c9464
33 changed files with 127 additions and 60 deletions

View File

@@ -0,0 +1 @@
export { feishuPlugin } from "./src/channel.js";

View File

@@ -67,7 +67,7 @@ export default defineBundledChannelEntry({
description: "Feishu/Lark channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "feishuPlugin",
},
secrets: {

View File

@@ -49,7 +49,6 @@ import {
DEFAULT_ACCOUNT_ID,
PAIRING_APPROVED_MESSAGE,
} from "./channel-runtime-api.js";
import { createFeishuClient } from "./client.js";
import { isRecord } from "./comment-shared.js";
import { FeishuConfigSchema } from "./config-schema.js";
import {
@@ -119,6 +118,11 @@ const loadFeishuChannelRuntime = createLazyRuntimeNamedExport(
"feishuChannelRuntime",
);
async function createFeishuActionClient(account: ResolvedFeishuAccount) {
const { createFeishuClient } = await import("./client.js");
return createFeishuClient(account);
}
const collectFeishuSecurityWarnings = createAllowlistProviderGroupPolicyWarningCollector<{
cfg: ClawdbotConfig;
accountId?: string | null;
@@ -841,7 +845,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
throw new Error("Feishu channel-info requires chatId or channelId.");
}
const runtime = await loadFeishuChannelRuntime();
const client = createFeishuClient(account);
const client = await createFeishuActionClient(account);
const channel = await runtime.getChatInfo(client, chatId);
const includeMembers =
ctx.params.includeMembers === true || ctx.params.members === true;
@@ -871,7 +875,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
if (ctx.action === "member-info") {
const runtime = await loadFeishuChannelRuntime();
const client = createFeishuClient(account);
const client = await createFeishuActionClient(account);
const memberId = resolveFeishuMemberId(ctx.params);
if (memberId) {
const member = await runtime.getFeishuMemberInfo(

View File

@@ -13,15 +13,7 @@ import {
type SecretInput,
} from "openclaw/plugin-sdk/setup";
import { inspectFeishuCredentials, resolveDefaultFeishuAccountId } from "./accounts.js";
import {
beginAppRegistration,
getAppOwnerOpenId,
initAppRegistration,
pollAppRegistration,
printQrCode,
type AppRegistrationResult,
} from "./app-registration.js";
import { probeFeishu } from "./probe.js";
import type { AppRegistrationResult } from "./app-registration.js";
import type { FeishuConfig, FeishuDomain } from "./types.js";
const channel = "feishu" as const;
@@ -254,6 +246,8 @@ function applyNewAppSecurityPolicy(
// ---------------------------------------------------------------------------
async function runScanToCreate(prompter: WizardPrompter): Promise<AppRegistrationResult | null> {
const { beginAppRegistration, initAppRegistration, pollAppRegistration, printQrCode } =
await import("./app-registration.js");
try {
await initAppRegistration("feishu");
} catch {
@@ -371,6 +365,7 @@ async function runNewAppFlow(params: {
// Fetch openId via API for manual flow.
if (appId && appSecretProbeValue) {
const { getAppOwnerOpenId } = await import("./app-registration.js");
scanOpenId = await getAppOwnerOpenId({
appId,
appSecret: appSecretProbeValue,
@@ -528,6 +523,7 @@ export const feishuSetupWizard: ChannelSetupWizard = {
let probeResult = null;
if (configured && resolvedCredentials) {
try {
const { probeFeishu } = await import("./probe.js");
probeResult = await probeFeishu(resolvedCredentials);
} catch {}
}

View File

@@ -0,0 +1 @@
export { googlechatPlugin } from "./src/channel.js";

View File

@@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
description: "OpenClaw Google Chat channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "googlechatPlugin",
},
secrets: {

View File

@@ -0,0 +1 @@
export { imessagePlugin } from "./src/channel.js";

View File

@@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
description: "iMessage channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "imessagePlugin",
},
runtime: {

View File

@@ -0,0 +1 @@
export { linePlugin } from "./src/channel.js";

View File

@@ -31,7 +31,7 @@ export default defineBundledChannelEntry({
description: "LINE Messaging API channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "linePlugin",
},
runtime: {

View File

@@ -0,0 +1 @@
export { msteamsPlugin } from "./src/channel.js";

View File

@@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
description: "Microsoft Teams channel plugin (Bot Framework)",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "msteamsPlugin",
},
secrets: {

View File

@@ -0,0 +1 @@
export { nextcloudTalkPlugin } from "./src/channel.js";

View File

@@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
description: "Nextcloud Talk channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "nextcloudTalkPlugin",
},
secrets: {

View File

@@ -0,0 +1 @@
export { nostrPlugin } from "./src/channel.js";

View File

@@ -35,7 +35,7 @@ export default defineBundledChannelEntry({
description: "Nostr DM channel plugin via NIP-04",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "nostrPlugin",
},
runtime: {

View File

@@ -0,0 +1 @@
export { qaChannelPlugin } from "./src/channel.js";

View File

@@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
description: "Synthetic QA channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "qaChannelPlugin",
},
runtime: {

View File

@@ -0,0 +1 @@
export { qqbotPlugin } from "./src/channel.js";

View File

@@ -95,7 +95,7 @@ export default defineBundledChannelEntry({
description: "QQ Bot channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "qqbotPlugin",
},
runtime: {

View File

@@ -0,0 +1 @@
export { synologyChatPlugin } from "./src/channel.js";

View File

@@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
description: "Native Synology Chat channel plugin for OpenClaw",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "synologyChatPlugin",
},
runtime: {

View File

@@ -0,0 +1 @@
export { tlonPlugin } from "./src/channel.js";

View File

@@ -119,7 +119,7 @@ export default defineBundledChannelEntry({
description: "Tlon/Urbit channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "tlonPlugin",
},
runtime: {

View File

@@ -0,0 +1 @@
export { twitchPlugin } from "./src/plugin.js";

View File

@@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
description: "Twitch IRC chat channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "twitchPlugin",
},
runtime: {

View File

@@ -0,0 +1 @@
export { zaloPlugin } from "./src/channel.js";

View File

@@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
description: "Zalo channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "zaloPlugin",
},
secrets: {

View File

@@ -1,11 +1,12 @@
import fs from "node:fs/promises";
import path from "node:path";
import { expect } from "vitest";
import { ensureAuthProfileStore, type AuthProfileStore } from "../agents/auth-profiles.js";
import type { AuthProfileStore } from "../agents/auth-profiles.js";
import { loadConfig } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginOrigin } from "../plugins/plugin-origin.types.js";
import type { captureEnv } from "../test-utils/env.js";
import { getActiveSecretsRuntimeSnapshot } from "./runtime.js";
export const OPENAI_ENV_KEY_REF = {
source: "env",
@@ -102,7 +103,10 @@ export function createOpenAIFileRuntimeConfig(secretFile: string): OpenClawConfi
export function expectResolvedOpenAIRuntime(agentDir: string) {
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
const activeAuthStore = getActiveSecretsRuntimeSnapshot()?.authStores.find(
(entry) => entry.agentDir === agentDir,
)?.store;
expect(activeAuthStore?.profiles["openai:default"]).toMatchObject({
type: "api_key",
key: "sk-file-runtime",
});

View File

@@ -0,0 +1,55 @@
import { listBundledChannelPluginIds as listCatalogBundledChannelPluginIds } from "../../../src/channels/plugins/bundled-ids.js";
import type { ChannelId } from "../../../src/channels/plugins/channel-id.types.js";
import type { ChannelPlugin } from "../../../src/channels/plugins/types.js";
import {
listChannelCatalogEntries,
type PluginChannelCatalogEntry,
} from "../../../src/plugins/channel-catalog-registry.js";
import { loadBundledPluginPublicSurfaceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
type ChannelPluginApiModule = Record<string, unknown>;
const channelPluginCache = new Map<ChannelId, ChannelPlugin | null>();
let channelCatalogEntries: PluginChannelCatalogEntry[] | undefined;
function isChannelPlugin(value: unknown): value is ChannelPlugin {
return (
Boolean(value) &&
typeof value === "object" &&
typeof (value as Partial<ChannelPlugin>).id === "string" &&
Boolean((value as Partial<ChannelPlugin>).meta) &&
Boolean((value as Partial<ChannelPlugin>).config)
);
}
export function listBundledChannelPluginIds(): readonly ChannelId[] {
return listCatalogBundledChannelPluginIds() as ChannelId[];
}
export function getBundledChannelCatalogEntry(
id: ChannelId,
): PluginChannelCatalogEntry | undefined {
channelCatalogEntries ??= listChannelCatalogEntries({ origin: "bundled" });
return channelCatalogEntries.find((entry) => entry.pluginId === id || entry.channel.id === id);
}
export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
if (channelPluginCache.has(id)) {
return channelPluginCache.get(id) ?? undefined;
}
const loaded = loadBundledPluginPublicSurfaceSync<ChannelPluginApiModule>({
pluginId: id,
artifactBasename: "channel-plugin-api.js",
});
const plugin = Object.values(loaded).find(isChannelPlugin) ?? null;
channelPluginCache.set(id, plugin);
return plugin ?? undefined;
}
export function listBundledChannelPlugins(): readonly ChannelPlugin[] {
return listBundledChannelPluginIds().flatMap((id) => {
const plugin = getBundledChannelPlugin(id);
return plugin ? [plugin] : [];
});
}

View File

@@ -1,11 +1,12 @@
import {
getBundledChannelPlugin,
listBundledChannelPluginIds,
listBundledChannelPlugins,
} from "../../../src/channels/plugins/bundled.js";
import type { ChannelId } from "../../../src/channels/plugins/channel-id.types.js";
import { normalizeChannelMeta } from "../../../src/channels/plugins/meta-normalization.js";
import type { ChannelPlugin } from "../../../src/channels/plugins/types.js";
import {
getBundledChannelCatalogEntry,
getBundledChannelPlugin,
listBundledChannelPluginIds,
listBundledChannelPlugins,
} from "./bundled-channel-plugin-loader.js";
type PluginContractEntry = {
id: string;
@@ -13,11 +14,12 @@ type PluginContractEntry = {
};
function toPluginContractEntry(plugin: ChannelPlugin): PluginContractEntry {
const existingMeta = getBundledChannelCatalogEntry(plugin.id)?.channel;
return {
id: plugin.id,
plugin: {
...plugin,
meta: normalizeChannelMeta({ id: plugin.id, meta: plugin.meta }),
meta: normalizeChannelMeta({ id: plugin.id, meta: plugin.meta, existing: existingMeta }),
},
};
}

View File

@@ -1,17 +1,11 @@
import {
getBundledChannelPlugin,
listBundledChannelPluginIds,
listBundledChannelPlugins,
setBundledChannelRuntime,
} from "../../../src/channels/plugins/bundled.js";
import type { ChannelId } from "../../../src/channels/plugins/channel-id.types.js";
import type { ChannelPlugin } from "../../../src/channels/plugins/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import {
listLineAccountIds,
resolveDefaultLineAccountId,
resolveLineAccount,
} from "../../../src/plugin-sdk/line.js";
getBundledChannelPlugin,
listBundledChannelPluginIds,
listBundledChannelPlugins,
} from "./bundled-channel-plugin-loader.js";
import { channelPluginSurfaceKeys, type ChannelPluginSurface } from "./manifest.js";
type SurfaceContractEntry = {
@@ -44,17 +38,6 @@ type DirectoryContractEntry = {
accountId?: string;
};
setBundledChannelRuntime("line", {
channel: {
line: {
listLineAccountIds,
resolveDefaultLineAccountId,
resolveLineAccount: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) =>
resolveLineAccount({ cfg, accountId }),
},
},
} as never);
let surfaceContractRegistryCache: SurfaceContractEntry[] | undefined;
const surfaceContractEntryCache = new Map<ChannelId, SurfaceContractEntry | null>();
let threadingContractRegistryCache: ThreadingContractEntry[] | undefined;

View File

@@ -7,9 +7,18 @@ import type {
} from "../../../src/channels/plugins/types.core.js";
import type { ChannelPlugin } from "../../../src/channels/plugins/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { createNonExitingRuntime } from "../../../src/runtime.js";
import type { RuntimeEnv } from "../../../src/runtime.js";
const contractRuntime = createNonExitingRuntime();
let contractRuntime: RuntimeEnv | undefined;
async function getDirectoryContractRuntime(): Promise<RuntimeEnv> {
if (contractRuntime) {
return contractRuntime;
}
const { createNonExitingRuntime } = await import("../../../src/runtime.js");
contractRuntime = createNonExitingRuntime();
return contractRuntime;
}
function expectDirectoryEntryShape(entry: ChannelDirectoryEntry) {
expect(["user", "group", "channel"]).toContain(entry.kind);
@@ -177,10 +186,11 @@ export function installChannelDirectoryContractSuite(params: {
if (params.coverage === "presence") {
return;
}
const runtime = await getDirectoryContractRuntime();
const self = await directory?.self?.({
cfg: params.cfg ?? ({} as OpenClawConfig),
accountId: params.accountId ?? "default",
runtime: contractRuntime,
runtime,
});
if (self) {
expectDirectoryEntryShape(self);
@@ -192,7 +202,7 @@ export function installChannelDirectoryContractSuite(params: {
accountId: params.accountId ?? "default",
query: "",
limit: 5,
runtime: contractRuntime,
runtime,
})) ?? [];
expect(Array.isArray(peers)).toBe(true);
for (const peer of peers) {
@@ -205,7 +215,7 @@ export function installChannelDirectoryContractSuite(params: {
accountId: params.accountId ?? "default",
query: "",
limit: 5,
runtime: contractRuntime,
runtime,
})) ?? [];
expect(Array.isArray(groups)).toBe(true);
for (const group of groups) {
@@ -218,7 +228,7 @@ export function installChannelDirectoryContractSuite(params: {
accountId: params.accountId ?? "default",
groupId: groups[0].id,
limit: 5,
runtime: contractRuntime,
runtime,
});
expect(Array.isArray(members)).toBe(true);
for (const member of members) {