mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 00:40:21 +00:00
refactor: add metadata-first channel configured-state probes
This commit is contained in:
44
src/channels/plugins/configured-state.test.ts
Normal file
44
src/channels/plugins/configured-state.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
hasBundledChannelConfiguredState,
|
||||
listBundledChannelIdsWithConfiguredState,
|
||||
} from "./configured-state.js";
|
||||
|
||||
describe("bundled channel configured-state metadata", () => {
|
||||
it("lists the shipped metadata-first configured-state channels", () => {
|
||||
expect(listBundledChannelIdsWithConfiguredState()).toEqual(
|
||||
expect.arrayContaining(["discord", "irc", "slack", "telegram"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves Discord, Slack, Telegram, and IRC env probes without full plugin loads", () => {
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
channelId: "discord",
|
||||
cfg: {},
|
||||
env: { DISCORD_BOT_TOKEN: "token" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
channelId: "slack",
|
||||
cfg: {},
|
||||
env: { SLACK_BOT_TOKEN: "xoxb-test" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
channelId: "telegram",
|
||||
cfg: {},
|
||||
env: { TELEGRAM_BOT_TOKEN: "token" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
channelId: "irc",
|
||||
cfg: {},
|
||||
env: { IRC_HOST: "irc.example.com", IRC_NICK: "openclaw" },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
22
src/channels/plugins/configured-state.ts
Normal file
22
src/channels/plugins/configured-state.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
hasBundledChannelPackageState,
|
||||
listBundledChannelIdsForPackageState,
|
||||
} from "./package-state-probes.js";
|
||||
|
||||
export function listBundledChannelIdsWithConfiguredState(): string[] {
|
||||
return listBundledChannelIdsForPackageState("configuredState");
|
||||
}
|
||||
|
||||
export function hasBundledChannelConfiguredState(params: {
|
||||
channelId: string;
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
return hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: params.channelId,
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
});
|
||||
}
|
||||
211
src/channels/plugins/package-state-probes.ts
Normal file
211
src/channels/plugins/package-state-probes.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { createJiti } from "jiti";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { openBoundaryFileSync } from "../../infra/boundary-file-read.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import {
|
||||
listChannelCatalogEntries,
|
||||
type PluginChannelCatalogEntry,
|
||||
} from "../../plugins/channel-catalog-registry.js";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
buildPluginLoaderJitiOptions,
|
||||
shouldPreferNativeJiti,
|
||||
} from "../../plugins/sdk-alias.js";
|
||||
|
||||
type ChannelPackageStateChecker = (params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => boolean;
|
||||
|
||||
type ChannelPackageStateMetadata = {
|
||||
specifier?: string;
|
||||
exportName?: string;
|
||||
};
|
||||
|
||||
export type ChannelPackageStateMetadataKey = "configuredState" | "persistedAuthState";
|
||||
|
||||
type ChannelPackageStateRegistry = {
|
||||
catalog: PluginChannelCatalogEntry[];
|
||||
entriesById: Map<string, PluginChannelCatalogEntry>;
|
||||
checkerCache: Map<string, ChannelPackageStateChecker | null>;
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("channels");
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
const registryCache = new Map<ChannelPackageStateMetadataKey, ChannelPackageStateRegistry>();
|
||||
|
||||
function resolveChannelPackageStateMetadata(
|
||||
entry: PluginChannelCatalogEntry,
|
||||
metadataKey: ChannelPackageStateMetadataKey,
|
||||
): ChannelPackageStateMetadata | null {
|
||||
const metadata = entry.channel[metadataKey];
|
||||
if (!metadata || typeof metadata !== "object") {
|
||||
return null;
|
||||
}
|
||||
const specifier = typeof metadata.specifier === "string" ? metadata.specifier.trim() : "";
|
||||
const exportName = typeof metadata.exportName === "string" ? metadata.exportName.trim() : "";
|
||||
if (!specifier || !exportName) {
|
||||
return null;
|
||||
}
|
||||
return { specifier, exportName };
|
||||
}
|
||||
|
||||
function createModuleLoader() {
|
||||
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
|
||||
return (modulePath: string) => {
|
||||
const tryNative =
|
||||
shouldPreferNativeJiti(modulePath) || modulePath.includes(`${path.sep}dist${path.sep}`);
|
||||
const aliasMap = buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url);
|
||||
const cacheKey = JSON.stringify({
|
||||
tryNative,
|
||||
aliasMap: Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
});
|
||||
const cached = jitiLoaders.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const loader = createJiti(import.meta.url, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
tryNative,
|
||||
});
|
||||
jitiLoaders.set(cacheKey, loader);
|
||||
return loader;
|
||||
};
|
||||
}
|
||||
|
||||
const loadModule = createModuleLoader();
|
||||
|
||||
function getChannelPackageStateRegistry(
|
||||
metadataKey: ChannelPackageStateMetadataKey,
|
||||
): ChannelPackageStateRegistry {
|
||||
const cached = registryCache.get(metadataKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const catalog = listChannelCatalogEntries({ origin: "bundled" }).filter((entry) =>
|
||||
Boolean(resolveChannelPackageStateMetadata(entry, metadataKey)),
|
||||
);
|
||||
const registry = {
|
||||
catalog,
|
||||
entriesById: new Map(catalog.map((entry) => [entry.pluginId, entry] as const)),
|
||||
checkerCache: new Map(),
|
||||
} satisfies ChannelPackageStateRegistry;
|
||||
registryCache.set(metadataKey, registry);
|
||||
return registry;
|
||||
}
|
||||
|
||||
function resolveModuleCandidates(entry: PluginChannelCatalogEntry, specifier: string): string[] {
|
||||
const normalizedSpecifier = specifier.replace(/\\/g, "/");
|
||||
const resolvedPath = path.resolve(entry.rootDir, normalizedSpecifier);
|
||||
const ext = path.extname(resolvedPath);
|
||||
if (ext) {
|
||||
return [resolvedPath];
|
||||
}
|
||||
return [
|
||||
resolvedPath,
|
||||
`${resolvedPath}.ts`,
|
||||
`${resolvedPath}.js`,
|
||||
`${resolvedPath}.mjs`,
|
||||
`${resolvedPath}.cjs`,
|
||||
];
|
||||
}
|
||||
|
||||
function resolveExistingModulePath(entry: PluginChannelCatalogEntry, specifier: string): string {
|
||||
for (const candidate of resolveModuleCandidates(entry, specifier)) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return path.resolve(entry.rootDir, specifier);
|
||||
}
|
||||
|
||||
function loadChannelPackageStateModule(modulePath: string, rootDir: string): unknown {
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: modulePath,
|
||||
rootPath: rootDir,
|
||||
boundaryLabel: "plugin root",
|
||||
rejectHardlinks: false,
|
||||
skipLexicalRootCheck: true,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
throw new Error("plugin package-state module escapes plugin root or fails alias checks");
|
||||
}
|
||||
const safePath = opened.path;
|
||||
fs.closeSync(opened.fd);
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
[".js", ".mjs", ".cjs"].includes(path.extname(safePath).toLowerCase())
|
||||
) {
|
||||
try {
|
||||
return nodeRequire(safePath);
|
||||
} catch {
|
||||
// Fall back to Jiti when native require cannot load the target.
|
||||
}
|
||||
}
|
||||
return loadModule(safePath)(safePath);
|
||||
}
|
||||
|
||||
function resolveChannelPackageStateChecker(params: {
|
||||
entry: PluginChannelCatalogEntry;
|
||||
metadataKey: ChannelPackageStateMetadataKey;
|
||||
}): ChannelPackageStateChecker | null {
|
||||
const registry = getChannelPackageStateRegistry(params.metadataKey);
|
||||
const cached = registry.checkerCache.get(params.entry.pluginId);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const metadata = resolveChannelPackageStateMetadata(params.entry, params.metadataKey);
|
||||
if (!metadata) {
|
||||
registry.checkerCache.set(params.entry.pluginId, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const moduleExport = loadChannelPackageStateModule(
|
||||
resolveExistingModulePath(params.entry, metadata.specifier!),
|
||||
params.entry.rootDir,
|
||||
) as Record<string, unknown>;
|
||||
const checker = moduleExport[metadata.exportName!] as ChannelPackageStateChecker | undefined;
|
||||
if (typeof checker !== "function") {
|
||||
throw new Error(`missing ${params.metadataKey} export ${metadata.exportName}`);
|
||||
}
|
||||
registry.checkerCache.set(params.entry.pluginId, checker);
|
||||
return checker;
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
log.warn(
|
||||
`[channels] failed to load ${params.metadataKey} checker for ${params.entry.pluginId}: ${detail}`,
|
||||
);
|
||||
registry.checkerCache.set(params.entry.pluginId, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function listBundledChannelIdsForPackageState(
|
||||
metadataKey: ChannelPackageStateMetadataKey,
|
||||
): string[] {
|
||||
return getChannelPackageStateRegistry(metadataKey).catalog.map((entry) => entry.pluginId);
|
||||
}
|
||||
|
||||
export function hasBundledChannelPackageState(params: {
|
||||
metadataKey: ChannelPackageStateMetadataKey;
|
||||
channelId: string;
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const registry = getChannelPackageStateRegistry(params.metadataKey);
|
||||
const entry = registry.entriesById.get(params.channelId);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
const checker = resolveChannelPackageStateChecker({
|
||||
entry,
|
||||
metadataKey: params.metadataKey,
|
||||
});
|
||||
return checker ? Boolean(checker({ cfg: params.cfg, env: params.env })) : false;
|
||||
}
|
||||
21
src/channels/plugins/persisted-auth-state.test.ts
Normal file
21
src/channels/plugins/persisted-auth-state.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
hasBundledChannelPersistedAuthState,
|
||||
listBundledChannelIdsWithPersistedAuthState,
|
||||
} from "./persisted-auth-state.js";
|
||||
|
||||
describe("bundled channel persisted-auth metadata", () => {
|
||||
it("lists shipped persisted-auth metadata channels", () => {
|
||||
expect(listBundledChannelIdsWithPersistedAuthState()).toContain("whatsapp");
|
||||
});
|
||||
|
||||
it("does not report auth state for channels without bundled metadata", () => {
|
||||
expect(
|
||||
hasBundledChannelPersistedAuthState({
|
||||
channelId: "discord",
|
||||
cfg: {},
|
||||
env: {},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,167 +1,11 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { createJiti } from "jiti";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { openBoundaryFileSync } from "../../infra/boundary-file-read.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import {
|
||||
listChannelCatalogEntries,
|
||||
type PluginChannelCatalogEntry,
|
||||
} from "../../plugins/channel-catalog-registry.js";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
buildPluginLoaderJitiOptions,
|
||||
shouldPreferNativeJiti,
|
||||
} from "../../plugins/sdk-alias.js";
|
||||
|
||||
type PersistedAuthStateChecker = (params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => boolean;
|
||||
|
||||
type PersistedAuthStateMetadata = {
|
||||
specifier?: string;
|
||||
exportName?: string;
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("channels");
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
const persistedAuthStateCatalog = listChannelCatalogEntries({ origin: "bundled" }).filter((entry) =>
|
||||
Boolean(resolvePersistedAuthStateMetadata(entry)),
|
||||
);
|
||||
const persistedAuthStateEntriesById = new Map(
|
||||
persistedAuthStateCatalog.map((entry) => [entry.pluginId, entry] as const),
|
||||
);
|
||||
const persistedAuthStateCheckerCache = new Map<string, PersistedAuthStateChecker | null>();
|
||||
|
||||
function resolvePersistedAuthStateMetadata(
|
||||
entry: PluginChannelCatalogEntry,
|
||||
): PersistedAuthStateMetadata | null {
|
||||
const metadata = entry.channel.persistedAuthState;
|
||||
if (!metadata || typeof metadata !== "object") {
|
||||
return null;
|
||||
}
|
||||
const specifier = typeof metadata.specifier === "string" ? metadata.specifier.trim() : "";
|
||||
const exportName = typeof metadata.exportName === "string" ? metadata.exportName.trim() : "";
|
||||
if (!specifier || !exportName) {
|
||||
return null;
|
||||
}
|
||||
return { specifier, exportName };
|
||||
}
|
||||
|
||||
function createModuleLoader() {
|
||||
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
|
||||
return (modulePath: string) => {
|
||||
const tryNative =
|
||||
shouldPreferNativeJiti(modulePath) || modulePath.includes(`${path.sep}dist${path.sep}`);
|
||||
const aliasMap = buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url);
|
||||
const cacheKey = JSON.stringify({
|
||||
tryNative,
|
||||
aliasMap: Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
});
|
||||
const cached = jitiLoaders.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const loader = createJiti(import.meta.url, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
tryNative,
|
||||
});
|
||||
jitiLoaders.set(cacheKey, loader);
|
||||
return loader;
|
||||
};
|
||||
}
|
||||
|
||||
const loadModule = createModuleLoader();
|
||||
|
||||
function resolveModuleCandidates(entry: PluginChannelCatalogEntry, specifier: string): string[] {
|
||||
const normalizedSpecifier = specifier.replace(/\\/g, "/");
|
||||
const resolvedPath = path.resolve(entry.rootDir, normalizedSpecifier);
|
||||
const ext = path.extname(resolvedPath);
|
||||
if (ext) {
|
||||
return [resolvedPath];
|
||||
}
|
||||
return [
|
||||
resolvedPath,
|
||||
`${resolvedPath}.ts`,
|
||||
`${resolvedPath}.js`,
|
||||
`${resolvedPath}.mjs`,
|
||||
`${resolvedPath}.cjs`,
|
||||
];
|
||||
}
|
||||
|
||||
function resolveExistingModulePath(entry: PluginChannelCatalogEntry, specifier: string): string {
|
||||
for (const candidate of resolveModuleCandidates(entry, specifier)) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return path.resolve(entry.rootDir, specifier);
|
||||
}
|
||||
|
||||
function loadPersistedAuthStateModule(modulePath: string, rootDir: string): unknown {
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: modulePath,
|
||||
rootPath: rootDir,
|
||||
boundaryLabel: "plugin root",
|
||||
rejectHardlinks: false,
|
||||
skipLexicalRootCheck: true,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
throw new Error("plugin persisted-auth module escapes plugin root or fails alias checks");
|
||||
}
|
||||
const safePath = opened.path;
|
||||
fs.closeSync(opened.fd);
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
[".js", ".mjs", ".cjs"].includes(path.extname(safePath).toLowerCase())
|
||||
) {
|
||||
try {
|
||||
return nodeRequire(safePath);
|
||||
} catch {
|
||||
// Fall back to Jiti when native require cannot load the target.
|
||||
}
|
||||
}
|
||||
return loadModule(safePath)(safePath);
|
||||
}
|
||||
|
||||
function resolvePersistedAuthStateChecker(
|
||||
entry: PluginChannelCatalogEntry,
|
||||
): PersistedAuthStateChecker | null {
|
||||
const cached = persistedAuthStateCheckerCache.get(entry.pluginId);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const metadata = resolvePersistedAuthStateMetadata(entry);
|
||||
if (!metadata) {
|
||||
persistedAuthStateCheckerCache.set(entry.pluginId, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const moduleExport = loadPersistedAuthStateModule(
|
||||
resolveExistingModulePath(entry, metadata.specifier!),
|
||||
entry.rootDir,
|
||||
) as Record<string, unknown>;
|
||||
const checker = moduleExport[metadata.exportName!] as PersistedAuthStateChecker | undefined;
|
||||
if (typeof checker !== "function") {
|
||||
throw new Error(`missing persisted auth export ${metadata.exportName}`);
|
||||
}
|
||||
persistedAuthStateCheckerCache.set(entry.pluginId, checker);
|
||||
return checker;
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
log.warn(`[channels] failed to load persisted auth checker for ${entry.pluginId}: ${detail}`);
|
||||
persistedAuthStateCheckerCache.set(entry.pluginId, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
hasBundledChannelPackageState,
|
||||
listBundledChannelIdsForPackageState,
|
||||
} from "./package-state-probes.js";
|
||||
|
||||
export function listBundledChannelIdsWithPersistedAuthState(): string[] {
|
||||
return persistedAuthStateCatalog.map((entry) => entry.pluginId);
|
||||
return listBundledChannelIdsForPackageState("persistedAuthState");
|
||||
}
|
||||
|
||||
export function hasBundledChannelPersistedAuthState(params: {
|
||||
@@ -169,10 +13,10 @@ export function hasBundledChannelPersistedAuthState(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const entry = persistedAuthStateEntriesById.get(params.channelId);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
const checker = resolvePersistedAuthStateChecker(entry);
|
||||
return checker ? Boolean(checker({ cfg: params.cfg, env: params.env })) : false;
|
||||
return hasBundledChannelPackageState({
|
||||
metadataKey: "persistedAuthState",
|
||||
channelId: params.channelId,
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@ import { describe, expect, it } from "vitest";
|
||||
import { isChannelConfigured } from "./channel-configured.js";
|
||||
|
||||
describe("isChannelConfigured", () => {
|
||||
it("detects Telegram env configuration through the channel plugin seam", () => {
|
||||
it("detects Telegram env configuration through the package metadata seam", () => {
|
||||
expect(isChannelConfigured({}, "telegram", { TELEGRAM_BOT_TOKEN: "token" })).toBe(true);
|
||||
});
|
||||
|
||||
it("detects Discord env configuration through the channel plugin seam", () => {
|
||||
it("detects Discord env configuration through the package metadata seam", () => {
|
||||
expect(isChannelConfigured({}, "discord", { DISCORD_BOT_TOKEN: "token" })).toBe(true);
|
||||
});
|
||||
|
||||
it("detects Slack env configuration through the channel plugin seam", () => {
|
||||
it("detects Slack env configuration through the package metadata seam", () => {
|
||||
expect(isChannelConfigured({}, "slack", { SLACK_BOT_TOKEN: "xoxb-test" })).toBe(true);
|
||||
});
|
||||
|
||||
it("requires both IRC host and nick env vars through the channel plugin seam", () => {
|
||||
it("requires both IRC host and nick env vars through the package metadata seam", () => {
|
||||
expect(isChannelConfigured({}, "irc", { IRC_HOST: "irc.example.com" })).toBe(false);
|
||||
expect(
|
||||
isChannelConfigured({}, "irc", {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { hasMeaningfulChannelConfig } from "../channels/config-presence.js";
|
||||
import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { hasBundledChannelConfiguredState } from "../channels/plugins/configured-state.js";
|
||||
import { hasBundledChannelPersistedAuthState } from "../channels/plugins/persisted-auth-state.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
@@ -23,14 +24,16 @@ export function isChannelConfigured(
|
||||
channelId: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
const plugin = getBootstrapChannelPlugin(channelId);
|
||||
const pluginConfigured = plugin?.config?.hasConfiguredState?.({ cfg, env });
|
||||
if (pluginConfigured) {
|
||||
if (hasBundledChannelConfiguredState({ channelId, cfg, env })) {
|
||||
return true;
|
||||
}
|
||||
const pluginPersistedAuthState = hasBundledChannelPersistedAuthState({ channelId, cfg, env });
|
||||
if (pluginPersistedAuthState) {
|
||||
return true;
|
||||
}
|
||||
return isGenericChannelConfigured(cfg, channelId);
|
||||
if (isGenericChannelConfigured(cfg, channelId)) {
|
||||
return true;
|
||||
}
|
||||
const plugin = getBootstrapChannelPlugin(channelId);
|
||||
return Boolean(plugin?.config?.hasConfiguredState?.({ cfg, env }));
|
||||
}
|
||||
|
||||
@@ -118,6 +118,12 @@ describe("plugin activation boundary", () => {
|
||||
it("does not load bundled plugins for config and env detection helpers", async () => {
|
||||
const { isChannelConfigured, resolveEnvApiKey } = await importConfigHelpers();
|
||||
|
||||
expect(isChannelConfigured({}, "telegram", { TELEGRAM_BOT_TOKEN: "token" })).toBe(true);
|
||||
expect(isChannelConfigured({}, "discord", { DISCORD_BOT_TOKEN: "token" })).toBe(true);
|
||||
expect(isChannelConfigured({}, "slack", { SLACK_BOT_TOKEN: "xoxb-test" })).toBe(true);
|
||||
expect(
|
||||
isChannelConfigured({}, "irc", { IRC_HOST: "irc.example.com", IRC_NICK: "openclaw" }),
|
||||
).toBe(true);
|
||||
expect(isChannelConfigured({}, "whatsapp", {})).toBe(false);
|
||||
expect(
|
||||
resolveEnvApiKey("anthropic-vertex", {
|
||||
|
||||
@@ -107,6 +107,45 @@ describe("bundled plugin metadata", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps bundled configured-state metadata on channel package manifests", () => {
|
||||
const configuredChannels = listBundledPluginMetadata()
|
||||
.filter((entry) => ["discord", "irc", "slack", "telegram"].includes(entry.dirName))
|
||||
.map((entry) => ({
|
||||
dir: entry.dirName,
|
||||
configuredState: entry.packageManifest?.channel?.configuredState,
|
||||
}));
|
||||
expect(configuredChannels).toEqual([
|
||||
{
|
||||
dir: "discord",
|
||||
configuredState: {
|
||||
specifier: "./configured-state",
|
||||
exportName: "hasDiscordConfiguredState",
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: "irc",
|
||||
configuredState: {
|
||||
specifier: "./configured-state",
|
||||
exportName: "hasIrcConfiguredState",
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: "slack",
|
||||
configuredState: {
|
||||
specifier: "./configured-state",
|
||||
exportName: "hasSlackConfiguredState",
|
||||
},
|
||||
},
|
||||
{
|
||||
dir: "telegram",
|
||||
configuredState: {
|
||||
specifier: "./configured-state",
|
||||
exportName: "hasTelegramConfiguredState",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("excludes test-only public surface artifacts", () => {
|
||||
listBundledPluginMetadata().forEach((entry) =>
|
||||
expectTestOnlyArtifactsExcluded(entry.publicSurfaceArtifacts ?? []),
|
||||
|
||||
@@ -438,6 +438,10 @@ export type PluginPackageChannel = {
|
||||
quickstartAllowFrom?: boolean;
|
||||
forceAccountBinding?: boolean;
|
||||
preferSessionLookupForAnnounceTarget?: boolean;
|
||||
configuredState?: {
|
||||
specifier?: string;
|
||||
exportName?: string;
|
||||
};
|
||||
persistedAuthState?: {
|
||||
specifier?: string;
|
||||
exportName?: string;
|
||||
|
||||
Reference in New Issue
Block a user