Files
letta-code/src/tests/permissions-mode.test.ts
2025-12-29 17:20:34 -08:00

455 lines
11 KiB
TypeScript

import { afterEach, expect, test } from "bun:test";
import { checkPermission } from "../permissions/checker";
import { permissionMode } from "../permissions/mode";
import type { PermissionRules } from "../permissions/types";
// Clean up after each test
afterEach(() => {
permissionMode.reset();
});
// ============================================================================
// Permission Mode: default
// ============================================================================
test("default mode - no overrides", () => {
permissionMode.setMode("default");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"Bash",
{ command: "curl http://example.com" }, // Use non-read-only command
permissions,
"/Users/test/project",
);
// Should fall back to tool default (ask for Bash)
expect(result.decision).toBe("ask");
expect(result.reason).toBe("Default behavior for tool");
});
// ============================================================================
// Permission Mode: bypassPermissions
// ============================================================================
test("bypassPermissions mode - allows all tools", () => {
permissionMode.setMode("bypassPermissions");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const bashResult = checkPermission(
"Bash",
{ command: "rm -rf /" },
permissions,
"/Users/test/project",
);
expect(bashResult.decision).toBe("allow");
expect(bashResult.reason).toBe("Permission mode: bypassPermissions");
const writeResult = checkPermission(
"Write",
{ file_path: "/etc/passwd" },
permissions,
"/Users/test/project",
);
expect(writeResult.decision).toBe("allow");
});
test("bypassPermissions mode - does NOT override deny rules", () => {
permissionMode.setMode("bypassPermissions");
const permissions: PermissionRules = {
allow: [],
deny: ["Bash(rm -rf:*)"],
ask: [],
};
const result = checkPermission(
"Bash",
{ command: "rm -rf /" },
permissions,
"/Users/test/project",
);
// Deny rules take precedence even in bypassPermissions mode
expect(result.decision).toBe("deny");
expect(result.reason).toBe("Matched deny rule");
});
// ============================================================================
// Permission Mode: acceptEdits
// ============================================================================
test("acceptEdits mode - allows Write", () => {
permissionMode.setMode("acceptEdits");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"Write",
{ file_path: "/tmp/test.txt" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("acceptEdits mode");
expect(result.reason).toBe("Permission mode: acceptEdits");
});
test("acceptEdits mode - allows Edit", () => {
permissionMode.setMode("acceptEdits");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"Edit",
{ file_path: "/tmp/test.txt", old_string: "old", new_string: "new" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("acceptEdits mode");
});
test("acceptEdits mode - allows NotebookEdit", () => {
permissionMode.setMode("acceptEdits");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"NotebookEdit",
{ notebook_path: "/tmp/test.ipynb", new_source: "code" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("acceptEdits mode");
});
test("acceptEdits mode - does NOT allow Bash", () => {
permissionMode.setMode("acceptEdits");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"Bash",
{ command: "curl http://example.com" }, // Use non-read-only command
permissions,
"/Users/test/project",
);
// Bash is not an edit tool, should fall back to default
expect(result.decision).toBe("ask");
expect(result.reason).toBe("Default behavior for tool");
});
// ============================================================================
// Permission Mode: plan
// ============================================================================
test("plan mode - allows Read", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"Read",
{ file_path: "/tmp/test.txt" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("plan mode");
});
test("plan mode - allows Glob", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"Glob",
{ pattern: "**/*.ts" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("plan mode");
});
test("plan mode - allows Grep", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"Grep",
{ pattern: "import", path: "/tmp" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("plan mode");
});
test("plan mode - allows TodoWrite", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"TodoWrite",
{ todos: [] },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("plan mode");
});
test("plan mode - denies Write", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"Write",
{ file_path: "/tmp/test.txt" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
expect(result.matchedRule).toBe("plan mode");
// Reason now includes detailed guidance (planFilePath not set in test, so shows error fallback)
expect(result.reason).toContain("Plan mode is active");
});
test("plan mode - denies non-read-only Bash", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"Bash",
{ command: "npm install" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
expect(result.matchedRule).toBe("plan mode");
});
test("plan mode - allows read-only Bash commands", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
// ls should be allowed
const lsResult = checkPermission(
"Bash",
{ command: "ls -la" },
permissions,
"/Users/test/project",
);
expect(lsResult.decision).toBe("allow");
expect(lsResult.matchedRule).toBe("plan mode");
// git status should be allowed
const gitStatusResult = checkPermission(
"Bash",
{ command: "git status" },
permissions,
"/Users/test/project",
);
expect(gitStatusResult.decision).toBe("allow");
// git log should be allowed
const gitLogResult = checkPermission(
"Bash",
{ command: "git log --oneline -10" },
permissions,
"/Users/test/project",
);
expect(gitLogResult.decision).toBe("allow");
// git diff should be allowed
const gitDiffResult = checkPermission(
"Bash",
{ command: "git diff HEAD~1" },
permissions,
"/Users/test/project",
);
expect(gitDiffResult.decision).toBe("allow");
// cd && git should be allowed (common CLI pattern)
const cdGitResult = checkPermission(
"Bash",
{ command: "cd /some/path && git status" },
permissions,
"/Users/test/project",
);
expect(cdGitResult.decision).toBe("allow");
// cd && git show should be allowed
const cdGitShowResult = checkPermission(
"Bash",
{ command: "cd /some/path && git show abc123" },
permissions,
"/Users/test/project",
);
expect(cdGitShowResult.decision).toBe("allow");
// chained safe commands with ; should be allowed
const chainedResult = checkPermission(
"Bash",
{ command: "ls; pwd; git status" },
permissions,
"/Users/test/project",
);
expect(chainedResult.decision).toBe("allow");
// cd && dangerous command should still be denied
const cdDangerousResult = checkPermission(
"Bash",
{ command: "cd /some/path && npm install" },
permissions,
"/Users/test/project",
);
expect(cdDangerousResult.decision).toBe("deny");
});
test("plan mode - denies WebFetch", () => {
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
const result = checkPermission(
"WebFetch",
{ url: "https://example.com" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
expect(result.matchedRule).toBe("plan mode");
});
// ============================================================================
// Precedence Tests
// ============================================================================
test("Deny rules override permission mode", () => {
permissionMode.setMode("bypassPermissions");
const permissions: PermissionRules = {
allow: [],
deny: ["Write(**)"],
ask: [],
};
const result = checkPermission(
"Write",
{ file_path: "/tmp/test.txt" },
permissions,
"/Users/test/project",
);
// Deny rule takes precedence over bypassPermissions
expect(result.decision).toBe("deny");
expect(result.reason).toBe("Matched deny rule");
});
test("Permission mode takes precedence over CLI allowedTools", () => {
const { cliPermissions } = require("../permissions/cli");
cliPermissions.setAllowedTools("Bash");
permissionMode.setMode("plan");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
};
// Use a non-read-only command to test precedence
const result = checkPermission(
"Bash",
{ command: "npm install" },
permissions,
"/Users/test/project",
);
// Permission mode denies take precedence over CLI allowedTools
expect(result.decision).toBe("deny");
expect(result.reason).toContain("Plan mode is active");
// Clean up
cliPermissions.clear();
});