import { describe, expect, it } from "bun:test"; import { mkdtemp, mkdir, rm, symlink } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { canAutoApprovePermission, resolvePermissionApproval, } from "../../src/routes/chatPermissionPolicy.js"; describe("permission approval policy", () => { it.each([ "show_chart", "web_search", "skill", ])("allows low-risk permission %s", (permission) => { expect(canAutoApprovePermission(permission)).toBe(true); }); it("allows structured searches within the workspace", () => { const workspaceRoot = process.cwd(); const context = { workspaceRoot, metadata: { path: join(workspaceRoot, "src"), include: "*.ts" }, patterns: ["*.ts"], }; expect(canAutoApprovePermission("glob", context)).toBe(true); expect(canAutoApprovePermission("grep", context)).toBe(true); }); it("allows a glob with an explicit safe prefix from the workspace root", () => { expect( canAutoApprovePermission("glob", { workspaceRoot: process.cwd(), metadata: { path: process.cwd(), pattern: "src/**/*.ts" }, patterns: ["src/**/*.ts"], }), ).toBe(true); }); it.each([ { metadata: { path: dirname(process.cwd()) }, patterns: ["*"] }, { metadata: { path: join(process.cwd(), ".local.env") }, patterns: ["*"] }, { metadata: { path: join(process.cwd(), "data") }, patterns: ["*"] }, { metadata: { path: join(process.cwd(), "src") }, patterns: ["../logs/**"] }, { metadata: { path: process.cwd() }, patterns: ["**/*.env"] }, { metadata: { path: process.cwd() }, patterns: ["**/*"] }, { metadata: { path: join(process.cwd(), "src") }, patterns: [".[e]nv"] }, ])("keeps protected or external searches interactive", (request) => { expect( canAutoApprovePermission("glob", { workspaceRoot: process.cwd(), ...request, }), ).toBe(false); }); it("keeps grep from the workspace root interactive", () => { expect( canAutoApprovePermission("grep", { workspaceRoot: process.cwd(), metadata: { path: process.cwd(), include: "*.ts" }, patterns: ["secret"], }), ).toBe(false); }); it("rejects a workspace symlink that resolves outside the workspace", async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), "permission-workspace-")); const externalRoot = await mkdtemp(join(tmpdir(), "permission-external-")); try { await mkdir(join(externalRoot, "src")); const linkedPath = join(workspaceRoot, "linked"); await symlink(join(externalRoot, "src"), linkedPath, "dir"); expect( canAutoApprovePermission("grep", { workspaceRoot, metadata: { path: linkedPath, include: "*.ts" }, patterns: ["secret"], }), ).toBe(false); } finally { await Promise.all([ rm(workspaceRoot, { force: true, recursive: true }), rm(externalRoot, { force: true, recursive: true }), ]); } }); it("rejects protected descendants below an otherwise safe search root", async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), "permission-descendant-")); try { const sourceRoot = join(workspaceRoot, "src"); await mkdir(join(sourceRoot, "data"), { recursive: true }); expect( canAutoApprovePermission("grep", { workspaceRoot, metadata: { path: sourceRoot, include: "*.ts" }, patterns: ["secret"], }), ).toBe(false); } finally { await rm(workspaceRoot, { force: true, recursive: true }); } }); it.each([ "bash", "edit", "external_directory", "store_render_ref", "tjwater_server_query", "tjwater_tjwater_server_query", ])( "requires confirmation for permission %s", (permission) => { expect(canAutoApprovePermission(permission)).toBe(false); }, ); it("resolves request, auto, and always modes", () => { expect(resolvePermissionApproval("request", "show_chart").autoApprove).toBe(false); expect(resolvePermissionApproval("auto", "show_chart").autoApprove).toBe(true); expect(resolvePermissionApproval("auto", "bash").autoApprove).toBe(false); expect(resolvePermissionApproval("always", "bash")).toMatchObject({ autoApprove: true, title: "已按始终允许模式放行", }); }); });