diff --git a/src/agents/pi-tools.safe-bins.test.ts b/src/agents/pi-tools.safe-bins.test.ts index db0c7a934e2..d4cdeeca4f1 100644 --- a/src/agents/pi-tools.safe-bins.test.ts +++ b/src/agents/pi-tools.safe-bins.test.ts @@ -1,46 +1,17 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { ExecApprovalsResolved } from "../infra/exec-approvals.js"; import type { SafeBinProfileFixture } from "../infra/exec-safe-bin-policy.js"; import { withEnvAsync } from "../test-utils/env.js"; +import { resetProcessRegistryForTests } from "./bash-process-registry.js"; let createOpenClawCodingTools: typeof import("./pi-tools.js").createOpenClawCodingTools; -beforeAll(async () => { - await withEnvAsync( - { - OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(os.tmpdir(), "openclaw-test-no-bundled-extensions"), - }, - async () => { - ({ createOpenClawCodingTools } = await import("./pi-tools.js")); - }, - ); -}); - -vi.mock("../infra/shell-env.js", async () => { - const mod = - await vi.importActual("../infra/shell-env.js"); - return { - ...mod, - getShellPathFromLoginShell: vi.fn(() => null), - resolveShellEnvFallbackTimeoutMs: vi.fn(() => 50), - }; -}); - -vi.mock("../plugins/tools.js", () => ({ - copyPluginToolMeta: vi.fn((_from, to) => to), - resolvePluginTools: () => [], - getPluginToolMeta: () => undefined, -})); - -vi.mock("../infra/exec-approvals.js", async () => { - const mod = await vi.importActual( - "../infra/exec-approvals.js", - ); - const approvals: ExecApprovalsResolved = { +const { mockExecApprovals, supervisorSpawnMock } = vi.hoisted(() => { + const execApprovals = { path: "/tmp/exec-approvals.json", socketPath: "/tmp/exec-approvals.sock", token: "token", @@ -74,7 +45,123 @@ vi.mock("../infra/exec-approvals.js", async () => { agents: {}, }, }; - return { ...mod, resolveExecApprovals: () => approvals }; + return { + mockExecApprovals: execApprovals, + supervisorSpawnMock: vi.fn( + async (input: { argv?: string[]; onStdout?: (chunk: string) => void }) => { + input.onStdout?.(`${input.argv?.join(" ") ?? ""}\n`); + return { + runId: "safe-bins-test-run", + pid: 1234, + startedAtMs: Date.now(), + stdin: undefined, + wait: async () => ({ + reason: "exit" as const, + exitCode: 0, + exitSignal: null, + durationMs: 1, + stdout: "", + stderr: "", + timedOut: false, + noOutputTimedOut: false, + }), + cancel: vi.fn(), + }; + }, + ), + }; +}); + +beforeAll(async () => { + await withEnvAsync( + { + OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(os.tmpdir(), "openclaw-test-no-bundled-extensions"), + }, + async () => { + ({ createOpenClawCodingTools } = await import("./pi-tools.js")); + }, + ); +}); + +beforeEach(() => { + supervisorSpawnMock.mockClear(); +}); + +vi.mock("../infra/shell-env.js", async () => { + const mod = + await vi.importActual("../infra/shell-env.js"); + return { + ...mod, + getShellPathFromLoginShell: vi.fn(() => null), + resolveShellEnvFallbackTimeoutMs: vi.fn(() => 50), + }; +}); + +vi.mock("../process/supervisor/index.js", () => ({ + getProcessSupervisor: () => ({ + spawn: supervisorSpawnMock, + cancel: vi.fn(), + cancelScope: vi.fn(), + reconcileOrphans: vi.fn(), + getRecord: vi.fn(), + }), +})); + +vi.mock("./channel-tools.js", () => ({ + copyChannelAgentToolMeta: vi.fn((_from, to) => to), + listChannelAgentTools: () => [], +})); + +vi.mock("./openclaw-tools.js", () => ({ + createOpenClawTools: () => [], +})); + +vi.mock("./bash-tools.exec-host-shared.js", async () => { + const mod = await vi.importActual( + "./bash-tools.exec-host-shared.js", + ); + return { + ...mod, + resolveExecHostApprovalContext: () => ({ + approvals: mockExecApprovals, + hostSecurity: "allowlist", + hostAsk: "off", + askFallback: "deny", + }), + }; +}); + +vi.mock("../plugins/tools.js", () => ({ + copyPluginToolMeta: vi.fn((_from, to) => to), + resolvePluginTools: () => [], + getPluginToolMeta: () => undefined, +})); + +vi.mock("@mariozechner/pi-coding-agent", () => ({ + AuthStorage: vi.fn(), + CURRENT_SESSION_VERSION: 1, + ModelRegistry: vi.fn(), + SessionManager: vi.fn(), + SettingsManager: vi.fn(), + codingTools: [], + createEditTool: vi.fn(), + createReadTool: vi.fn(), + createWriteTool: vi.fn(), + estimateTokens: vi.fn(() => 0), + formatSkillsForPrompt: vi.fn(() => ""), + readTool: undefined, +})); + +vi.mock("../infra/exec-approvals.js", async () => { + const mod = await vi.importActual( + "../infra/exec-approvals.js", + ); + const approvals = mockExecApprovals as ExecApprovalsResolved; + return { + ...mod, + loadExecApprovals: () => approvals.file, + resolveExecApprovals: () => approvals, + }; }); type ExecToolResult = { @@ -118,6 +205,9 @@ async function createSafeBinsExecTool(params: { const tools = createOpenClawCodingTools({ config: cfg, + exec: { + notifyOnExit: false, + }, sessionKey: "agent:main:main", workspaceDir: tmpDir, agentDir: path.join(tmpDir, "agent"), @@ -138,9 +228,18 @@ async function withSafeBinsExecTool( } const ctx = await createSafeBinsExecTool(params); try { - await run(ctx); + await withEnvAsync( + { + OPENCLAW_SHELL_ENV_TIMEOUT_MS: "1", + SHELL: "/bin/sh", + }, + async () => { + await run(ctx); + }, + ); } finally { fs.rmSync(ctx.tmpDir, { recursive: true, force: true }); + resetProcessRegistryForTests(); } } @@ -165,6 +264,7 @@ describe("createOpenClawCodingTools safeBins", () => { const resultDetails = result.details as { status?: string }; expect(resultDetails.status).toBe("completed"); expect(text).toContain(marker); + expect(supervisorSpawnMock).toHaveBeenCalledOnce(); }, ); });