multica/src/hub/exec-approval-manager.test.ts
Jiang Bohan 5380b146b3 chore(deps): upgrade pi-ai and pi-agent-core to 0.52.9
Upgrade @mariozechner/pi-ai and @mariozechner/pi-agent-core from 0.50.3
to 0.52.9 to support latest models (claude-opus-4-6, o3, o3-mini).

Breaking type changes addressed:
- exactOptionalPropertyTypes: use conditional spread or `| undefined`
- TOOL_PROFILES removed: strip all profile references from CLI
- AgentMessage union requires timestamp: cast test fixtures
- AsyncAgent.id → sessionId
- Add explicit callback parameter types for SDK event handlers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 19:13:38 +08:00

286 lines
8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { ExecApprovalManager } from "./exec-approval-manager.js";
describe("ExecApprovalManager", () => {
let manager: ExecApprovalManager;
let sendToClient: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.useFakeTimers();
sendToClient = vi.fn();
manager = new ExecApprovalManager(sendToClient as any, 5000); // 5s timeout for tests
});
afterEach(() => {
vi.useRealTimers();
});
it("sends approval request to client and resolves on decision", async () => {
const promise = manager.requestApproval({
agentId: "agent-1",
command: "rm -rf /tmp/test",
cwd: "/workspace",
riskLevel: "dangerous",
riskReasons: ["Recursive delete"],
});
// Verify sendToClient was called
expect(sendToClient).toHaveBeenCalledTimes(1);
const [agentId, request] = sendToClient.mock.calls[0]!;
expect(agentId).toBe("agent-1");
expect(request.command).toBe("rm -rf /tmp/test");
expect(request.approvalId).toBeTruthy();
// Resolve the approval
const resolved = manager.resolveApproval(request.approvalId, "allow-once");
expect(resolved).toBe(true);
const result = await promise;
expect(result.approved).toBe(true);
expect(result.decision).toBe("allow-once");
});
it("resolves with deny when decision is deny", async () => {
const promise = manager.requestApproval({
agentId: "agent-1",
command: "sudo reboot",
riskLevel: "dangerous",
riskReasons: [],
});
const request = sendToClient.mock.calls[0]![1];
manager.resolveApproval(request.approvalId, "deny");
const result = await promise;
expect(result.approved).toBe(false);
expect(result.decision).toBe("deny");
});
it("resolves with allow-always", async () => {
const promise = manager.requestApproval({
agentId: "agent-1",
command: "git push",
riskLevel: "needs-review",
riskReasons: [],
});
const request = sendToClient.mock.calls[0]![1];
manager.resolveApproval(request.approvalId, "allow-always");
const result = await promise;
expect(result.approved).toBe(true);
expect(result.decision).toBe("allow-always");
});
it("auto-denies on timeout (fail-closed)", async () => {
const promise = manager.requestApproval({
agentId: "agent-1",
command: "dangerous-command",
riskLevel: "dangerous",
riskReasons: [],
});
// Fast-forward past timeout
vi.advanceTimersByTime(6000);
const result = await promise;
expect(result.approved).toBe(false);
expect(result.decision).toBe("deny");
});
it("keeps approval pending indefinitely when timeoutMs is -1", async () => {
const promise = manager.requestApproval({
agentId: "agent-1",
command: "cmd",
riskLevel: "needs-review",
riskReasons: [],
timeoutMs: -1,
});
const request = sendToClient.mock.calls[0]![1];
expect(request.expiresAtMs).toBe(-1);
vi.advanceTimersByTime(60_000);
expect(manager.pendingCount).toBe(1);
manager.resolveApproval(request.approvalId, "allow-once");
const result = await promise;
expect(result.approved).toBe(true);
expect(result.decision).toBe("allow-once");
});
it("honors askFallback full on timeout", async () => {
const promise = manager.requestApproval({
agentId: "agent-1",
command: "cmd",
riskLevel: "needs-review",
riskReasons: [],
askFallback: "full",
});
vi.advanceTimersByTime(6000);
const result = await promise;
expect(result.approved).toBe(true);
expect(result.decision).toBe("allow-once");
});
it("honors askFallback allowlist on timeout", async () => {
const allowPromise = manager.requestApproval({
agentId: "agent-1",
command: "cmd",
riskLevel: "needs-review",
riskReasons: [],
askFallback: "allowlist",
allowlistSatisfied: true,
});
vi.advanceTimersByTime(6000);
const allowResult = await allowPromise;
expect(allowResult.approved).toBe(true);
expect(allowResult.decision).toBe("allow-once");
const denyPromise = manager.requestApproval({
agentId: "agent-1",
command: "cmd",
riskLevel: "needs-review",
riskReasons: [],
askFallback: "allowlist",
allowlistSatisfied: false,
});
vi.advanceTimersByTime(6000);
const denyResult = await denyPromise;
expect(denyResult.approved).toBe(false);
expect(denyResult.decision).toBe("deny");
});
it("returns false when resolving unknown approval", () => {
const resolved = manager.resolveApproval("unknown-id", "allow-once");
expect(resolved).toBe(false);
});
it("returns false when resolving already-resolved approval", async () => {
const promise = manager.requestApproval({
agentId: "agent-1",
command: "cmd",
riskLevel: "needs-review",
riskReasons: [],
});
const request = sendToClient.mock.calls[0]![1];
// First resolve succeeds
expect(manager.resolveApproval(request.approvalId, "allow-once")).toBe(true);
// Second resolve fails
expect(manager.resolveApproval(request.approvalId, "deny")).toBe(false);
await promise;
});
it("cancels all pending approvals for an agent", async () => {
const promise1 = manager.requestApproval({
agentId: "agent-1",
command: "cmd1",
riskLevel: "needs-review",
riskReasons: [],
});
const promise2 = manager.requestApproval({
agentId: "agent-1",
command: "cmd2",
riskLevel: "needs-review",
riskReasons: [],
});
const promise3 = manager.requestApproval({
agentId: "agent-2",
command: "cmd3",
riskLevel: "needs-review",
riskReasons: [],
});
// Cancel agent-1's approvals
manager.cancelPending("agent-1");
const result1 = await promise1;
const result2 = await promise2;
expect(result1.approved).toBe(false);
expect(result1.decision).toBe("deny");
expect(result2.approved).toBe(false);
expect(result2.decision).toBe("deny");
// agent-2's approval should still be pending
expect(manager.pendingCount).toBe(1);
// Resolve agent-2's approval
const request3 = sendToClient.mock.calls[2]![1];
manager.resolveApproval(request3.approvalId, "allow-once");
const result3 = await promise3;
expect(result3.approved).toBe(true);
});
it("auto-denies when sendToClient throws", async () => {
const failingSender = vi.fn().mockImplementation(() => {
throw new Error("Connection lost");
});
const failManager = new ExecApprovalManager(failingSender, 5000);
const result = await failManager.requestApproval({
agentId: "agent-1",
command: "cmd",
riskLevel: "needs-review",
riskReasons: [],
});
expect(result.approved).toBe(false);
expect(result.decision).toBe("deny");
});
it("getSnapshot returns request details", () => {
manager.requestApproval({
agentId: "agent-1",
command: "ls",
riskLevel: "safe",
riskReasons: [],
});
const request = sendToClient.mock.calls[0]![1];
const snapshot = manager.getSnapshot(request.approvalId);
expect(snapshot).toBeTruthy();
expect(snapshot!.command).toBe("ls");
expect(snapshot!.agentId).toBe("agent-1");
});
it("getSnapshot returns null for unknown id", () => {
expect(manager.getSnapshot("unknown")).toBeNull();
});
it("tracks pendingCount correctly", () => {
expect(manager.pendingCount).toBe(0);
manager.requestApproval({
agentId: "agent-1",
command: "cmd1",
riskLevel: "needs-review",
riskReasons: [],
});
expect(manager.pendingCount).toBe(1);
manager.requestApproval({
agentId: "agent-1",
command: "cmd2",
riskLevel: "needs-review",
riskReasons: [],
});
expect(manager.pendingCount).toBe(2);
const request = sendToClient.mock.calls[0]![1];
manager.resolveApproval(request.approvalId, "deny");
expect(manager.pendingCount).toBe(1);
});
});