319 lines · 12.5 KB · TypeScript
RawDownload
import { describe, test, expect } from "bun:test"import { PermissionNext } from "../src/permission/next"import { Config } from "../src/config/config"import { Instance } from "../src/project/instance"import { tmpdir } from "./fixture/fixture" describe("PermissionNext.evaluate for permission.task", () => {  const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionNext.Ruleset =>    Object.entries(rules).map(([pattern, action]) => ({      permission: "task",      pattern,      action,    }))   test("returns ask when no match (default)", () => {    expect(PermissionNext.evaluate("task", "code-reviewer", []).action).toBe("ask")  })   test("returns deny for explicit deny", () => {    const ruleset = createRuleset({ "code-reviewer": "deny" })    expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")  })   test("returns allow for explicit allow", () => {    const ruleset = createRuleset({ "code-reviewer": "allow" })    expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("allow")  })   test("returns ask for explicit ask", () => {    const ruleset = createRuleset({ "code-reviewer": "ask" })    expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("ask")  })   test("matches wildcard patterns with deny", () => {    const ruleset = createRuleset({ "orchestrator-*": "deny" })    expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny")    expect(PermissionNext.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny")    expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("ask")  })   test("matches wildcard patterns with allow", () => {    const ruleset = createRuleset({ "orchestrator-*": "allow" })    expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow")    expect(PermissionNext.evaluate("task", "orchestrator-slow", ruleset).action).toBe("allow")  })   test("matches wildcard patterns with ask", () => {    const ruleset = createRuleset({ "orchestrator-*": "ask" })    expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("ask")    const globalRuleset = createRuleset({ "*": "ask" })    expect(PermissionNext.evaluate("task", "code-reviewer", globalRuleset).action).toBe("ask")  })   test("later rules take precedence (last match wins)", () => {    const ruleset = createRuleset({      "orchestrator-*": "deny",      "orchestrator-fast": "allow",    })    expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow")    expect(PermissionNext.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny")  })   test("matches global wildcard", () => {    expect(PermissionNext.evaluate("task", "any-agent", createRuleset({ "*": "allow" })).action).toBe("allow")    expect(PermissionNext.evaluate("task", "any-agent", createRuleset({ "*": "deny" })).action).toBe("deny")    expect(PermissionNext.evaluate("task", "any-agent", createRuleset({ "*": "ask" })).action).toBe("ask")  })}) describe("PermissionNext.disabled for task tool", () => {  // Note: The `disabled` function checks if a TOOL should be completely removed from the tool list.  // It only disables a tool when there's a rule with `pattern: "*"` and `action: "deny"`.  // It does NOT evaluate complex subagent patterns - those are handled at runtime by `evaluate`.  const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionNext.Ruleset =>    Object.entries(rules).map(([pattern, action]) => ({      permission: "task",      pattern,      action,    }))   test("task tool is disabled when global deny pattern exists (even with specific allows)", () => {    // When "*": "deny" exists, the task tool is disabled because the disabled() function    // only checks for wildcard deny patterns - it doesn't consider that specific subagents might be allowed    const ruleset = createRuleset({      "orchestrator-*": "allow",      "*": "deny",    })    const disabled = PermissionNext.disabled(["task", "bash", "read"], ruleset)    // The task tool IS disabled because there's a pattern: "*" with action: "deny"    expect(disabled.has("task")).toBe(true)  })   test("task tool is disabled when global deny pattern exists (even with ask overrides)", () => {    const ruleset = createRuleset({      "orchestrator-*": "ask",      "*": "deny",    })    const disabled = PermissionNext.disabled(["task"], ruleset)    // The task tool IS disabled because there's a pattern: "*" with action: "deny"    expect(disabled.has("task")).toBe(true)  })   test("task tool is disabled when global deny pattern exists", () => {    const ruleset = createRuleset({ "*": "deny" })    const disabled = PermissionNext.disabled(["task"], ruleset)    expect(disabled.has("task")).toBe(true)  })   test("task tool is NOT disabled when only specific patterns are denied (no wildcard)", () => {    // The disabled() function only disables tools when pattern: "*" && action: "deny"    // Specific subagent denies don't disable the task tool - those are handled at runtime    const ruleset = createRuleset({      "orchestrator-*": "deny",      general: "deny",    })    const disabled = PermissionNext.disabled(["task"], ruleset)    // The task tool is NOT disabled because no rule has pattern: "*" with action: "deny"    expect(disabled.has("task")).toBe(false)  })   test("task tool is enabled when no task rules exist (default ask)", () => {    const disabled = PermissionNext.disabled(["task"], [])    expect(disabled.has("task")).toBe(false)  })   test("task tool is NOT disabled when last wildcard pattern is allow", () => {    // Last matching rule wins - if wildcard allow comes after wildcard deny, tool is enabled    const ruleset = createRuleset({      "*": "deny",      "orchestrator-coder": "allow",    })    const disabled = PermissionNext.disabled(["task"], ruleset)    // The disabled() function uses findLast and checks if the last matching rule    // has pattern: "*" and action: "deny". In this case, the last rule matching    // "task" permission has pattern "orchestrator-coder", not "*", so not disabled    expect(disabled.has("task")).toBe(false)  })}) // Integration tests that load permissions from real config filesdescribe("permission.task with real config files", () => {  test("loads task permissions from openscience.json config", async () => {    await using tmp = await tmpdir({      git: true,      config: {        permission: {          task: {            "*": "allow",            "code-reviewer": "deny",          },        },      },    })    await Instance.provide({      directory: tmp.path,      fn: async () => {        const config = await Config.get()        const ruleset = PermissionNext.fromConfig(config.permission ?? {})        // general and orchestrator-fast should be allowed, code-reviewer denied        expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("allow")        expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow")        expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")      },    })  })   test("loads task permissions with wildcard patterns from config", async () => {    await using tmp = await tmpdir({      git: true,      config: {        permission: {          task: {            "*": "ask",            "orchestrator-*": "deny",          },        },      },    })    await Instance.provide({      directory: tmp.path,      fn: async () => {        const config = await Config.get()        const ruleset = PermissionNext.fromConfig(config.permission ?? {})        // general and code-reviewer should be ask, orchestrator-* denied        expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("ask")        expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("ask")        expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny")      },    })  })   test("evaluate respects task permission from config", async () => {    await using tmp = await tmpdir({      git: true,      config: {        permission: {          task: {            general: "allow",            "code-reviewer": "deny",          },        },      },    })    await Instance.provide({      directory: tmp.path,      fn: async () => {        const config = await Config.get()        const ruleset = PermissionNext.fromConfig(config.permission ?? {})        expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("allow")        expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")        // Unspecified agents default to "ask"        expect(PermissionNext.evaluate("task", "unknown-agent", ruleset).action).toBe("ask")      },    })  })   test("mixed permission config with task and other tools", async () => {    await using tmp = await tmpdir({      git: true,      config: {        permission: {          bash: "allow",          edit: "ask",          task: {            "*": "deny",            general: "allow",          },        },      },    })    await Instance.provide({      directory: tmp.path,      fn: async () => {        const config = await Config.get()        const ruleset = PermissionNext.fromConfig(config.permission ?? {})         // Verify task permissions        expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("allow")        expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")         // Verify other tool permissions        expect(PermissionNext.evaluate("bash", "*", ruleset).action).toBe("allow")        expect(PermissionNext.evaluate("edit", "*", ruleset).action).toBe("ask")         // Verify disabled tools        const disabled = PermissionNext.disabled(["bash", "edit", "task"], ruleset)        expect(disabled.has("bash")).toBe(false)        expect(disabled.has("edit")).toBe(false)        // task is NOT disabled because disabled() uses findLast, and the last rule        // matching "task" permission is {pattern: "general", action: "allow"}, not pattern: "*"        expect(disabled.has("task")).toBe(false)      },    })  })   test("task tool disabled when global deny comes last in config", async () => {    await using tmp = await tmpdir({      git: true,      config: {        permission: {          task: {            general: "allow",            "code-reviewer": "allow",            "*": "deny",          },        },      },    })    await Instance.provide({      directory: tmp.path,      fn: async () => {        const config = await Config.get()        const ruleset = PermissionNext.fromConfig(config.permission ?? {})         // Last matching rule wins - "*" deny is last, so all agents are denied        expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("deny")        expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")        expect(PermissionNext.evaluate("task", "unknown", ruleset).action).toBe("deny")         // Since "*": "deny" is the last rule, disabled() finds it with findLast        // and sees pattern: "*" with action: "deny", so task is disabled        const disabled = PermissionNext.disabled(["task"], ruleset)        expect(disabled.has("task")).toBe(true)      },    })  })   test("task tool NOT disabled when specific allow comes last in config", async () => {    await using tmp = await tmpdir({      git: true,      config: {        permission: {          task: {            "*": "deny",            general: "allow",          },        },      },    })    await Instance.provide({      directory: tmp.path,      fn: async () => {        const config = await Config.get()        const ruleset = PermissionNext.fromConfig(config.permission ?? {})         // Evaluate uses findLast - "general" allow comes after "*" deny        expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("allow")        // Other agents still denied by the earlier "*" deny        expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")         // disabled() uses findLast and checks if the last rule has pattern: "*" with action: "deny"        // In this case, the last rule is {pattern: "general", action: "allow"}, not pattern: "*"        // So the task tool is NOT disabled (even though most subagents are denied)        const disabled = PermissionNext.disabled(["task"], ruleset)        expect(disabled.has("task")).toBe(false)      },    })  })})

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t