test: trim import-heavy startup paths

This commit is contained in:
Peter Steinberger
2026-03-22 00:52:03 +00:00
parent 8b7f40580d
commit d0d82ea67b
9 changed files with 367 additions and 268 deletions

View File

@@ -35,7 +35,7 @@ const baselinePathByMode = {
),
};
const inventoryPromiseByMode = new Map();
let allInventoryByModePromise;
let parsedExtensionSourceFilesPromise;
const ruleTextByMode = {
@@ -193,30 +193,42 @@ function shouldReport(mode, resolvedPath) {
return !resolvedPath.startsWith("src/plugin-sdk/");
}
function collectFromSourceFile(mode, sourceFile, filePath) {
const entries = [];
function collectEntriesByModeFromSourceFile(sourceFile, filePath) {
const entriesByMode = {
"src-outside-plugin-sdk": [],
"plugin-sdk-internal": [],
"relative-outside-package": [],
};
const extensionRoot = resolveExtensionRoot(filePath);
function push(kind, specifierNode, specifier) {
const resolvedPath = resolveSpecifier(specifier, filePath);
if (mode === "relative-outside-package") {
if (!specifier.startsWith(".") || !resolvedPath || !extensionRoot) {
return;
}
if (resolvedPath === extensionRoot || resolvedPath.startsWith(`${extensionRoot}/`)) {
return;
}
} else if (!shouldReport(mode, resolvedPath)) {
return;
}
entries.push({
const baseEntry = {
file: normalizePath(filePath),
line: toLine(sourceFile, specifierNode),
kind,
specifier,
resolvedPath,
reason: classifyReason(mode, kind, resolvedPath, specifier),
});
};
if (specifier.startsWith(".") && resolvedPath && extensionRoot) {
if (!(resolvedPath === extensionRoot || resolvedPath.startsWith(`${extensionRoot}/`))) {
entriesByMode["relative-outside-package"].push({
...baseEntry,
reason: classifyReason("relative-outside-package", kind, resolvedPath, specifier),
});
}
}
for (const mode of ["src-outside-plugin-sdk", "plugin-sdk-internal"]) {
if (!shouldReport(mode, resolvedPath)) {
continue;
}
entriesByMode[mode].push({
...baseEntry,
reason: classifyReason(mode, kind, resolvedPath, specifier),
});
}
}
function visit(node) {
@@ -240,26 +252,35 @@ function collectFromSourceFile(mode, sourceFile, filePath) {
}
visit(sourceFile);
return entries;
return entriesByMode;
}
export async function collectExtensionPluginSdkBoundaryInventory(mode) {
if (!MODES.has(mode)) {
throw new Error(`Unknown mode: ${mode}`);
}
let pending = inventoryPromiseByMode.get(mode);
if (!pending) {
pending = (async () => {
if (!allInventoryByModePromise) {
allInventoryByModePromise = (async () => {
const files = await collectParsedExtensionSourceFiles();
const inventory = [];
const inventoryByMode = {
"src-outside-plugin-sdk": [],
"plugin-sdk-internal": [],
"relative-outside-package": [],
};
for (const { filePath, sourceFile } of files) {
inventory.push(...collectFromSourceFile(mode, sourceFile, filePath));
const entriesByMode = collectEntriesByModeFromSourceFile(sourceFile, filePath);
for (const inventoryMode of MODES) {
inventoryByMode[inventoryMode].push(...entriesByMode[inventoryMode]);
}
}
return inventory.toSorted(compareEntries);
for (const inventoryMode of MODES) {
inventoryByMode[inventoryMode] = inventoryByMode[inventoryMode].toSorted(compareEntries);
}
return inventoryByMode;
})();
inventoryPromiseByMode.set(mode, pending);
}
return await pending;
const inventoryByMode = await allInventoryByModePromise;
return inventoryByMode[mode];
}
export async function readExpectedInventory(mode) {

View File

@@ -60,6 +60,8 @@ const ignoredFiles = new Set([
"src/secrets/runtime-web-tools.test.ts",
]);
let webSearchProviderInventoryPromise;
function normalizeRelativePath(filePath) {
return path.relative(repoRoot, filePath).split(path.sep).join("/");
}
@@ -185,32 +187,37 @@ function scanGenericCoreImports(lines, relativeFile, inventory) {
}
export async function collectWebSearchProviderBoundaryInventory() {
const inventory = [];
const files = (
await Promise.all(scanRoots.map(async (root) => await walkFiles(path.join(repoRoot, root))))
)
.flat()
.toSorted((left, right) =>
normalizeRelativePath(left).localeCompare(normalizeRelativePath(right)),
);
if (!webSearchProviderInventoryPromise) {
webSearchProviderInventoryPromise = (async () => {
const inventory = [];
const files = (
await Promise.all(scanRoots.map(async (root) => await walkFiles(path.join(repoRoot, root))))
)
.flat()
.toSorted((left, right) =>
normalizeRelativePath(left).localeCompare(normalizeRelativePath(right)),
);
for (const filePath of files) {
const relativeFile = normalizeRelativePath(filePath);
if (ignoredFiles.has(relativeFile) || relativeFile.includes(".test.")) {
continue;
}
const content = await fs.readFile(filePath, "utf8");
const lines = content.split(/\r?\n/);
for (const filePath of files) {
const relativeFile = normalizeRelativePath(filePath);
if (ignoredFiles.has(relativeFile) || relativeFile.includes(".test.")) {
continue;
}
const content = await fs.readFile(filePath, "utf8");
const lines = content.split(/\r?\n/);
if (relativeFile === "src/plugins/web-search-providers.ts") {
scanWebSearchProviderRegistry(lines, relativeFile, inventory);
continue;
}
if (relativeFile === "src/plugins/web-search-providers.ts") {
scanWebSearchProviderRegistry(lines, relativeFile, inventory);
continue;
}
scanGenericCoreImports(lines, relativeFile, inventory);
scanGenericCoreImports(lines, relativeFile, inventory);
}
return inventory.toSorted(compareInventoryEntries);
})();
}
return inventory.toSorted(compareInventoryEntries);
return await webSearchProviderInventoryPromise;
}
export async function readExpectedInventory() {

View File

@@ -12,8 +12,8 @@ import {
listChatChannels,
} from "../channels/registry.js";
import { formatCliCommand } from "../cli/command-format.js";
import { isChannelConfigured } from "../config/channel-configured.js";
import type { OpenClawConfig } from "../config/config.js";
import { isChannelConfigured } from "../config/plugin-auto-enable.js";
import type { DmPolicy } from "../config/types.js";
import { enablePluginInConfig } from "../plugins/enable.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";

View File

@@ -0,0 +1,159 @@
import { hasAnyWhatsAppAuth } from "../../extensions/whatsapp/auth-presence.js";
import { hasMeaningfulChannelConfig } from "../channels/config-presence.js";
import { isRecord } from "../utils.js";
import type { OpenClawConfig } from "./config.js";
function hasNonEmptyString(value: unknown): boolean {
return typeof value === "string" && value.trim().length > 0;
}
function accountsHaveKeys(value: unknown, keys: readonly string[]): boolean {
if (!isRecord(value)) {
return false;
}
for (const account of Object.values(value)) {
if (!isRecord(account)) {
continue;
}
for (const key of keys) {
if (hasNonEmptyString(account[key])) {
return true;
}
}
}
return false;
}
function resolveChannelConfig(
cfg: OpenClawConfig,
channelId: string,
): Record<string, unknown> | null {
const channels = cfg.channels as Record<string, unknown> | undefined;
const entry = channels?.[channelId];
return isRecord(entry) ? entry : null;
}
type StructuredChannelConfigSpec = {
envAny?: readonly string[];
envAll?: readonly string[];
stringKeys?: readonly string[];
numberKeys?: readonly string[];
accountStringKeys?: readonly string[];
};
const STRUCTURED_CHANNEL_CONFIG_SPECS: Record<string, StructuredChannelConfigSpec> = {
telegram: {
envAny: ["TELEGRAM_BOT_TOKEN"],
stringKeys: ["botToken", "tokenFile"],
accountStringKeys: ["botToken", "tokenFile"],
},
discord: {
envAny: ["DISCORD_BOT_TOKEN"],
stringKeys: ["token"],
accountStringKeys: ["token"],
},
irc: {
envAll: ["IRC_HOST", "IRC_NICK"],
stringKeys: ["host", "nick"],
accountStringKeys: ["host", "nick"],
},
slack: {
envAny: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_TOKEN"],
stringKeys: ["botToken", "appToken", "userToken"],
accountStringKeys: ["botToken", "appToken", "userToken"],
},
signal: {
stringKeys: ["account", "httpUrl", "httpHost", "cliPath"],
numberKeys: ["httpPort"],
accountStringKeys: ["account", "httpUrl", "httpHost", "cliPath"],
},
imessage: {
stringKeys: ["cliPath"],
},
};
function envHasAnyKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean {
for (const key of keys) {
if (hasNonEmptyString(env[key])) {
return true;
}
}
return false;
}
function envHasAllKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean {
for (const key of keys) {
if (!hasNonEmptyString(env[key])) {
return false;
}
}
return keys.length > 0;
}
function hasAnyNumberKeys(entry: Record<string, unknown>, keys: readonly string[]): boolean {
for (const key of keys) {
if (typeof entry[key] === "number") {
return true;
}
}
return false;
}
function isStructuredChannelConfigured(
cfg: OpenClawConfig,
channelId: string,
env: NodeJS.ProcessEnv,
spec: StructuredChannelConfigSpec,
): boolean {
if (spec.envAny && envHasAnyKeys(env, spec.envAny)) {
return true;
}
if (spec.envAll && envHasAllKeys(env, spec.envAll)) {
return true;
}
const entry = resolveChannelConfig(cfg, channelId);
if (!entry) {
return false;
}
if (spec.stringKeys && spec.stringKeys.some((key) => hasNonEmptyString(entry[key]))) {
return true;
}
if (spec.numberKeys && hasAnyNumberKeys(entry, spec.numberKeys)) {
return true;
}
if (spec.accountStringKeys && accountsHaveKeys(entry.accounts, spec.accountStringKeys)) {
return true;
}
return hasMeaningfulChannelConfig(entry);
}
function isWhatsAppConfigured(cfg: OpenClawConfig): boolean {
if (hasAnyWhatsAppAuth(cfg)) {
return true;
}
const entry = resolveChannelConfig(cfg, "whatsapp");
if (!entry) {
return false;
}
return hasMeaningfulChannelConfig(entry);
}
function isGenericChannelConfigured(cfg: OpenClawConfig, channelId: string): boolean {
const entry = resolveChannelConfig(cfg, channelId);
return hasMeaningfulChannelConfig(entry);
}
export function isChannelConfigured(
cfg: OpenClawConfig,
channelId: string,
env: NodeJS.ProcessEnv = process.env,
): boolean {
if (channelId === "whatsapp") {
return isWhatsAppConfigured(cfg);
}
const spec = STRUCTURED_CHANNEL_CONFIG_SPECS[channelId];
if (spec) {
return isStructuredChannelConfigured(cfg, channelId, env, spec);
}
return isGenericChannelConfigured(cfg, channelId);
}

View File

@@ -1,10 +1,6 @@
import { hasAnyWhatsAppAuth } from "../../extensions/whatsapp/auth-presence.js";
import fs from "node:fs";
import path from "node:path";
import { normalizeProviderId } from "../agents/model-selection.js";
import { hasMeaningfulChannelConfig } from "../channels/config-presence.js";
import {
getChannelPluginCatalogEntry,
listChannelPluginCatalogEntries,
} from "../channels/plugins/catalog.js";
import {
getChatChannelMeta,
listChatChannels,
@@ -14,7 +10,8 @@ import {
loadPluginManifestRegistry,
type PluginManifestRegistry,
} from "../plugins/manifest-registry.js";
import { isRecord } from "../utils.js";
import { isRecord, resolveConfigDir, resolveUserPath } from "../utils.js";
import { isChannelConfigured } from "./channel-configured.js";
import type { OpenClawConfig } from "./config.js";
import { ensurePluginAllowlisted } from "./plugins-allowlist.js";
@@ -39,161 +36,7 @@ const PROVIDER_PLUGIN_IDS: Array<{ pluginId: string; providerId: string }> = [
{ pluginId: "copilot-proxy", providerId: "copilot-proxy" },
{ pluginId: "minimax", providerId: "minimax-portal" },
];
function hasNonEmptyString(value: unknown): boolean {
return typeof value === "string" && value.trim().length > 0;
}
function accountsHaveKeys(value: unknown, keys: readonly string[]): boolean {
if (!isRecord(value)) {
return false;
}
for (const account of Object.values(value)) {
if (!isRecord(account)) {
continue;
}
for (const key of keys) {
if (hasNonEmptyString(account[key])) {
return true;
}
}
}
return false;
}
function resolveChannelConfig(
cfg: OpenClawConfig,
channelId: string,
): Record<string, unknown> | null {
const channels = cfg.channels as Record<string, unknown> | undefined;
const entry = channels?.[channelId];
return isRecord(entry) ? entry : null;
}
type StructuredChannelConfigSpec = {
envAny?: readonly string[];
envAll?: readonly string[];
stringKeys?: readonly string[];
numberKeys?: readonly string[];
accountStringKeys?: readonly string[];
};
const STRUCTURED_CHANNEL_CONFIG_SPECS: Record<string, StructuredChannelConfigSpec> = {
telegram: {
envAny: ["TELEGRAM_BOT_TOKEN"],
stringKeys: ["botToken", "tokenFile"],
accountStringKeys: ["botToken", "tokenFile"],
},
discord: {
envAny: ["DISCORD_BOT_TOKEN"],
stringKeys: ["token"],
accountStringKeys: ["token"],
},
irc: {
envAll: ["IRC_HOST", "IRC_NICK"],
stringKeys: ["host", "nick"],
accountStringKeys: ["host", "nick"],
},
slack: {
envAny: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_TOKEN"],
stringKeys: ["botToken", "appToken", "userToken"],
accountStringKeys: ["botToken", "appToken", "userToken"],
},
signal: {
stringKeys: ["account", "httpUrl", "httpHost", "cliPath"],
numberKeys: ["httpPort"],
accountStringKeys: ["account", "httpUrl", "httpHost", "cliPath"],
},
imessage: {
stringKeys: ["cliPath"],
},
};
function envHasAnyKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean {
for (const key of keys) {
if (hasNonEmptyString(env[key])) {
return true;
}
}
return false;
}
function envHasAllKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean {
for (const key of keys) {
if (!hasNonEmptyString(env[key])) {
return false;
}
}
return keys.length > 0;
}
function hasAnyNumberKeys(entry: Record<string, unknown>, keys: readonly string[]): boolean {
for (const key of keys) {
if (typeof entry[key] === "number") {
return true;
}
}
return false;
}
function isStructuredChannelConfigured(
cfg: OpenClawConfig,
channelId: string,
env: NodeJS.ProcessEnv,
spec: StructuredChannelConfigSpec,
): boolean {
if (spec.envAny && envHasAnyKeys(env, spec.envAny)) {
return true;
}
if (spec.envAll && envHasAllKeys(env, spec.envAll)) {
return true;
}
const entry = resolveChannelConfig(cfg, channelId);
if (!entry) {
return false;
}
if (spec.stringKeys && spec.stringKeys.some((key) => hasNonEmptyString(entry[key]))) {
return true;
}
if (spec.numberKeys && hasAnyNumberKeys(entry, spec.numberKeys)) {
return true;
}
if (spec.accountStringKeys && accountsHaveKeys(entry.accounts, spec.accountStringKeys)) {
return true;
}
return hasMeaningfulChannelConfig(entry);
}
function isWhatsAppConfigured(cfg: OpenClawConfig): boolean {
if (hasAnyWhatsAppAuth(cfg)) {
return true;
}
const entry = resolveChannelConfig(cfg, "whatsapp");
if (!entry) {
return false;
}
return hasMeaningfulChannelConfig(entry);
}
function isGenericChannelConfigured(cfg: OpenClawConfig, channelId: string): boolean {
const entry = resolveChannelConfig(cfg, channelId);
return hasMeaningfulChannelConfig(entry);
}
export function isChannelConfigured(
cfg: OpenClawConfig,
channelId: string,
env: NodeJS.ProcessEnv = process.env,
): boolean {
if (channelId === "whatsapp") {
return isWhatsAppConfigured(cfg);
}
const spec = STRUCTURED_CHANNEL_CONFIG_SPECS[channelId];
if (spec) {
return isStructuredChannelConfigured(cfg, channelId, env, spec);
}
return isGenericChannelConfigured(cfg, channelId);
}
const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALOG_PATHS"];
function collectModelRefs(cfg: OpenClawConfig): string[] {
const refs: string[] = [];
@@ -297,6 +140,89 @@ function buildChannelToPluginIdMap(registry: PluginManifestRegistry): Map<string
return map;
}
type ExternalCatalogChannelEntry = {
id: string;
preferOver: string[];
};
function splitEnvPaths(value: string): string[] {
const trimmed = value.trim();
if (!trimmed) {
return [];
}
return trimmed
.split(/[;,]/g)
.flatMap((chunk) => chunk.split(path.delimiter))
.map((entry) => entry.trim())
.filter(Boolean);
}
function resolveExternalCatalogPaths(env: NodeJS.ProcessEnv): string[] {
for (const key of ENV_CATALOG_PATHS) {
const raw = env[key];
if (raw && raw.trim()) {
return splitEnvPaths(raw);
}
}
const configDir = resolveConfigDir(env);
return [
path.join(configDir, "mpm", "plugins.json"),
path.join(configDir, "mpm", "catalog.json"),
path.join(configDir, "plugins", "catalog.json"),
];
}
function parseExternalCatalogChannelEntries(raw: unknown): ExternalCatalogChannelEntry[] {
const list = (() => {
if (Array.isArray(raw)) {
return raw;
}
if (!isRecord(raw)) {
return [];
}
const entries = raw.entries ?? raw.packages ?? raw.plugins;
return Array.isArray(entries) ? entries : [];
})();
const channels: ExternalCatalogChannelEntry[] = [];
for (const entry of list) {
if (!isRecord(entry) || !isRecord(entry.openclaw) || !isRecord(entry.openclaw.channel)) {
continue;
}
const channel = entry.openclaw.channel;
const id = typeof channel.id === "string" ? channel.id.trim() : "";
if (!id) {
continue;
}
const preferOver = Array.isArray(channel.preferOver)
? channel.preferOver.filter((value): value is string => typeof value === "string")
: [];
channels.push({ id, preferOver });
}
return channels;
}
function resolveExternalCatalogPreferOver(channelId: string, env: NodeJS.ProcessEnv): string[] {
for (const rawPath of resolveExternalCatalogPaths(env)) {
const resolved = resolveUserPath(rawPath, env);
if (!fs.existsSync(resolved)) {
continue;
}
try {
const payload = JSON.parse(fs.readFileSync(resolved, "utf-8")) as unknown;
const channel = parseExternalCatalogChannelEntries(payload).find(
(entry) => entry.id === channelId,
);
if (channel) {
return channel.preferOver;
}
} catch {
// Ignore invalid catalog files.
}
}
return [];
}
function resolvePluginIdForChannel(
channelId: string,
channelToPluginId: ReadonlyMap<string, string>,
@@ -310,17 +236,12 @@ function resolvePluginIdForChannel(
return channelToPluginId.get(channelId) ?? channelId;
}
function listKnownChannelPluginIds(env: NodeJS.ProcessEnv): string[] {
return Array.from(
new Set([
...listChatChannels().map((meta) => meta.id),
...listChannelPluginCatalogEntries({ env }).map((entry) => entry.id),
]),
);
function listKnownChannelPluginIds(): string[] {
return listChatChannels().map((meta) => meta.id);
}
function collectCandidateChannelIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] {
const channelIds = new Set<string>(listKnownChannelPluginIds(env));
function collectCandidateChannelIds(cfg: OpenClawConfig): string[] {
const channelIds = new Set<string>(listKnownChannelPluginIds());
const configuredChannels = cfg.channels as Record<string, unknown> | undefined;
if (!configuredChannels || typeof configuredChannels !== "object") {
return Array.from(channelIds);
@@ -359,7 +280,7 @@ function resolveConfiguredPlugins(
const changes: PluginEnableChange[] = [];
// Build reverse map: channel ID → plugin ID from installed plugin manifests.
const channelToPluginId = buildChannelToPluginIdMap(registry);
for (const channelId of collectCandidateChannelIds(cfg, env)) {
for (const channelId of collectCandidateChannelIds(cfg)) {
const pluginId = resolvePluginIdForChannel(channelId, channelToPluginId);
if (isChannelConfigured(cfg, channelId, env)) {
changes.push({ pluginId, reason: `${channelId} configured` });
@@ -410,13 +331,22 @@ function isPluginDenied(cfg: OpenClawConfig, pluginId: string): boolean {
return Array.isArray(deny) && deny.includes(pluginId);
}
function resolvePreferredOverIds(pluginId: string, env: NodeJS.ProcessEnv): string[] {
function resolvePreferredOverIds(
pluginId: string,
env: NodeJS.ProcessEnv,
registry: PluginManifestRegistry,
): string[] {
const normalized = normalizeChatChannelId(pluginId);
if (normalized) {
return getChatChannelMeta(normalized).preferOver ?? [];
}
const catalogEntry = getChannelPluginCatalogEntry(pluginId, { env });
return catalogEntry?.meta.preferOver ?? [];
const installedChannelMeta = registry.plugins.find(
(record) => record.id === pluginId,
)?.channelCatalogMeta;
if (installedChannelMeta?.preferOver?.length) {
return installedChannelMeta.preferOver;
}
return resolveExternalCatalogPreferOver(pluginId, env);
}
function shouldSkipPreferredPluginAutoEnable(
@@ -424,6 +354,7 @@ function shouldSkipPreferredPluginAutoEnable(
entry: PluginEnableChange,
configured: PluginEnableChange[],
env: NodeJS.ProcessEnv,
registry: PluginManifestRegistry,
): boolean {
for (const other of configured) {
if (other.pluginId === entry.pluginId) {
@@ -435,7 +366,7 @@ function shouldSkipPreferredPluginAutoEnable(
if (isPluginExplicitlyDisabled(cfg, other.pluginId)) {
continue;
}
const preferOver = resolvePreferredOverIds(other.pluginId, env);
const preferOver = resolvePreferredOverIds(other.pluginId, env, registry);
if (preferOver.includes(entry.pluginId)) {
return true;
}
@@ -523,7 +454,7 @@ export function applyPluginAutoEnable(params: {
if (isPluginExplicitlyDisabled(next, entry.pluginId)) {
continue;
}
if (shouldSkipPreferredPluginAutoEnable(next, entry, configured, env)) {
if (shouldSkipPreferredPluginAutoEnable(next, entry, configured, env, registry)) {
continue;
}
const allow = next.plugins?.allow;

View File

@@ -4,7 +4,6 @@ import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import { writeWorkspaceFile } from "../../../test-helpers/workspace.js";
import type { HookHandler } from "../../hooks.js";
import { createHookEvent } from "../../hooks.js";
// Avoid calling the embedded Pi agent (global command lane); keep this unit test deterministic.
@@ -12,7 +11,7 @@ vi.mock("../../llm-slug-generator.js", () => ({
generateSlugViaLLM: vi.fn().mockResolvedValue("simple-math"),
}));
let handler: HookHandler;
let handler: typeof import("./handler.js").default;
let suiteWorkspaceRoot = "";
let workspaceCaseCounter = 0;

View File

@@ -1,52 +1,20 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { afterAll, afterEach, describe, expect, it } from "vitest";
import { emitDiagnosticEvent, resetDiagnosticEventsForTest } from "../infra/diagnostic-events.js";
import { buildMemoryPromptSection, registerMemoryPromptSection } from "../memory/prompt-section.js";
import { withEnv } from "../test-utils/env.js";
import { clearPluginCommands, getPluginCommandSpecs } from "./commands.js";
async function importFreshPluginTestModules() {
vi.resetModules();
vi.doUnmock("node:fs");
vi.doUnmock("node:fs/promises");
vi.doUnmock("node:module");
vi.doUnmock("./hook-runner-global.js");
vi.doUnmock("./hooks.js");
vi.doUnmock("./loader.js");
vi.doUnmock("jiti");
const [loader, hookRunnerGlobal, hooks, runtime, registry, promptSection] = await Promise.all([
import("./loader.js"),
import("./hook-runner-global.js"),
import("./hooks.js"),
import("./runtime.js"),
import("./registry.js"),
import("../memory/prompt-section.js"),
]);
return {
...loader,
...hookRunnerGlobal,
...hooks,
...runtime,
...registry,
...promptSection,
};
}
const {
__testing,
buildMemoryPromptSection,
clearPluginLoaderCache,
createHookRunner,
createEmptyPluginRegistry,
import { getGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js";
import { createHookRunner } from "./hooks.js";
import { __testing, clearPluginLoaderCache, loadOpenClawPlugins } from "./loader.js";
import { createEmptyPluginRegistry } from "./registry.js";
import {
getActivePluginRegistry,
getActivePluginRegistryKey,
getGlobalHookRunner,
loadOpenClawPlugins,
registerMemoryPromptSection,
resetGlobalHookRunner,
setActivePluginRegistry,
} = await importFreshPluginTestModules();
} from "./runtime.js";
type TempPlugin = { dir: string; file: string; id: string };
type PluginLoadConfig = NonNullable<Parameters<typeof loadOpenClawPlugins>[0]>["config"];

View File

@@ -2,8 +2,8 @@ import fs from "node:fs";
import path from "node:path";
import { createJiti } from "jiti";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import { isChannelConfigured } from "../config/channel-configured.js";
import type { OpenClawConfig } from "../config/config.js";
import { isChannelConfigured } from "../config/plugin-auto-enable.js";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";

View File

@@ -57,6 +57,10 @@ export type PluginManifestRecord = {
schemaCacheKey?: string;
configSchema?: Record<string, unknown>;
configUiHints?: Record<string, PluginConfigUiHint>;
channelCatalogMeta?: {
id: string;
preferOver?: string[];
};
};
export type PluginManifestRegistry = {
@@ -178,6 +182,16 @@ function buildRecord(params: {
schemaCacheKey: params.schemaCacheKey,
configSchema: params.configSchema,
configUiHints: params.manifest.uiHints,
...(params.candidate.packageManifest?.channel?.id
? {
channelCatalogMeta: {
id: params.candidate.packageManifest.channel.id,
...(params.candidate.packageManifest.channel.preferOver
? { preferOver: params.candidate.packageManifest.channel.preferOver }
: {}),
},
}
: {}),
};
}