Files
letta-code/src/tests/permissions-checker.test.ts
2025-10-25 11:33:30 -07:00

556 lines
13 KiB
TypeScript

import { expect, test } from "bun:test";
import { checkPermission } from "../permissions/checker";
import { sessionPermissions } from "../permissions/session";
import type { PermissionRules } from "../permissions/types";
// ============================================================================
// Working Directory Tests
// ============================================================================
test("Read within working directory is auto-allowed", () => {
if (process.platform === "win32") return; // Skip on Windows - Unix paths
const result = checkPermission(
"Read",
{ file_path: "src/test.ts" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.reason).toBe("Within working directory");
});
test("Read outside working directory requires permission", () => {
const result = checkPermission(
"Read",
{ file_path: "/Users/test/other/file.ts" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
// Default for Read is allow, but not within working directory
expect(result.decision).toBe("allow");
expect(result.reason).toBe("Default behavior for tool");
});
test("Glob within working directory is auto-allowed", () => {
const result = checkPermission(
"Glob",
{ path: "/Users/test/project/src" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("allow");
});
test("Grep within working directory is auto-allowed", () => {
const result = checkPermission(
"Grep",
{ path: "/Users/test/project" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("allow");
});
// ============================================================================
// Additional Directories Tests
// ============================================================================
test("Read in additional directory is auto-allowed", () => {
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
additionalDirectories: ["../docs"],
};
const result = checkPermission(
"Read",
{ file_path: "/Users/test/docs/api.md" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.reason).toBe("Within working directory");
});
test("Multiple additional directories", () => {
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: [],
additionalDirectories: ["../docs", "~/shared"],
};
const result1 = checkPermission(
"Read",
{ file_path: "/Users/test/docs/file.md" },
permissions,
"/Users/test/project",
);
expect(result1.decision).toBe("allow");
const homedir = require("node:os").homedir();
const result2 = checkPermission(
"Read",
{ file_path: `${homedir}/shared/file.txt` },
permissions,
"/Users/test/project",
);
expect(result2.decision).toBe("allow");
});
// ============================================================================
// Deny Rule Precedence Tests
// ============================================================================
test("Deny rule overrides working directory auto-allow", () => {
const permissions: PermissionRules = {
allow: [],
deny: ["Read(.env)"],
ask: [],
};
const result = checkPermission(
"Read",
{ file_path: ".env" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
expect(result.matchedRule).toBe("Read(.env)");
});
test("Deny pattern blocks multiple files", () => {
const permissions: PermissionRules = {
allow: [],
deny: ["Read(.env.*)"],
ask: [],
};
const result1 = checkPermission(
"Read",
{ file_path: ".env.local" },
permissions,
"/Users/test/project",
);
expect(result1.decision).toBe("deny");
const result2 = checkPermission(
"Read",
{ file_path: ".env.production" },
permissions,
"/Users/test/project",
);
expect(result2.decision).toBe("deny");
});
test("Deny directory blocks all files within", () => {
const permissions: PermissionRules = {
allow: [],
deny: ["Read(secrets/**)"],
ask: [],
};
const result = checkPermission(
"Read",
{ file_path: "secrets/api-key.txt" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
});
// ============================================================================
// Allow Rule Tests
// ============================================================================
test("Allow rule for file outside working directory", () => {
if (process.platform === "win32") return; // Skip on Windows - Unix paths
const permissions: PermissionRules = {
allow: ["Read(/Users/test/docs/**)"],
deny: [],
ask: [],
};
const result = checkPermission(
"Read",
{ file_path: "/Users/test/docs/api.md" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("Read(/Users/test/docs/**)");
});
test("Allow rule for Bash command", () => {
const permissions: PermissionRules = {
allow: ["Bash(git diff:*)"],
deny: [],
ask: [],
};
const result = checkPermission(
"Bash",
{ command: "git diff HEAD" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBe("Bash(git diff:*)");
});
test("Allow exact Bash command", () => {
const permissions: PermissionRules = {
allow: ["Bash(npm run lint)"],
deny: [],
ask: [],
};
const result1 = checkPermission(
"Bash",
{ command: "npm run lint" },
permissions,
"/Users/test/project",
);
expect(result1.decision).toBe("allow");
const result2 = checkPermission(
"Bash",
{ command: "npm run lint --fix" },
permissions,
"/Users/test/project",
);
expect(result2.decision).toBe("ask"); // Doesn't match exact
});
// ============================================================================
// Ask Rule Tests
// ============================================================================
test("Ask rule forces prompt", () => {
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: ["Bash(git push:*)"],
};
const result = checkPermission(
"Bash",
{ command: "git push origin main" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("ask");
expect(result.matchedRule).toBe("Bash(git push:*)");
});
test("Ask rule for specific file pattern", () => {
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: ["Write(**/*.sql)"],
};
const result = checkPermission(
"Write",
{ file_path: "migrations/001.sql" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("ask");
});
// ============================================================================
// Default Behavior Tests
// ============================================================================
test("Read defaults to allow", () => {
const result = checkPermission(
"Read",
{ file_path: "/some/external/file.txt" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.reason).toBe("Default behavior for tool");
});
test("Bash defaults to ask", () => {
const result = checkPermission(
"Bash",
{ command: "ls -la" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("ask");
expect(result.reason).toBe("Default behavior for tool");
});
test("Write defaults to ask", () => {
const result = checkPermission(
"Write",
{ file_path: "new-file.txt" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("ask");
});
test("Edit defaults to ask", () => {
const result = checkPermission(
"Edit",
{ file_path: "existing-file.txt" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("ask");
});
test("TodoWrite defaults to allow", () => {
const result = checkPermission(
"TodoWrite",
{ todos: [] },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("allow");
});
// ============================================================================
// Precedence Order Tests
// ============================================================================
test("Deny takes precedence over allow", () => {
const permissions: PermissionRules = {
allow: ["Read(secrets/**)"],
deny: ["Read(secrets/**)"],
ask: [],
};
const result = checkPermission(
"Read",
{ file_path: "secrets/key.txt" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
});
test("Deny takes precedence over working directory", () => {
const permissions: PermissionRules = {
allow: [],
deny: ["Read(.env)"],
ask: [],
};
const result = checkPermission(
"Read",
{ file_path: ".env" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
});
test("Allow takes precedence over ask", () => {
const permissions: PermissionRules = {
allow: ["Bash(git diff:*)"],
deny: [],
ask: ["Bash(git diff:*)"],
};
const result = checkPermission(
"Bash",
{ command: "git diff HEAD" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
});
test("Ask takes precedence over default", () => {
if (process.platform === "win32") return; // Skip on Windows - Unix paths
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: ["Read(/etc/**)"],
};
const result = checkPermission(
"Read",
{ file_path: "/etc/hosts" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("ask");
});
// ============================================================================
// Session Permission Tests (Integration)
// ============================================================================
test("Session allow rule takes precedence over persisted allow", () => {
// Add a session rule
sessionPermissions.clear();
sessionPermissions.addRule("Bash(git push:*)", "allow");
const permissions: PermissionRules = {
allow: [],
deny: [],
ask: ["Bash(git push:*)"], // Would normally ask
};
const result = checkPermission(
"Bash",
{ command: "git push origin main" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toContain("session");
// Clean up
sessionPermissions.clear();
});
test("Session rules don't persist after clear", () => {
sessionPermissions.clear();
sessionPermissions.addRule("Bash(ls:*)", "allow");
expect(sessionPermissions.hasRule("Bash(ls:*)", "allow")).toBe(true);
sessionPermissions.clear();
expect(sessionPermissions.hasRule("Bash(ls:*)", "allow")).toBe(false);
});
// ============================================================================
// Edge Cases and Error Handling
// ============================================================================
test("Missing file_path parameter", () => {
const result = checkPermission(
"Read",
{}, // No file_path
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
// Should fall back to default
expect(result.decision).toBe("allow");
});
test("Missing command parameter for Bash", () => {
const result = checkPermission(
"Bash",
{}, // No command
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
// Should fall back to default
expect(result.decision).toBe("ask");
});
test("Unknown tool defaults to ask", () => {
const result = checkPermission(
"UnknownTool",
{},
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("ask");
expect(result.reason).toBe("Default behavior for tool");
});
test("Empty permissions object", () => {
const result = checkPermission(
"Read",
{ file_path: "test.txt" },
{}, // No arrays defined
"/Users/test/project",
);
expect(result.decision).toBe("allow");
});
test("Relative path normalization", () => {
const permissions: PermissionRules = {
allow: [],
deny: ["Read(./secrets/**)"],
ask: [],
};
const result = checkPermission(
"Read",
{ file_path: "secrets/key.txt" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
});
test("Parent directory traversal", () => {
const result = checkPermission(
"Read",
{ file_path: "../other-project/file.txt" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
// Outside working directory, uses default
expect(result.decision).toBe("allow");
expect(result.reason).toBe("Default behavior for tool");
});
test("Absolute path handling", () => {
if (process.platform === "win32") return; // Skip on Windows - Unix paths
const permissions: PermissionRules = {
allow: [],
deny: ["Read(/etc/**)"],
ask: [],
};
const result = checkPermission(
"Read",
{ file_path: "/etc/hosts" },
permissions,
"/Users/test/project",
);
expect(result.decision).toBe("deny");
});
test("Tool with alternative path parameter (Glob uses 'path' not 'file_path')", () => {
const result = checkPermission(
"Glob",
{ path: "src" },
{ allow: [], deny: [], ask: [] },
"/Users/test/project",
);
expect(result.decision).toBe("allow");
});