mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-25 00:42:24 +00:00
* test: align extension runtime mocks with plugin-sdk Update stale extension tests to mock the plugin-sdk runtime barrels that production code now imports, and harden the Signal tool-result harness around system-event assertions so the channels lane matches current extension boundaries. Regeneration-Prompt: | Verify the failing channels-lane tests against current origin/main in an isolated worktree before changing anything. If the failures reproduce on main, keep the fix test-only unless production behavior is clearly wrong. Recent extension refactors moved Telegram, WhatsApp, and Signal code onto plugin-sdk runtime barrels, so update stale tests that still mock old core module paths to intercept the seams production code now uses. For Signal reaction notifications, avoid brittle assertions that depend on shared queued system-event state when a direct harness spy on enqueue behavior is sufficient. Preserve scope: only touch the failing tests and their local harness, then rerun the reproduced targeted tests plus the full channels lane and repo check gate. * test: fix extension test drift on main * fix: lazy-load bundled web search plugin registry * test: make matrix sweeper failure injection portable * fix: split heavy matrix runtime-api seams * fix: simplify bundled web search id lookup * test: tolerate windows env key casing
136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { withEnv } from "../test-utils/env.js";
|
||
import { decodeCapturedOutputBuffer, parseWindowsCodePage, sanitizeEnv } from "./invoke.js";
|
||
import { buildNodeInvokeResultParams } from "./runner.js";
|
||
|
||
function getEnvValueCaseInsensitive(
|
||
env: Record<string, string>,
|
||
expectedKey: string,
|
||
): string | undefined {
|
||
const direct = env[expectedKey];
|
||
if (direct !== undefined) {
|
||
return direct;
|
||
}
|
||
const upper = expectedKey.toUpperCase();
|
||
const actualKey = Object.keys(env).find((key) => key.toUpperCase() === upper);
|
||
return actualKey ? env[actualKey] : undefined;
|
||
}
|
||
|
||
describe("node-host sanitizeEnv", () => {
|
||
it("ignores PATH overrides", () => {
|
||
withEnv({ PATH: "/usr/bin" }, () => {
|
||
const env = sanitizeEnv({ PATH: "/tmp/evil:/usr/bin" });
|
||
expect(env.PATH).toBe("/usr/bin");
|
||
});
|
||
});
|
||
|
||
it("blocks dangerous env keys/prefixes", () => {
|
||
withEnv(
|
||
{ PYTHONPATH: undefined, LD_PRELOAD: undefined, BASH_ENV: undefined, SHELLOPTS: undefined },
|
||
() => {
|
||
const env = sanitizeEnv({
|
||
PYTHONPATH: "/tmp/pwn",
|
||
LD_PRELOAD: "/tmp/pwn.so",
|
||
BASH_ENV: "/tmp/pwn.sh",
|
||
SHELLOPTS: "xtrace",
|
||
PS4: "$(touch /tmp/pwned)",
|
||
FOO: "bar",
|
||
});
|
||
expect(env.FOO).toBe("bar");
|
||
expect(env.PYTHONPATH).toBeUndefined();
|
||
expect(env.LD_PRELOAD).toBeUndefined();
|
||
expect(env.BASH_ENV).toBeUndefined();
|
||
expect(env.SHELLOPTS).toBeUndefined();
|
||
expect(env.PS4).toBeUndefined();
|
||
},
|
||
);
|
||
});
|
||
|
||
it("blocks dangerous override-only env keys", () => {
|
||
withEnv({ HOME: "/Users/trusted", ZDOTDIR: "/Users/trusted/.zdot" }, () => {
|
||
const env = sanitizeEnv({
|
||
HOME: "/tmp/evil-home",
|
||
ZDOTDIR: "/tmp/evil-zdotdir",
|
||
});
|
||
expect(env.HOME).toBe("/Users/trusted");
|
||
expect(env.ZDOTDIR).toBe("/Users/trusted/.zdot");
|
||
});
|
||
});
|
||
|
||
it("drops dangerous inherited env keys even without overrides", () => {
|
||
withEnv({ PATH: "/usr/bin:/bin", BASH_ENV: "/tmp/pwn.sh" }, () => {
|
||
const env = sanitizeEnv(undefined);
|
||
expect(env.PATH).toBe("/usr/bin:/bin");
|
||
expect(env.BASH_ENV).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
it("preserves inherited non-portable Windows-style env keys", () => {
|
||
withEnv({ "ProgramFiles(x86)": "C:\\Program Files (x86)" }, () => {
|
||
const env = sanitizeEnv(undefined);
|
||
expect(getEnvValueCaseInsensitive(env, "ProgramFiles(x86)")).toBe("C:\\Program Files (x86)");
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("node-host output decoding", () => {
|
||
it("parses code pages from chcp output text", () => {
|
||
expect(parseWindowsCodePage("Active code page: 936")).toBe(936);
|
||
expect(parseWindowsCodePage("活动代码页: 65001")).toBe(65001);
|
||
expect(parseWindowsCodePage("no code page")).toBeNull();
|
||
});
|
||
|
||
it("decodes GBK output on Windows when code page is known", () => {
|
||
let supportsGbk = true;
|
||
try {
|
||
void new TextDecoder("gbk");
|
||
} catch {
|
||
supportsGbk = false;
|
||
}
|
||
|
||
const raw = Buffer.from([0xb2, 0xe2, 0xca, 0xd4, 0xa1, 0xab, 0xa3, 0xbb]);
|
||
const decoded = decodeCapturedOutputBuffer({
|
||
buffer: raw,
|
||
platform: "win32",
|
||
windowsEncoding: "gbk",
|
||
});
|
||
|
||
if (!supportsGbk) {
|
||
expect(decoded).toContain("<22>");
|
||
return;
|
||
}
|
||
expect(decoded).toBe("测试~;");
|
||
});
|
||
});
|
||
|
||
describe("buildNodeInvokeResultParams", () => {
|
||
it("omits optional fields when null/undefined", () => {
|
||
const params = buildNodeInvokeResultParams(
|
||
{ id: "invoke-1", nodeId: "node-1", command: "system.run" },
|
||
{ ok: true, payloadJSON: null, error: null },
|
||
);
|
||
|
||
expect(params).toEqual({ id: "invoke-1", nodeId: "node-1", ok: true });
|
||
expect("payloadJSON" in params).toBe(false);
|
||
expect("error" in params).toBe(false);
|
||
});
|
||
|
||
it("includes payloadJSON when provided", () => {
|
||
const params = buildNodeInvokeResultParams(
|
||
{ id: "invoke-2", nodeId: "node-2", command: "system.run" },
|
||
{ ok: true, payloadJSON: '{"ok":true}' },
|
||
);
|
||
|
||
expect(params.payloadJSON).toBe('{"ok":true}');
|
||
});
|
||
|
||
it("includes payload when provided", () => {
|
||
const params = buildNodeInvokeResultParams(
|
||
{ id: "invoke-3", nodeId: "node-3", command: "system.run" },
|
||
{ ok: false, payload: { reason: "bad" } },
|
||
);
|
||
|
||
expect(params.payload).toEqual({ reason: "bad" });
|
||
});
|
||
});
|