import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const preparedPlan = vi.hoisted(() => ({ argv: ["bun", "./script.ts"], cwd: "/tmp/work", commandText: "bun ./script.ts", commandPreview: "bun ./script.ts", agentId: "prepared-agent", sessionKey: "prepared-session", mutableFileOperand: { argvIndex: 1, path: "/tmp/work/script.ts", sha256: "abc123", }, })); const callGatewayToolMock = vi.hoisted(() => vi.fn()); const listNodesMock = vi.hoisted(() => vi.fn()); const parsePreparedSystemRunPayloadMock = vi.hoisted(() => vi.fn()); const requiresExecApprovalMock = vi.hoisted(() => vi.fn(() => true)); const resolveExecHostApprovalContextMock = vi.hoisted(() => vi.fn(() => ({ approvals: { allowlist: [], file: { version: 1, agents: {} } }, hostSecurity: "full", hostAsk: "off", askFallback: "deny", })), ); const createAndRegisterDefaultExecApprovalRequestMock = vi.hoisted(() => vi.fn()); const resolveApprovalDecisionOrUndefinedMock = vi.hoisted(() => vi.fn(async () => "allow-once")); const createExecApprovalDecisionStateMock = vi.hoisted(() => vi.fn(() => ({ baseDecision: { timedOut: false }, approvedByAsk: false, deniedReason: null, })), ); const buildExecApprovalPendingToolResultMock = vi.hoisted(() => vi.fn()); const registerExecApprovalRequestForHostOrThrowMock = vi.hoisted(() => vi.fn(async () => undefined), ); vi.mock("../infra/exec-approvals.js", () => ({ evaluateShellAllowlist: vi.fn(() => ({ allowlistMatches: [], analysisOk: true, allowlistSatisfied: false, segments: [{ resolution: null, argv: ["bun", "./script.ts"] }], segmentAllowlistEntries: [], })), hasDurableExecApproval: vi.fn(() => false), requiresExecApproval: requiresExecApprovalMock, resolveExecApprovalAllowedDecisions: vi.fn(() => ["allow-once", "allow-always", "deny"]), resolveExecApprovalsFromFile: vi.fn(() => ({ allowlist: [], file: { version: 1, agents: {} }, })), })); vi.mock("../infra/exec-inline-eval.js", () => ({ describeInterpreterInlineEval: vi.fn(() => "inline-eval"), detectInterpreterInlineEvalArgv: vi.fn(() => null), })); vi.mock("../infra/node-shell.js", () => ({ buildNodeShellCommand: vi.fn(() => ["bash", "-lc", "bun ./script.ts"]), })); vi.mock("../infra/system-run-approval-context.js", () => ({ parsePreparedSystemRunPayload: parsePreparedSystemRunPayloadMock, })); vi.mock("./bash-tools.exec-approval-request.js", () => ({ buildExecApprovalRequesterContext: vi.fn(() => ({})), buildExecApprovalTurnSourceContext: vi.fn(() => ({})), registerExecApprovalRequestForHostOrThrow: registerExecApprovalRequestForHostOrThrowMock, })); vi.mock("./bash-tools.exec-host-shared.js", () => ({ resolveExecHostApprovalContext: resolveExecHostApprovalContextMock, buildDefaultExecApprovalRequestArgs: vi.fn(() => ({})), createAndRegisterDefaultExecApprovalRequest: createAndRegisterDefaultExecApprovalRequestMock, shouldResolveExecApprovalUnavailableInline: vi.fn(() => false), buildExecApprovalFollowupTarget: vi.fn(() => ({ approvalId: "approval-1" })), resolveApprovalDecisionOrUndefined: resolveApprovalDecisionOrUndefinedMock, createExecApprovalDecisionState: createExecApprovalDecisionStateMock, sendExecApprovalFollowupResult: vi.fn(async () => undefined), buildExecApprovalPendingToolResult: buildExecApprovalPendingToolResultMock, buildHeadlessExecApprovalDeniedMessage: vi.fn(() => "denied"), })); vi.mock("./bash-tools.exec-runtime.js", () => ({ DEFAULT_NOTIFY_TAIL_CHARS: 1000, createApprovalSlug: vi.fn(() => "slug"), normalizeNotifyOutput: vi.fn((value: string) => value), })); vi.mock("./tools/gateway.js", () => ({ callGatewayTool: callGatewayToolMock, })); vi.mock("./tools/nodes-utils.js", () => ({ listNodes: listNodesMock, resolveNodeIdFromList: vi.fn(() => "node-1"), })); vi.mock("../logger.js", () => ({ logInfo: vi.fn(), })); let executeNodeHostCommand: typeof import("./bash-tools.exec-host-node.js").executeNodeHostCommand; type MockNodeInvokeParams = { command?: string; }; describe("executeNodeHostCommand", () => { beforeAll(async () => { ({ executeNodeHostCommand } = await import("./bash-tools.exec-host-node.js")); }); beforeEach(() => { callGatewayToolMock.mockReset(); callGatewayToolMock.mockImplementation( async (method: string, _options: unknown, params: MockNodeInvokeParams | undefined) => { if (method !== "node.invoke") { throw new Error(`unexpected gateway method: ${method}`); } if (params?.command === "system.run.prepare") { return { payload: { plan: preparedPlan } }; } if (params?.command === "system.run") { return { payload: { success: true, stdout: "ok", stderr: "", exitCode: 0, timedOut: false, }, }; } throw new Error(`unexpected node invoke command: ${String(params?.command)}`); }, ); listNodesMock.mockReset(); listNodesMock.mockResolvedValue([ { nodeId: "node-1", commands: ["system.run"], platform: process.platform }, ]); parsePreparedSystemRunPayloadMock.mockReset(); parsePreparedSystemRunPayloadMock.mockReturnValue({ plan: preparedPlan }); requiresExecApprovalMock.mockReset(); requiresExecApprovalMock.mockReturnValue(true); resolveExecHostApprovalContextMock.mockReset(); resolveExecHostApprovalContextMock.mockReturnValue({ approvals: { allowlist: [], file: { version: 1, agents: {} } }, hostSecurity: "full", hostAsk: "off", askFallback: "deny", }); createAndRegisterDefaultExecApprovalRequestMock.mockReset(); createAndRegisterDefaultExecApprovalRequestMock.mockImplementation(async (args?: unknown) => { const register = args && typeof args === "object" && "register" in args ? (args as { register?: (approvalId: string) => Promise }).register : undefined; await register?.("approval-1"); return { approvalId: "approval-1", approvalSlug: "slug-1", warningText: "", expiresAtMs: Date.now() + 60_000, preResolvedDecision: null, initiatingSurface: "origin", sentApproverDms: false, unavailableReason: null, }; }); resolveApprovalDecisionOrUndefinedMock.mockReset(); resolveApprovalDecisionOrUndefinedMock.mockResolvedValue("allow-once"); createExecApprovalDecisionStateMock.mockReset(); createExecApprovalDecisionStateMock.mockReturnValue({ baseDecision: { timedOut: false }, approvedByAsk: false, deniedReason: null, }); buildExecApprovalPendingToolResultMock.mockReset(); buildExecApprovalPendingToolResultMock.mockReturnValue({ content: [], details: { status: "approval-pending" }, }); registerExecApprovalRequestForHostOrThrowMock.mockReset(); }); it("forwards prepared systemRunPlan on async node invoke after approval", async () => { const result = await executeNodeHostCommand({ command: "bun ./script.ts", workdir: "/tmp/work", env: {}, security: "full", ask: "off", defaultTimeoutSec: 30, approvalRunningNoticeMs: 0, warnings: [], agentId: "requested-agent", sessionKey: "requested-session", }); expect(result.details?.status).toBe("approval-pending"); expect(registerExecApprovalRequestForHostOrThrowMock).toHaveBeenCalledWith( expect.objectContaining({ systemRunPlan: preparedPlan, }), ); await vi.waitFor(() => { expect(callGatewayToolMock).toHaveBeenCalledTimes(2); }); expect(callGatewayToolMock).toHaveBeenNthCalledWith( 2, "node.invoke", expect.anything(), expect.objectContaining({ command: "system.run", params: expect.objectContaining({ approved: true, approvalDecision: "allow-once", systemRunPlan: preparedPlan, }), }), ); }); });