test: fix remaining CI regressions
This commit is contained in:
parent
e7a736f513
commit
4f76c0bd6b
@ -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";
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -49,6 +49,7 @@ describe("hooks install (e2e)", () => {
|
||||
{
|
||||
name: "@acme/hello-hooks",
|
||||
version: "0.0.0",
|
||||
type: "module",
|
||||
openclaw: { hooks: ["./hooks/hello-hook"] },
|
||||
},
|
||||
null,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 },
|
||||
});
|
||||
|
||||
@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
@ -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" },
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user