Files
fusion/packages/engine/src/__tests__/gating-classifications.test.ts
Fusion (runfusion.ai) 8f7697198e test(FN-4624): add worktrunk installer tests and gating classification tests
Fusion-Task-Id: FN-4624
Fusion-Task-Lineage: 854e6033-bc17-45be-8d04-18bcb66b31c4
2026-05-15 20:24:58 -07:00

193 lines
7.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { evaluateAgentActionGate } from "../agent-action-gate.js";
import {
ACTION_GATE_NETWORK_API_TOOLS,
ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS,
COORDINATION_EXEMPT_TOOLS,
FILE_WRITE_DELETE_FN_TOOLS,
NETWORK_API_TOOLS,
READONLY_FN_TOOLS,
TASK_AGENT_MUTATION_TOOLS,
classifyGitCommand,
isGitWriteCommand,
} from "../gating-classifications.js";
import { classifyPermanentAgentToolCall, resolvePermanentAgentToolDecision } from "../permanent-agent-gating.js";
import type { AgentPermissionPolicy } from "@fusion/core";
const blockedPolicy: AgentPermissionPolicy = {
presetId: "locked-down",
rules: {
git_write: "block",
file_write_delete: "block",
command_execution: "block",
network_api: "block",
task_agent_mutation: "block",
},
};
const gitCases = [
["git status", false, "git status"],
["git diff", false, "git diff"],
["git log --oneline", false, "git log"],
["git show HEAD", false, "git show"],
["git add .", true, "git add"],
["git commit -m x", true, "git commit"],
["git branch", false, "git branch"],
["git branch --show-current", false, "git branch --show-current"],
["git branch feature", true, "git branch"],
["git branch -d feature", true, "git branch"],
["git switch main", false, "git switch"],
["git switch -c feature", true, "git switch -c"],
["git checkout main", false, "git checkout"],
["git checkout -b feature", true, "git checkout -b"],
["git pull", false, "git pull"],
["git pull --rebase", true, "git pull --rebase"],
["git restore file.ts", false, "git restore"],
["git restore --staged file.ts", true, "git restore --staged"],
["git remote -v", false, "git remote -v"],
["git remote add origin x", true, "git remote"],
["git remote set-url origin y", true, "git remote"],
["git worktree list", false, "git worktree"],
["git worktree add ../x", true, "git worktree add"],
["git worktree remove ../x", true, "git worktree remove"],
["echo hi && git status", false, "git status"],
["echo hi; git commit -m x", true, "git commit"],
["echo hi | git diff", false, "git diff"],
["echo hi\ngit checkout -b t", true, "git checkout -b"],
] as const;
const ACTION_MUTATION_PERMANENT_READONLY_TOOLS = new Set([
"fn_task_import_github",
"fn_task_import_github_issue",
]);
describe("gating-classifications parity", () => {
it("locks coordination exempt membership", () => {
expect([...COORDINATION_EXEMPT_TOOLS].sort()).toMatchInlineSnapshot(`
[
"find",
"fn_agent_org_chart",
"fn_agent_show",
"fn_delegate_task",
"fn_heartbeat_done",
"fn_list_agents",
"fn_memory_append",
"fn_memory_get",
"fn_memory_search",
"fn_post_room_message",
"fn_read_evaluations",
"fn_read_messages",
"fn_reflect_on_performance",
"fn_send_message",
"fn_task_create",
"fn_task_document_read",
"fn_task_document_write",
"fn_task_done",
"fn_task_log",
"fn_task_update",
"fn_update_identity",
"grep",
"ls",
"read",
]
`);
});
it("ensures coordination exempt tools are recognized and allowed in permanent gating", () => {
for (const toolName of COORDINATION_EXEMPT_TOOLS) {
const classification = classifyPermanentAgentToolCall(toolName);
const decision = resolvePermanentAgentToolDecision({
toolName,
gating: { permissionPolicy: blockedPolicy },
});
expect(classification.recognized).toBe(true);
expect(decision.disposition).toBe("allow");
expect(decision.category).toBe("none");
}
});
it("classifies fn_web_fetch as network_api in both action and permanent sets", () => {
expect(ACTION_GATE_NETWORK_API_TOOLS.has("fn_web_fetch")).toBe(true);
expect(NETWORK_API_TOOLS.has("fn_web_fetch")).toBe(true);
expect((COORDINATION_EXEMPT_TOOLS as readonly string[]).includes("fn_web_fetch")).toBe(false);
});
it("classifies worktrunk_install as network_api in both action and permanent sets", () => {
expect(ACTION_GATE_NETWORK_API_TOOLS.has("worktrunk_install")).toBe(true);
expect(NETWORK_API_TOOLS.has("worktrunk_install")).toBe(true);
expect((COORDINATION_EXEMPT_TOOLS as readonly string[]).includes("worktrunk_install")).toBe(false);
});
it("keeps fn_* category equivalence mappings across gates", () => {
const fnTools = new Set<string>();
for (const source of [
READONLY_FN_TOOLS,
TASK_AGENT_MUTATION_TOOLS,
ACTION_GATE_NETWORK_API_TOOLS,
FILE_WRITE_DELETE_FN_TOOLS,
NETWORK_API_TOOLS,
]) {
for (const toolName of source) {
if (toolName.startsWith("fn_")) fnTools.add(toolName);
}
}
for (const toolName of fnTools) {
const action = evaluateAgentActionGate({
agentId: "a1",
toolName,
args: {},
permissionPolicy: blockedPolicy,
});
const permanent = classifyPermanentAgentToolCall(toolName);
const actionKind = action.category === "task_agent_mutation"
? "mutating"
: action.category === "network_api"
? "network"
: action.category === "file_write_delete"
? "file-write"
: "readonly";
const permanentKind = permanent.category === "task_agent_mutation"
? "mutating"
: permanent.category === "network_api"
? "network"
: permanent.category === "file_write_delete"
? "file-write"
: "readonly";
if (FILE_WRITE_DELETE_FN_TOOLS.has(toolName)) {
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: "readonly", permanentKind: "file-write" });
continue;
}
if (NETWORK_API_TOOLS.has(toolName) && !ACTION_GATE_NETWORK_API_TOOLS.has(toolName)) {
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: "readonly", permanentKind: "network" });
continue;
}
if (TASK_AGENT_MUTATION_TOOLS.has(toolName) && !ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS.has(toolName)) {
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: "readonly", permanentKind: "mutating" });
continue;
}
if (ACTION_MUTATION_PERMANENT_READONLY_TOOLS.has(toolName)) {
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: "mutating", permanentKind: "readonly" });
continue;
}
expect({ toolName, actionKind, permanentKind }).toEqual({ toolName, actionKind: permanentKind, permanentKind });
}
});
it.each(gitCases)("classifyGitCommand handles %s", (command, write, operation) => {
expect(classifyGitCommand(command)).toEqual({ write, operation });
});
it("classifyGitCommand returns null when no git command is present", () => {
expect(classifyGitCommand("pnpm test")).toBeNull();
});
it.each(gitCases)("isGitWriteCommand agrees with classifyGitCommand for %s", (command) => {
expect(isGitWriteCommand(command)).toBe(classifyGitCommand(command)?.write ?? false);
});
});