|
1 | 1 | // Covers install-policy checks for packages and plugin installs. |
| 2 | +import type { ChildProcess } from "node:child_process"; |
| 3 | +import { EventEmitter } from "node:events"; |
2 | 4 | import fs from "node:fs/promises"; |
3 | 5 | import os from "node:os"; |
4 | 6 | import path from "node:path"; |
5 | 7 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 8 | + |
| 9 | +const spawnMock = vi.hoisted(() => vi.fn()); |
| 10 | +vi.mock("node:child_process", async (importOriginal) => { |
| 11 | + const actual = await importOriginal<typeof import("node:child_process")>(); |
| 12 | + return { |
| 13 | + ...actual, |
| 14 | + spawn: (...args: Parameters<typeof actual.spawn>) => |
| 15 | + spawnMock(...args) ?? actual.spawn(...args), |
| 16 | + }; |
| 17 | +}); |
6 | 18 | import type { OpenClawConfig } from "../config/types.openclaw.js"; |
7 | 19 | import { |
8 | 20 | killPidIfAlive, |
@@ -674,3 +686,75 @@ describe("runInstallPolicy", () => { |
674 | 686 | }, |
675 | 687 | ); |
676 | 688 | }); |
| 689 | + |
| 690 | +describe("runPolicyCommand stream error handling", () => { |
| 691 | + function createFakeChild(): ChildProcess { |
| 692 | + const child = new EventEmitter() as EventEmitter & ChildProcess; |
| 693 | + child.stdout = new EventEmitter() as EventEmitter & NonNullable<ChildProcess["stdout"]>; |
| 694 | + child.stderr = new EventEmitter() as EventEmitter & NonNullable<ChildProcess["stderr"]>; |
| 695 | + child.stdin = new EventEmitter() as EventEmitter & NonNullable<ChildProcess["stdin"]>; |
| 696 | + child.stdin.write = vi.fn(() => true) as NonNullable<ChildProcess["stdin"]>["write"]; |
| 697 | + child.stdin.end = vi.fn() as NonNullable<ChildProcess["stdin"]>["end"]; |
| 698 | + Object.defineProperties(child, { |
| 699 | + pid: { configurable: true, enumerable: true, get: () => 1234 }, |
| 700 | + killed: { configurable: true, enumerable: true, get: () => false }, |
| 701 | + }); |
| 702 | + child.kill = vi.fn(() => true) as ChildProcess["kill"]; |
| 703 | + return child; |
| 704 | + } |
| 705 | + |
| 706 | + beforeEach(() => { |
| 707 | + spawnMock.mockReset(); |
| 708 | + }); |
| 709 | + |
| 710 | + it("swallows stdout and stderr stream errors without rejecting", { timeout: 5_000 }, async () => { |
| 711 | + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-policy-stream-")); |
| 712 | + tempDirs.push(dir); |
| 713 | + const policyScriptPath = path.join(dir, "policy.cjs"); |
| 714 | + await fs.writeFile(policyScriptPath, "module.exports = {};", "utf8"); |
| 715 | + await fs.chmod(policyScriptPath, 0o700); |
| 716 | + |
| 717 | + spawnMock.mockImplementation(() => { |
| 718 | + const child = createFakeChild(); |
| 719 | + const response = Buffer.from(JSON.stringify({ protocolVersion: 1, decision: "allow" })); |
| 720 | + // Emit stream errors and response after listeners are attached in runPolicyCommand. |
| 721 | + queueMicrotask(() => { |
| 722 | + child.stdout?.emit("error", new Error("stdout read failed")); |
| 723 | + child.stdout?.emit("data", response); |
| 724 | + child.stderr?.emit("error", new Error("stderr read failed")); |
| 725 | + child.emit("close", 0, null); |
| 726 | + }); |
| 727 | + return child; |
| 728 | + }); |
| 729 | + |
| 730 | + await expect( |
| 731 | + runInstallPolicy({ |
| 732 | + config: { |
| 733 | + security: { |
| 734 | + installPolicy: { |
| 735 | + enabled: true, |
| 736 | + exec: { |
| 737 | + source: "exec", |
| 738 | + command: policyScriptPath, |
| 739 | + args: [], |
| 740 | + allowInsecurePath: true, |
| 741 | + timeoutMs: 5_000, |
| 742 | + noOutputTimeoutMs: 5_000, |
| 743 | + maxOutputBytes: 16 * 1024, |
| 744 | + }, |
| 745 | + }, |
| 746 | + }, |
| 747 | + }, |
| 748 | + request: { |
| 749 | + targetType: "skill", |
| 750 | + targetName: "weather", |
| 751 | + sourcePath: dir, |
| 752 | + sourcePathKind: "directory", |
| 753 | + source: { kind: "clawhub", authority: "openclaw", mutable: false, network: true }, |
| 754 | + origin: { type: "clawhub" }, |
| 755 | + request: { kind: "skill-install", mode: "install" }, |
| 756 | + }, |
| 757 | + }), |
| 758 | + ).resolves.toEqual({}); |
| 759 | + }); |
| 760 | +}); |
0 commit comments