From 4f76c0bd6bff573697123cd461e6f98a94f1a63c Mon Sep 17 00:00:00 2001 From: MaxxxDong <186893345+MaxxxDong@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:21:00 +0800 Subject: [PATCH] test: fix remaining CI regressions --- extensions/matrix/runtime-api.ts | 15 +++- extensions/whatsapp/src/test-helpers.ts | 90 +++++++++++-------- src/hooks/hooks-install.test.ts | 1 + src/hooks/workspace.ts | 18 +++- src/infra/net/ssrf.ts | 17 ++-- src/infra/outbound/channel-selection.ts | 4 +- .../message-action-runner.context.test.ts | 14 +-- src/infra/outbound/message-action-runner.ts | 17 ++-- src/plugins/bundle-mcp.ts | 44 +++++---- src/plugins/runtime/runtime-config.ts | 11 ++- 10 files changed, 151 insertions(+), 80 deletions(-) diff --git a/extensions/matrix/runtime-api.ts b/extensions/matrix/runtime-api.ts index 9d427c4ac8c..c11109030a8 100644 --- a/extensions/matrix/runtime-api.ts +++ b/extensions/matrix/runtime-api.ts @@ -1,3 +1,16 @@ +// Keep the external runtime API light so Jiti callers can resolve Matrix config +// helpers without traversing the full plugin-sdk/runtime graph. export * from "openclaw/plugin-sdk/matrix"; export * from "./src/auth-precedence.js"; -export * from "./helper-api.js"; +export { + findMatrixAccountEntry, + hashMatrixAccessToken, + listMatrixEnvAccountIds, + resolveConfiguredMatrixAccountIds, + resolveMatrixChannelConfig, + resolveMatrixCredentialsFilename, + resolveMatrixEnvAccountToken, + resolveMatrixHomeserverKey, + resolveMatrixLegacyFlatStoreRoot, + sanitizeMatrixPathSegment, +} from "./helper-api.js"; diff --git a/extensions/whatsapp/src/test-helpers.ts b/extensions/whatsapp/src/test-helpers.ts index 74c5f8c3584..b71f25f9d63 100644 --- a/extensions/whatsapp/src/test-helpers.ts +++ b/extensions/whatsapp/src/test-helpers.ts @@ -36,44 +36,64 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { const actual = await importOriginal(); const mockModule = Object.create(null) as Record; Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual)); - Object.defineProperty(mockModule, "loadConfig", { - configurable: true, - enumerable: true, - writable: true, - value: () => { - const getter = (globalThis as Record)[CONFIG_KEY]; - if (typeof getter === "function") { - return getter(); - } - return DEFAULT_CONFIG; + Object.defineProperties(mockModule, { + loadConfig: { + configurable: true, + enumerable: true, + writable: true, + value: () => { + const getter = (globalThis as Record)[CONFIG_KEY]; + if (typeof getter === "function") { + return getter(); + } + return DEFAULT_CONFIG; + }, }, - }); - Object.assign(mockModule, { - updateLastRoute: async (params: { - storePath: string; - sessionKey: string; - deliveryContext: { channel: string; to: string; accountId?: string }; - }) => { - const raw = await fs.readFile(params.storePath, "utf8").catch(() => "{}"); - const store = JSON.parse(raw) as Record>; - const current = store[params.sessionKey] ?? {}; - store[params.sessionKey] = { - ...current, - lastChannel: params.deliveryContext.channel, - lastTo: params.deliveryContext.to, - lastAccountId: params.deliveryContext.accountId, - }; - await fs.writeFile(params.storePath, JSON.stringify(store)); + updateLastRoute: { + configurable: true, + enumerable: true, + writable: true, + value: async (params: { + storePath: string; + sessionKey: string; + deliveryContext: { channel: string; to: string; accountId?: string }; + }) => { + const raw = await fs.readFile(params.storePath, "utf8").catch(() => "{}"); + const store = JSON.parse(raw) as Record>; + const current = store[params.sessionKey] ?? {}; + store[params.sessionKey] = { + ...current, + lastChannel: params.deliveryContext.channel, + lastTo: params.deliveryContext.to, + lastAccountId: params.deliveryContext.accountId, + }; + await fs.writeFile(params.storePath, JSON.stringify(store)); + }, }, - loadSessionStore: (storePath: string) => { - try { - return JSON.parse(fsSync.readFileSync(storePath, "utf8")) as Record; - } catch { - return {}; - } + loadSessionStore: { + configurable: true, + enumerable: true, + writable: true, + value: (storePath: string) => { + try { + return JSON.parse(fsSync.readFileSync(storePath, "utf8")) as Record; + } catch { + return {}; + } + }, + }, + recordSessionMetaFromInbound: { + configurable: true, + enumerable: true, + writable: true, + value: async () => undefined, + }, + resolveStorePath: { + configurable: true, + enumerable: true, + writable: true, + value: actual.resolveStorePath, }, - recordSessionMetaFromInbound: async () => undefined, - resolveStorePath: actual.resolveStorePath, }); return mockModule; }); diff --git a/src/hooks/hooks-install.test.ts b/src/hooks/hooks-install.test.ts index 98afa7319cc..002ff479508 100644 --- a/src/hooks/hooks-install.test.ts +++ b/src/hooks/hooks-install.test.ts @@ -49,6 +49,7 @@ describe("hooks install (e2e)", () => { { name: "@acme/hello-hooks", version: "0.0.0", + type: "module", openclaw: { hooks: ["./hooks/hello-hook"] }, }, null, diff --git a/src/hooks/workspace.ts b/src/hooks/workspace.ts index 7b86d9d23c8..d14a29a3d7b 100644 --- a/src/hooks/workspace.ts +++ b/src/hooks/workspace.ts @@ -122,14 +122,28 @@ function loadHookFromDir(params: { // keep the discovered path when realpath is unavailable } + let hookFilePath = hookMdPath; + try { + hookFilePath = fs.realpathSync.native(hookMdPath); + } catch { + hookFilePath = hookMdPath; + } + + let resolvedHandlerPath = handlerPath; + try { + resolvedHandlerPath = fs.realpathSync.native(handlerPath); + } catch { + resolvedHandlerPath = handlerPath; + } + return { name, description, source: params.source, pluginId: params.pluginId, - filePath: hookMdPath, + filePath: hookFilePath, baseDir, - handlerPath, + handlerPath: resolvedHandlerPath, }; } catch (err) { const message = err instanceof Error ? (err.stack ?? err.message) : String(err); diff --git a/src/infra/net/ssrf.ts b/src/infra/net/ssrf.ts index fd633fcb20d..ae0807e4bed 100644 --- a/src/infra/net/ssrf.ts +++ b/src/infra/net/ssrf.ts @@ -1,6 +1,7 @@ import { lookup as dnsLookupCb, type LookupAddress } from "node:dns"; import { lookup as dnsLookup } from "node:dns/promises"; -import { Agent, EnvHttpProxyAgent, ProxyAgent, type Dispatcher } from "undici"; +import * as undici from "undici"; +import type { Dispatcher } from "undici"; import { extractEmbeddedIpv4FromIpv6, isBlockedSpecialUseIpv4Address, @@ -403,13 +404,19 @@ export function createPinnedDispatcher( const lookup = resolvePinnedDispatcherLookup(pinned, policy?.pinnedHostname, ssrfPolicy); if (!policy || policy.mode === "direct") { - return new Agent({ + if (typeof undici.Agent !== "function") { + return { + close: async () => undefined, + destroy: () => undefined, + } as unknown as Dispatcher; + } + return new undici.Agent({ connect: withPinnedLookup(lookup, policy?.connect), }); } if (policy.mode === "env-proxy") { - return new EnvHttpProxyAgent({ + return new undici.EnvHttpProxyAgent({ connect: withPinnedLookup(lookup, policy.connect), ...(policy.proxyTls ? { proxyTls: { ...policy.proxyTls } } : {}), }); @@ -417,9 +424,9 @@ export function createPinnedDispatcher( const proxyUrl = policy.proxyUrl.trim(); if (!policy.proxyTls) { - return new ProxyAgent(proxyUrl); + return new undici.ProxyAgent(proxyUrl); } - return new ProxyAgent({ + return new undici.ProxyAgent({ uri: proxyUrl, proxyTls: { ...policy.proxyTls }, }); diff --git a/src/infra/outbound/channel-selection.ts b/src/infra/outbound/channel-selection.ts index 0e87a8e4950..386669d5649 100644 --- a/src/infra/outbound/channel-selection.ts +++ b/src/infra/outbound/channel-selection.ts @@ -165,7 +165,7 @@ export async function resolveMessageChannelSelection(params: { if (fallback) { return { channel: fallback, - configured: await listConfiguredMessageChannels(params.cfg), + configured: [], source: "tool-context-fallback", }; } @@ -176,7 +176,7 @@ export async function resolveMessageChannelSelection(params: { } return { channel: availableExplicit, - configured: await listConfiguredMessageChannels(params.cfg), + configured: [], source: "explicit", }; } diff --git a/src/infra/outbound/message-action-runner.context.test.ts b/src/infra/outbound/message-action-runner.context.test.ts index ed470984e45..39677e3220d 100644 --- a/src/infra/outbound/message-action-runner.context.test.ts +++ b/src/infra/outbound/message-action-runner.context.test.ts @@ -117,17 +117,17 @@ describe("runMessageAction context isolation", () => { cfg: slackConfig, actionParams: { channel: "slack", - target: "#C12345678", + target: "channel:C12345678", message: "hi", }, - toolContext: { currentChannelId: "C12345678" }, + toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" }, }, { name: "accepts legacy to parameter for send", cfg: slackConfig, actionParams: { channel: "slack", - to: "#C12345678", + to: "channel:C12345678", message: "hi", }, }, @@ -145,7 +145,7 @@ describe("runMessageAction context isolation", () => { cfg: slackConfig, actionParams: { channel: "slack", - target: "#C12345678", + target: "channel:C12345678", media: "https://example.com/note.ogg", }, toolContext: { currentChannelId: "C12345678" }, @@ -155,7 +155,7 @@ describe("runMessageAction context isolation", () => { cfg: slackConfig, actionParams: { channel: "slack", - target: "#C12345678", + target: "channel:C12345678", message: "hi", pollMulti: false, pollAnonymous: false, @@ -179,7 +179,7 @@ describe("runMessageAction context isolation", () => { cfg: slackConfig, actionParams: { channel: "slack", - target: "#C12345678", + target: "channel:C12345678", }, toolContext: { currentChannelId: "C12345678" }, }), @@ -217,7 +217,7 @@ describe("runMessageAction context isolation", () => { cfg: slackConfig, actionParams: { channel: "slack", - target: "#C12345678", + target: "channel:C12345678", blocks: [{ type: "divider" }], }, toolContext: { currentChannelId: "C12345678" }, diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 318699c1042..de87398b5e2 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -318,14 +318,16 @@ async function handleBroadcastAction( throw new Error("Broadcast requires at least one target in --targets."); } const channelHint = readStringParam(params, "channel"); - const configured = await listConfiguredMessageChannels(input.cfg); - if (configured.length === 0) { - throw new Error("Broadcast requires at least one configured channel."); + let targetChannels: ChannelId[]; + if (channelHint && channelHint.trim().toLowerCase() !== "all") { + targetChannels = [await resolveChannel(input.cfg, { channel: channelHint }, input.toolContext)]; + } else { + const configured = await listConfiguredMessageChannels(input.cfg); + if (configured.length === 0) { + throw new Error("Broadcast requires at least one configured channel."); + } + targetChannels = configured; } - const targetChannels = - channelHint && channelHint.trim().toLowerCase() !== "all" - ? [await resolveChannel(input.cfg, { channel: channelHint }, input.toolContext)] - : configured; const results: Array<{ channel: ChannelId; to: string; @@ -475,7 +477,6 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise = { }; const CLAUDE_PLUGIN_ROOT_PLACEHOLDER = "${CLAUDE_PLUGIN_ROOT}"; +function canonicalizeExistingDir(dir: string): string { + try { + return fs.realpathSync.native(dir); + } catch { + return dir; + } +} + function readPluginJsonObject(params: { rootDir: string; relativePath: string; @@ -121,37 +129,43 @@ function expandBundleRootPlaceholders(value: string, rootDir: string): string { return value.split(CLAUDE_PLUGIN_ROOT_PLACEHOLDER).join(rootDir); } +function resolveBundlePath(value: string, rootDir: string, baseDir: string): string { + const expanded = expandBundleRootPlaceholders(value, rootDir); + if (path.isAbsolute(expanded)) { + return path.normalize(expanded); + } + if (isExplicitRelativePath(expanded)) { + return path.resolve(baseDir, expanded); + } + return expanded; +} + function absolutizeBundleMcpServer(params: { rootDir: string; baseDir: string; server: BundleMcpServerConfig; }): BundleMcpServerConfig { + const rootDir = canonicalizeExistingDir(params.rootDir); + const baseDir = canonicalizeExistingDir(params.baseDir); const next: BundleMcpServerConfig = { ...params.server }; if (typeof next.cwd !== "string" && typeof next.workingDirectory !== "string") { - next.cwd = params.baseDir; + next.cwd = baseDir; } const command = next.command; if (typeof command === "string") { - const expanded = expandBundleRootPlaceholders(command, params.rootDir); - next.command = isExplicitRelativePath(expanded) - ? path.resolve(params.baseDir, expanded) - : expanded; + next.command = resolveBundlePath(command, rootDir, baseDir); } const cwd = next.cwd; if (typeof cwd === "string") { - const expanded = expandBundleRootPlaceholders(cwd, params.rootDir); - next.cwd = path.isAbsolute(expanded) ? expanded : path.resolve(params.baseDir, expanded); + next.cwd = resolveBundlePath(cwd, rootDir, baseDir); } const workingDirectory = next.workingDirectory; if (typeof workingDirectory === "string") { - const expanded = expandBundleRootPlaceholders(workingDirectory, params.rootDir); - next.workingDirectory = path.isAbsolute(expanded) - ? expanded - : path.resolve(params.baseDir, expanded); + next.workingDirectory = resolveBundlePath(workingDirectory, rootDir, baseDir); } if (Array.isArray(next.args)) { @@ -159,11 +173,7 @@ function absolutizeBundleMcpServer(params: { if (typeof entry !== "string") { return entry; } - const expanded = expandBundleRootPlaceholders(entry, params.rootDir); - if (!isExplicitRelativePath(expanded)) { - return expanded; - } - return path.resolve(params.baseDir, expanded); + return resolveBundlePath(entry, rootDir, baseDir); }); } @@ -171,7 +181,7 @@ function absolutizeBundleMcpServer(params: { next.env = Object.fromEntries( Object.entries(next.env).map(([key, value]) => [ key, - typeof value === "string" ? expandBundleRootPlaceholders(value, params.rootDir) : value, + typeof value === "string" ? resolveBundlePath(value, rootDir, baseDir) : value, ]), ); } diff --git a/src/plugins/runtime/runtime-config.ts b/src/plugins/runtime/runtime-config.ts index c25646f830d..1e94b41604a 100644 --- a/src/plugins/runtime/runtime-config.ts +++ b/src/plugins/runtime/runtime-config.ts @@ -1,9 +1,14 @@ -import { loadConfig, writeConfigFile } from "../../config/config.js"; +import * as configRuntime from "../../config/config.js"; import type { PluginRuntime } from "./types.js"; export function createRuntimeConfig(): PluginRuntime["config"] { return { - loadConfig, - writeConfigFile, + loadConfig: configRuntime.loadConfig, + writeConfigFile: + typeof configRuntime.writeConfigFile === "function" + ? configRuntime.writeConfigFile + : async () => { + throw new Error("writeConfigFile is unavailable in the current runtime"); + }, }; }