556 lines
13 KiB
TypeScript
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");
|
|
});
|