Address Windows review regressions
This commit is contained in:
parent
cc4464f2ce
commit
8d66245825
@ -47,6 +47,9 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Hooks/Windows: preserve Windows-aware hook path handling across plugin-managed hook loading and bundle MCP config resolution, so path aliases and canonicalization differences no longer drop hook metadata or break bundled MCP launches.
|
||||
- Outbound/channels: skip full configured-channel scans when explicit channel selection already determines the target, so explicit sends and broadcasts avoid slow unrelated plugin configuration checks.
|
||||
- Tlon/install: fetch `@tloncorp/api` from the pinned HTTPS tarball artifact instead of a Git transport URL so installs no longer depend on GitHub SSH access.
|
||||
- CLI/Ollama onboarding: keep the interactive model picker for explicit `openclaw onboard --auth-choice ollama` runs so setup still selects a default model without reintroducing pre-picker auto-pulls. (#49249) Thanks @BruceMacD.
|
||||
- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev.
|
||||
- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev.
|
||||
|
||||
@ -222,11 +222,12 @@ async function resolveChannel(
|
||||
params: Record<string, unknown>,
|
||||
toolContext?: { currentChannelProvider?: string },
|
||||
) {
|
||||
const explicitChannel = readStringParam(params, "channel");
|
||||
const selection = await resolveMessageChannelSelection({
|
||||
cfg,
|
||||
channel: readStringParam(params, "channel"),
|
||||
channel: explicitChannel,
|
||||
fallbackChannel: toolContext?.currentChannelProvider,
|
||||
includeConfigured: false,
|
||||
includeConfigured: !explicitChannel,
|
||||
});
|
||||
if (selection.source === "tool-context-fallback") {
|
||||
params.channel = selection.channel;
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createMSTeamsTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
createChannelTestPluginBase,
|
||||
createMSTeamsTestPlugin,
|
||||
createTestRegistry,
|
||||
} from "../../test-utils/channel-plugins.js";
|
||||
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
|
||||
@ -242,6 +246,78 @@ describe("sendPoll channel normalization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("implicit single-channel selection", () => {
|
||||
it("keeps single configured channel fallback for sendMessage when channel is omitted", async () => {
|
||||
const sendText = vi.fn(async () => ({ channel: "msteams", messageId: "m1" }));
|
||||
setRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: {
|
||||
...createChannelTestPluginBase({
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
docsPath: "/channels/msteams",
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({}),
|
||||
isConfigured: () => true,
|
||||
},
|
||||
}),
|
||||
outbound: {
|
||||
...createMSTeamsOutbound(),
|
||||
sendText,
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await sendMessage({
|
||||
cfg: {},
|
||||
to: "conversation:19:abc@thread.tacv2",
|
||||
content: "hi",
|
||||
});
|
||||
|
||||
expect(result.channel).toBe("msteams");
|
||||
expect(sendText).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps single configured channel fallback for sendPoll when channel is omitted", async () => {
|
||||
setRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: {
|
||||
...createChannelTestPluginBase({
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
docsPath: "/channels/msteams",
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({}),
|
||||
isConfigured: () => true,
|
||||
},
|
||||
}),
|
||||
outbound: createMSTeamsOutbound({ includePoll: true }),
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await sendPoll({
|
||||
cfg: {},
|
||||
to: "conversation:19:abc@thread.tacv2",
|
||||
question: "Lunch?",
|
||||
options: ["Pizza", "Sushi"],
|
||||
});
|
||||
|
||||
expect(result.channel).toBe("msteams");
|
||||
});
|
||||
});
|
||||
|
||||
const setMattermostGatewayRegistry = () => {
|
||||
setRegistry(
|
||||
createTestRegistry([
|
||||
|
||||
@ -132,11 +132,12 @@ async function resolveRequiredChannel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel?: string;
|
||||
}): Promise<string> {
|
||||
const explicitChannel = typeof params.channel === "string" ? params.channel.trim() : "";
|
||||
return (
|
||||
await resolveMessageChannelSelection({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
includeConfigured: false,
|
||||
channel: explicitChannel || undefined,
|
||||
includeConfigured: !explicitChannel,
|
||||
})
|
||||
).channel;
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ describe("isPathInside", () => {
|
||||
it("accepts identical and nested paths but rejects escapes", () => {
|
||||
expect(isPathInside("/workspace/root", "/workspace/root")).toBe(true);
|
||||
expect(isPathInside("/workspace/root", "/workspace/root/nested/file.txt")).toBe(true);
|
||||
expect(isPathInside("/workspace/root", "/workspace/root/..cache/file.txt")).toBe(true);
|
||||
expect(isPathInside("/workspace/root", "/workspace/root/../escape.txt")).toBe(false);
|
||||
});
|
||||
|
||||
@ -75,6 +76,9 @@ describe("isPathInside", () => {
|
||||
expect(
|
||||
isPathInside(String.raw`C:\workspace\root`, String.raw`C:\workspace\root\Nested\File.txt`),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPathInside(String.raw`C:\workspace\root`, String.raw`C:\workspace\root\..cache\file.txt`),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPathInside(String.raw`C:\workspace\root`, String.raw`C:\workspace\root\..\escape.txt`),
|
||||
).toBe(false);
|
||||
|
||||
@ -37,11 +37,20 @@ export function isPathInside(root: string, target: string): boolean {
|
||||
const rootForCompare = normalizeWindowsPathForComparison(path.win32.resolve(root));
|
||||
const targetForCompare = normalizeWindowsPathForComparison(path.win32.resolve(target));
|
||||
const relative = path.win32.relative(rootForCompare, targetForCompare);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.win32.isAbsolute(relative));
|
||||
return (
|
||||
relative === "" ||
|
||||
(relative !== ".." &&
|
||||
!relative.startsWith(`..\\`) &&
|
||||
!relative.startsWith("../") &&
|
||||
!path.win32.isAbsolute(relative))
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedRoot = path.resolve(root);
|
||||
const resolvedTarget = path.resolve(target);
|
||||
const relative = path.relative(resolvedRoot, resolvedTarget);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
return (
|
||||
relative === "" ||
|
||||
(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
||||
);
|
||||
}
|
||||
|
||||
@ -327,7 +327,7 @@ export function loadEnabledBundleMcpConfig(params: {
|
||||
|
||||
const loaded = loadBundleMcpConfig({
|
||||
pluginId: record.id,
|
||||
rootDir: record.rootDir,
|
||||
rootDir: record.format === "bundle" ? record.source : record.rootDir,
|
||||
bundleFormat: record.bundleFormat,
|
||||
});
|
||||
merged = applyMergePatch(merged, loaded.config) as BundleMcpConfig;
|
||||
|
||||
@ -299,7 +299,9 @@ describe("discoverOpenClawPlugins", () => {
|
||||
expect(bundle?.format).toBe("bundle");
|
||||
expect(bundle?.bundleFormat).toBe("codex");
|
||||
expect(bundle?.source).toBe(bundleDir);
|
||||
expect(normalizePathForAssertion(bundle?.rootDir)).toBe(normalizePathForAssertion(bundleDir));
|
||||
expect(normalizePathForAssertion(bundle?.rootDir)).toBe(
|
||||
normalizePathForAssertion(fs.realpathSync(bundleDir)),
|
||||
);
|
||||
});
|
||||
|
||||
it("auto-detects manifestless Claude bundles from the default layout", async () => {
|
||||
|
||||
@ -377,8 +377,7 @@ function addCandidate(params: {
|
||||
if (params.seen.has(resolved)) {
|
||||
return;
|
||||
}
|
||||
const lexicalRoot = path.resolve(params.rootDir);
|
||||
const resolvedRoot = safeRealpathSync(params.rootDir) ?? lexicalRoot;
|
||||
const resolvedRoot = safeRealpathSync(params.rootDir) ?? path.resolve(params.rootDir);
|
||||
if (
|
||||
isUnsafePluginCandidate({
|
||||
source: resolved,
|
||||
@ -396,7 +395,7 @@ function addCandidate(params: {
|
||||
idHint: params.idHint,
|
||||
source: resolved,
|
||||
setupSource: params.setupSource,
|
||||
rootDir: lexicalRoot,
|
||||
rootDir: resolvedRoot,
|
||||
origin: params.origin,
|
||||
format: params.format ?? "openclaw",
|
||||
bundleFormat: params.bundleFormat,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user