mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:50:43 +00:00
fix: stabilize plugin discovery and session message tests
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
import { z, type ZodType } from "zod";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import {
|
||||
resolveSingleAccountKeysToMove,
|
||||
resolveSingleAccountPromotionTarget,
|
||||
} from "./setup-promotion-helpers.js";
|
||||
import type { ChannelSetupAdapter } from "./types.adapters.js";
|
||||
import type { ChannelSetupInput } from "./types.core.js";
|
||||
|
||||
@@ -14,6 +10,81 @@ type ChannelSectionBase = {
|
||||
accounts?: Record<string, Record<string, unknown>>;
|
||||
};
|
||||
|
||||
const COMMON_SINGLE_ACCOUNT_KEYS_TO_MOVE = new Set([
|
||||
"name",
|
||||
"token",
|
||||
"tokenFile",
|
||||
"botToken",
|
||||
"appToken",
|
||||
"account",
|
||||
"signalNumber",
|
||||
"authDir",
|
||||
"cliPath",
|
||||
"dbPath",
|
||||
"httpUrl",
|
||||
"httpHost",
|
||||
"httpPort",
|
||||
"webhookPath",
|
||||
"webhookUrl",
|
||||
"webhookSecret",
|
||||
"service",
|
||||
"region",
|
||||
"homeserver",
|
||||
"userId",
|
||||
"accessToken",
|
||||
"password",
|
||||
"deviceName",
|
||||
"url",
|
||||
"code",
|
||||
"dmPolicy",
|
||||
"allowFrom",
|
||||
"groupPolicy",
|
||||
"groupAllowFrom",
|
||||
"defaultTo",
|
||||
"streaming",
|
||||
"deviceId",
|
||||
"avatarUrl",
|
||||
"initialSyncLimit",
|
||||
"encryption",
|
||||
"allowlistOnly",
|
||||
"allowBots",
|
||||
"blockStreaming",
|
||||
"replyToMode",
|
||||
"threadReplies",
|
||||
"textChunkLimit",
|
||||
"chunkMode",
|
||||
"responsePrefix",
|
||||
"ackReaction",
|
||||
"ackReactionScope",
|
||||
"reactionNotifications",
|
||||
"threadBindings",
|
||||
"startupVerification",
|
||||
"startupVerificationCooldownHours",
|
||||
"mediaMaxMb",
|
||||
"autoJoin",
|
||||
"autoJoinAllowlist",
|
||||
"dm",
|
||||
"groups",
|
||||
"rooms",
|
||||
"actions",
|
||||
]);
|
||||
|
||||
const NAMED_ACCOUNT_PROMOTION_KEYS_BY_CHANNEL: Record<string, readonly string[]> = {
|
||||
matrix: [
|
||||
"name",
|
||||
"homeserver",
|
||||
"userId",
|
||||
"accessToken",
|
||||
"password",
|
||||
"deviceId",
|
||||
"deviceName",
|
||||
"avatarUrl",
|
||||
"initialSyncLimit",
|
||||
"encryption",
|
||||
],
|
||||
telegram: ["botToken", "tokenFile"],
|
||||
};
|
||||
|
||||
function channelHasAccounts(cfg: OpenClawConfig, channelKey: string): boolean {
|
||||
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||
const base = channels?.[channelKey] as ChannelSectionBase | undefined;
|
||||
@@ -427,6 +498,46 @@ function resolveExistingAccountKey(
|
||||
return targetAccountId;
|
||||
}
|
||||
|
||||
function resolveSingleAccountKeysToMove(params: {
|
||||
channelKey: string;
|
||||
channel: Record<string, unknown>;
|
||||
}): string[] {
|
||||
const hasNamedAccounts = Object.keys(
|
||||
(params.channel.accounts as Record<string, unknown>) ?? {},
|
||||
).some(Boolean);
|
||||
const entries = Object.entries(params.channel)
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
key !== "accounts" && key !== "defaultAccount" && key !== "enabled" && value !== undefined,
|
||||
)
|
||||
.map(([key]) => key);
|
||||
const keysToMove = entries.filter((key) => COMMON_SINGLE_ACCOUNT_KEYS_TO_MOVE.has(key));
|
||||
if (!hasNamedAccounts || keysToMove.length === 0) {
|
||||
return keysToMove;
|
||||
}
|
||||
const namedAccountPromotionKeys = NAMED_ACCOUNT_PROMOTION_KEYS_BY_CHANNEL[params.channelKey];
|
||||
return namedAccountPromotionKeys
|
||||
? keysToMove.filter((key) => namedAccountPromotionKeys.includes(key))
|
||||
: keysToMove;
|
||||
}
|
||||
|
||||
function resolveSingleAccountPromotionTarget(params: { channel: ChannelSectionBase }): string {
|
||||
const accounts = params.channel.accounts ?? {};
|
||||
const normalizedDefaultAccount =
|
||||
typeof params.channel.defaultAccount === "string" && params.channel.defaultAccount.trim()
|
||||
? normalizeAccountId(params.channel.defaultAccount)
|
||||
: undefined;
|
||||
if (normalizedDefaultAccount) {
|
||||
return (
|
||||
Object.keys(accounts).find(
|
||||
(accountId) => normalizeAccountId(accountId) === normalizedDefaultAccount,
|
||||
) ?? DEFAULT_ACCOUNT_ID
|
||||
);
|
||||
}
|
||||
const namedAccounts = Object.keys(accounts).filter(Boolean);
|
||||
return namedAccounts.length === 1 ? namedAccounts[0] : DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
// When promoting a single-account channel config to multi-account,
|
||||
// move top-level account settings into accounts.default so the original
|
||||
// account keeps working without duplicate account values at channel root.
|
||||
@@ -453,7 +564,6 @@ export function moveSingleAccountChannelSectionToDefaultAccount(params: {
|
||||
}
|
||||
|
||||
const targetAccountId = resolveSingleAccountPromotionTarget({
|
||||
channelKey: params.channelKey,
|
||||
channel: base,
|
||||
});
|
||||
const resolvedTargetAccountKey = resolveExistingAccountKey(accounts, targetAccountId);
|
||||
|
||||
@@ -35,6 +35,14 @@ vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: mockLoadPluginManifestRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/plugin-registry.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../plugins/plugin-registry.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadPluginManifestRegistryForPluginRegistry: mockLoadPluginManifestRegistry,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../plugins/doctor-contract-registry.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../plugins/doctor-contract-registry.js")>();
|
||||
return {
|
||||
@@ -804,7 +812,7 @@ describe("config io write", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves parsed source config when snapshot validation throws", async () => {
|
||||
it("preserves parsed source config when snapshot validation fails", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
@@ -814,10 +822,6 @@ describe("config io write", () => {
|
||||
};
|
||||
const originalRaw = `${JSON.stringify(original, null, 2)}\n`;
|
||||
await fs.writeFile(configPath, originalRaw, "utf-8");
|
||||
mockLoadPluginManifestRegistry.mockImplementationOnce(() => {
|
||||
throw new Error("manifest registry unavailable");
|
||||
});
|
||||
|
||||
const io = createFastConfigIO(home);
|
||||
|
||||
const snapshot = await io.readConfigFileSnapshot();
|
||||
@@ -827,7 +831,7 @@ describe("config io write", () => {
|
||||
expect(snapshot.parsed).toEqual(original);
|
||||
expect(snapshot.sourceConfig).toEqual(original);
|
||||
expect(snapshot.config).toEqual(original);
|
||||
expect(snapshot.issues[0]?.message).toContain("manifest registry unavailable");
|
||||
expect(snapshot.issues[0]?.message).toContain("unknown channel id: test-plugin-channel");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ describe("normalizePluginsConfig", () => {
|
||||
expect(result.entries.minimax?.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it("reuses the plugin alias discovery during one config normalization", async () => {
|
||||
it("normalizes unknown plugin ids without loading discovery", async () => {
|
||||
vi.resetModules();
|
||||
const discovery = await import("./discovery.js");
|
||||
const discoverPlugins = vi.spyOn(discovery, "discoverOpenClawPlugins");
|
||||
@@ -176,10 +176,10 @@ describe("normalizePluginsConfig", () => {
|
||||
expect(result.allow).toEqual(["unknown-plugin-one", "unknown-plugin-two"]);
|
||||
expect(result.deny).toEqual(["unknown-plugin-three"]);
|
||||
expect(result.entries["unknown-plugin-four"]?.enabled).toBe(true);
|
||||
expect(discoverPlugins).toHaveBeenCalledTimes(1);
|
||||
expect(discoverPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps alias lookup limited to bundled plugin manifests", async () => {
|
||||
it("does not load discovery or manifests for alias lookup", async () => {
|
||||
vi.resetModules();
|
||||
const discovery = await import("./discovery.js");
|
||||
const manifest = await import("./manifest.js");
|
||||
@@ -224,7 +224,7 @@ describe("normalizePluginsConfig", () => {
|
||||
});
|
||||
|
||||
expect(result.deny).toEqual(["anthropic"]);
|
||||
expect(discoverPlugins).toHaveBeenCalledTimes(1);
|
||||
expect(discoverPlugins).not.toHaveBeenCalled();
|
||||
expect(loadManifest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
type NormalizePluginId,
|
||||
type NormalizedPluginsConfig as SharedNormalizedPluginsConfig,
|
||||
} from "./config-normalization-shared.js";
|
||||
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||
import { loadPluginManifest } from "./manifest.js";
|
||||
import type { PluginOrigin } from "./plugin-origin.types.js";
|
||||
import { defaultSlotIdForKey } from "./slots.js";
|
||||
|
||||
@@ -48,34 +46,6 @@ const BUILT_IN_PLUGIN_ALIAS_LOOKUP = new Map<string, string>([
|
||||
|
||||
function getBundledPluginAliasLookup(): ReadonlyMap<string, string> {
|
||||
const lookup = new Map<string, string>();
|
||||
for (const candidate of discoverOpenClawPlugins({}).candidates) {
|
||||
if (candidate.origin !== "bundled") {
|
||||
continue;
|
||||
}
|
||||
const manifestResult = candidate.bundledManifest
|
||||
? { ok: true as const, manifest: candidate.bundledManifest }
|
||||
: loadPluginManifest(candidate.rootDir, false);
|
||||
if (!manifestResult.ok) {
|
||||
continue;
|
||||
}
|
||||
const manifest = manifestResult.manifest;
|
||||
const pluginId = normalizeOptionalLowercaseString(manifest.id);
|
||||
if (pluginId) {
|
||||
lookup.set(pluginId, manifest.id);
|
||||
}
|
||||
for (const providerId of manifest.providers ?? []) {
|
||||
const normalizedProviderId = normalizeOptionalLowercaseString(providerId);
|
||||
if (normalizedProviderId) {
|
||||
lookup.set(normalizedProviderId, manifest.id);
|
||||
}
|
||||
}
|
||||
for (const legacyPluginId of manifest.legacyPluginIds ?? []) {
|
||||
const normalizedLegacyPluginId = normalizeOptionalLowercaseString(legacyPluginId);
|
||||
if (normalizedLegacyPluginId) {
|
||||
lookup.set(normalizedLegacyPluginId, manifest.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [alias, pluginId] of BUILT_IN_PLUGIN_ALIAS_FALLBACKS) {
|
||||
lookup.set(alias, pluginId);
|
||||
}
|
||||
|
||||
@@ -1178,8 +1178,22 @@ function activatePluginRegistry(
|
||||
export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry {
|
||||
const requestedOnlyPluginIds = normalizePluginIdScope(options.onlyPluginIds);
|
||||
const requestedOnlyPluginIdSet = createPluginIdScopeSet(requestedOnlyPluginIds);
|
||||
if (options.activate === false && requestedOnlyPluginIdSet?.size === 0) {
|
||||
return createEmptyPluginRegistry();
|
||||
if (requestedOnlyPluginIdSet && requestedOnlyPluginIdSet.size === 0) {
|
||||
const emptyRegistry = createEmptyPluginRegistry();
|
||||
if (options.activate !== false) {
|
||||
clearAgentHarnesses();
|
||||
clearPluginCommands();
|
||||
clearPluginInteractiveHandlers();
|
||||
clearDetachedTaskLifecycleRuntimeRegistration();
|
||||
clearMemoryPluginState();
|
||||
activatePluginRegistry(
|
||||
emptyRegistry,
|
||||
`empty-plugin-scope::${resolveRuntimeSubagentMode(options.runtimeOptions)}::${options.workspaceDir ?? ""}`,
|
||||
resolveRuntimeSubagentMode(options.runtimeOptions),
|
||||
options.workspaceDir,
|
||||
);
|
||||
}
|
||||
return emptyRegistry;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -1203,19 +1217,6 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const validateOnly = options.mode === "validate";
|
||||
const onlyPluginIdSet = createPluginIdScopeSet(onlyPluginIds);
|
||||
|
||||
if (onlyPluginIdSet && onlyPluginIdSet.size === 0) {
|
||||
const emptyRegistry = createEmptyPluginRegistry();
|
||||
if (shouldActivate) {
|
||||
clearAgentHarnesses();
|
||||
clearPluginCommands();
|
||||
clearPluginInteractiveHandlers();
|
||||
clearDetachedTaskLifecycleRuntimeRegistration();
|
||||
clearMemoryPluginState();
|
||||
activatePluginRegistry(emptyRegistry, cacheKey, runtimeSubagentMode, options.workspaceDir);
|
||||
}
|
||||
return emptyRegistry;
|
||||
}
|
||||
|
||||
const cacheEnabled = options.cache !== false;
|
||||
if (cacheEnabled) {
|
||||
const cached = getCachedPluginRegistry(cacheKey);
|
||||
|
||||
Reference in New Issue
Block a user