import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ sendExecApprovalFollowup: vi.fn(), logWarn: vi.fn(), resolveExecApprovals: vi.fn(() => ({ defaults: { security: "allowlist", ask: "off", askFallback: "deny", autoAllowSkills: false, }, agent: { security: "allowlist", ask: "off", askFallback: "deny", autoAllowSkills: false, }, allowlist: [], file: { version: 1, agents: {} }, })), })); vi.mock("./bash-tools.exec-approval-followup.js", () => ({ sendExecApprovalFollowup: mocks.sendExecApprovalFollowup, })); vi.mock("../logger.js", () => ({ logWarn: mocks.logWarn, })); vi.mock("../infra/exec-approvals.js", async (importOriginal) => { const mod = await importOriginal(); return { ...mod, resolveExecApprovals: mocks.resolveExecApprovals, }; }); let sendExecApprovalFollowupResult: typeof import("./bash-tools.exec-host-shared.js").sendExecApprovalFollowupResult; let maxExecApprovalFollowupFailureLogKeys: typeof import("./bash-tools.exec-host-shared.js").MAX_EXEC_APPROVAL_FOLLOWUP_FAILURE_LOG_KEYS; let resolveExecHostApprovalContext: typeof import("./bash-tools.exec-host-shared.js").resolveExecHostApprovalContext; let sendExecApprovalFollowup: typeof import("./bash-tools.exec-approval-followup.js").sendExecApprovalFollowup; let logWarn: typeof import("../logger.js").logWarn; beforeAll(async () => { ({ sendExecApprovalFollowupResult, MAX_EXEC_APPROVAL_FOLLOWUP_FAILURE_LOG_KEYS: maxExecApprovalFollowupFailureLogKeys, resolveExecHostApprovalContext, } = await import("./bash-tools.exec-host-shared.js")); ({ sendExecApprovalFollowup } = await import("./bash-tools.exec-approval-followup.js")); ({ logWarn } = await import("../logger.js")); }); describe("sendExecApprovalFollowupResult", () => { beforeEach(() => { vi.mocked(sendExecApprovalFollowup).mockReset(); vi.mocked(logWarn).mockReset(); mocks.resolveExecApprovals.mockReset(); mocks.resolveExecApprovals.mockReturnValue({ defaults: { security: "allowlist", ask: "off", askFallback: "deny", autoAllowSkills: false, }, agent: { security: "allowlist", ask: "off", askFallback: "deny", autoAllowSkills: false, }, allowlist: [], file: { version: 1, agents: {} }, }); }); it("logs repeated followup dispatch failures once per approval id and error message", async () => { vi.mocked(sendExecApprovalFollowup).mockRejectedValue(new Error("Channel is required")); const target = { approvalId: "approval-log-once", sessionKey: "agent:main:main", }; await sendExecApprovalFollowupResult(target, "Exec finished"); await sendExecApprovalFollowupResult(target, "Exec finished"); expect(logWarn).toHaveBeenCalledTimes(1); expect(logWarn).toHaveBeenCalledWith( "exec approval followup dispatch failed (id=approval-log-once): Channel is required", ); }); it("evicts oldest followup failure dedupe keys after reaching the cap", async () => { vi.mocked(sendExecApprovalFollowup).mockRejectedValue(new Error("Channel is required")); for (let i = 0; i <= maxExecApprovalFollowupFailureLogKeys; i += 1) { await sendExecApprovalFollowupResult( { approvalId: `approval-${i}`, sessionKey: "agent:main:main", }, "Exec finished", ); } await sendExecApprovalFollowupResult( { approvalId: "approval-0", sessionKey: "agent:main:main", }, "Exec finished", ); expect(logWarn).toHaveBeenCalledTimes(maxExecApprovalFollowupFailureLogKeys + 2); expect(logWarn).toHaveBeenLastCalledWith( "exec approval followup dispatch failed (id=approval-0): Channel is required", ); }); }); describe("resolveExecHostApprovalContext", () => { it("uses exec-approvals.json agent security even when it is broader than the tool default", () => { mocks.resolveExecApprovals.mockReturnValue({ defaults: { security: "allowlist", ask: "off", askFallback: "deny", autoAllowSkills: false, }, agent: { security: "full", ask: "off", askFallback: "deny", autoAllowSkills: false, }, allowlist: [], file: { version: 1, agents: {} }, }); const result = resolveExecHostApprovalContext({ agentId: "agent-main", security: "allowlist", ask: "off", host: "gateway", }); expect(result.hostSecurity).toBe("full"); }); });