Files
fusion/packages/engine/src/__tests__/agent-tools-provisioning-approval.test.ts
Fusion 3ec12f10e7 feat(FN-3791): add agent provisioning approval guards and policy enforcemen
Implements approval guards for agent provisioning (FN-3791), adding policy-gated create/delete flows with dedupe logic, CLI tool alignment, engine run-audit coverage, and corresponding test suites, plus documentation updates and a regression fix for verification/tool docs sync.

Fusion-Task-Id: FN-3791
2026-05-10 23:36:10 -07:00

142 lines
5.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Agent, AgentStore, ApprovalRequestStore, ProjectSettings } from "@fusion/core";
import { createAgentCreateTool, createAgentDeleteTool, executeApprovedAgentProvisioning } from "../agent-tools.js";
function makeAgent(overrides: Partial<Agent> = {}): Agent {
const now = new Date().toISOString();
return {
id: "agent-caller",
name: "Caller",
role: "executor",
reportsTo: "agent-root",
state: "idle",
createdAt: now,
updatedAt: now,
metadata: {},
...overrides,
};
}
const withProvisioning = (agentProvisioning: NonNullable<ProjectSettings["agentProvisioning"]>): ProjectSettings => ({
maxConcurrent: 2,
maxWorktrees: 2,
pollIntervalMs: 5000,
groupOverlappingFiles: true,
autoMerge: false,
autoResolveConflicts: true,
agentProvisioning,
});
describe("agent provisioning approval tools", () => {
let agentStore: AgentStore;
let approvalRequestStore: ApprovalRequestStore;
beforeEach(() => {
const caller = makeAgent({ id: "agent-caller", role: "executor" });
const target = makeAgent({ id: "agent-target", reportsTo: "agent-caller" });
agentStore = {
getAgent: vi.fn(async (id: string) => (id === caller.id ? caller : id === target.id ? target : null)),
createAgent: vi.fn(async (input: any) => makeAgent({ id: "agent-created", name: input.name, role: input.role })),
deleteAgent: vi.fn(async () => undefined),
} as unknown as AgentStore;
approvalRequestStore = {
create: vi.fn((input: any) => ({
id: "APR-1",
status: "pending",
requester: input.requester,
targetAction: input.targetAction,
})),
} as unknown as ApprovalRequestStore;
});
it("creates pending approval for untrusted create and includes approvalDedupeKey", async () => {
const tool = createAgentCreateTool(agentStore, "agent-caller", {
approvalRequestStore,
settingsProvider: async () => withProvisioning({ approvalMode: "trusted-only" }),
});
const result = await tool.execute("s", { name: "New Agent", role: "executor" } as any, undefined as any, undefined as any, undefined as any);
expect((result.details as any).outcome).toBe("pending_approval");
expect(approvalRequestStore.create).toHaveBeenCalledTimes(1);
const context = vi.mocked(approvalRequestStore.create).mock.calls[0]?.[0]?.targetAction?.context as any;
expect(context.tool).toBe("fn_agent_create");
expect(typeof context.approvalDedupeKey).toBe("string");
expect(context.approvalDedupeKey.length).toBeGreaterThan(0);
expect(agentStore.createAgent).not.toHaveBeenCalled();
});
it("auto-approves trusted role create", async () => {
vi.mocked(agentStore.getAgent).mockResolvedValueOnce(makeAgent({ id: "agent-caller", role: "ceo" as any }));
const tool = createAgentCreateTool(agentStore, "agent-caller", {
approvalRequestStore,
settingsProvider: async () => withProvisioning({ approvalMode: "trusted-only", trustedRoles: ["ceo"] }),
});
const result = await tool.execute("s", { name: "New Agent", role: "executor" } as any, undefined as any, undefined as any, undefined as any);
expect((result.details as any).outcome).toBe("created");
expect(agentStore.createAgent).toHaveBeenCalledTimes(1);
});
it("delete requires approval by default and includes approvalDedupeKey", async () => {
const tool = createAgentDeleteTool(agentStore, "agent-caller", {
approvalRequestStore,
settingsProvider: async () => withProvisioning({ approvalMode: "trusted-only", trustedAgentIds: ["agent-caller"] }),
});
const result = await tool.execute("s", { agent_id: "agent-target" } as any, undefined as any, undefined as any, undefined as any);
expect((result.details as any).outcome).toBe("pending_approval");
const context = vi.mocked(approvalRequestStore.create).mock.calls[0]?.[0]?.targetAction?.context as any;
expect(context.tool).toBe("fn_agent_delete");
expect(typeof context.approvalDedupeKey).toBe("string");
expect(agentStore.deleteAgent).not.toHaveBeenCalled();
});
it("allows trusted delete when alwaysApproveDelete is false", async () => {
const tool = createAgentDeleteTool(agentStore, "agent-caller", {
approvalRequestStore,
settingsProvider: async () => withProvisioning({
approvalMode: "trusted-only",
trustedAgentIds: ["agent-caller"],
alwaysApproveDelete: false,
}),
});
const result = await tool.execute("s", { agent_id: "agent-target" } as any, undefined as any, undefined as any, undefined as any);
expect((result.details as any).outcome).toBe("deleted");
expect(agentStore.deleteAgent).toHaveBeenCalledWith("agent-target", { force: false, reassignTo: undefined });
});
it("executeApprovedAgentProvisioning creates/deletes from request payload", async () => {
const created = await executeApprovedAgentProvisioning({
id: "APR-C",
status: "approved",
targetAction: {
category: "agent_provisioning",
action: "create",
summary: "",
resourceType: "agent",
resourceId: "",
context: { tool: "fn_agent_create", params: { name: "X", role: "executor" } },
},
} as any, { agentStore });
expect((created as Agent).name).toBe("X");
const deleted = await executeApprovedAgentProvisioning({
id: "APR-D",
status: "approved",
targetAction: {
category: "agent_provisioning",
action: "delete",
summary: "",
resourceType: "agent",
resourceId: "agent-target",
context: { tool: "fn_agent_delete", params: { agent_id: "agent-target" } },
},
} as any, { agentStore });
expect(deleted).toEqual({ deletedId: "agent-target" });
});
});