test: fix remaining CI regressions

This commit is contained in:
MaxxxDong 2026-03-20 10:21:00 +08:00
parent e7a736f513
commit 4f76c0bd6b
10 changed files with 151 additions and 80 deletions

View File

@ -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";

View File

@ -36,44 +36,64 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
const mockModule = Object.create(null) as Record<string, unknown>;
Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual));
Object.defineProperty(mockModule, "loadConfig", {
configurable: true,
enumerable: true,
writable: true,
value: () => {
const getter = (globalThis as Record<symbol, unknown>)[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<symbol, unknown>)[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<string, Record<string, unknown>>;
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<string, Record<string, unknown>>;
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<string, unknown>;
} catch {
return {};
}
loadSessionStore: {
configurable: true,
enumerable: true,
writable: true,
value: (storePath: string) => {
try {
return JSON.parse(fsSync.readFileSync(storePath, "utf8")) as Record<string, unknown>;
} 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;
});

View File

@ -49,6 +49,7 @@ describe("hooks install (e2e)", () => {
{
name: "@acme/hello-hooks",
version: "0.0.0",
type: "module",
openclaw: { hooks: ["./hooks/hello-hook"] },
},
null,

View File

@ -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);

View File

@ -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 },
});

View File

@ -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",
};
}

View File

@ -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" },

View File

@ -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<MessageActi
message,
preferComponents: true,
});
const mediaUrl = readStringParam(params, "media", { trim: false });
if (
!hasReplyPayloadContent(

View File

@ -44,6 +44,14 @@ const MANIFEST_PATH_BY_FORMAT: Record<PluginBundleFormat, string> = {
};
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,
]),
);
}

View File

@ -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");
},
};
}