refactor: simplify task tool subagent filtering (#7165)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
This commit is contained in:
M. Adel Alhashemi
2026-01-07 22:28:13 +03:00
committed by GitHub
parent fe57d7bb38
commit 34c9d106ee
3 changed files with 36 additions and 198 deletions

View File

@@ -1,147 +1,9 @@
import { describe, test, expect } from "bun:test"
import type { Agent } from "../src/agent/agent"
import { filterSubagents } from "../src/tool/task"
import { PermissionNext } from "../src/permission/next"
import { Config } from "../src/config/config"
import { Instance } from "../src/project/instance"
import { tmpdir } from "./fixture/fixture"
describe("filterSubagents - permission.task filtering", () => {
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionNext.Ruleset =>
Object.entries(rules).map(([pattern, action]) => ({
permission: "task",
pattern,
action,
}))
const mockAgents = [
{ name: "general", mode: "subagent", permission: [], options: {} },
{ name: "code-reviewer", mode: "subagent", permission: [], options: {} },
{ name: "orchestrator-fast", mode: "subagent", permission: [], options: {} },
{ name: "orchestrator-slow", mode: "subagent", permission: [], options: {} },
] as Agent.Info[]
test("returns all agents when permissions config is empty", () => {
const result = filterSubagents(mockAgents, [])
expect(result).toHaveLength(4)
expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer", "orchestrator-fast", "orchestrator-slow"])
})
test("excludes agents with explicit deny", () => {
const ruleset = createRuleset({ "code-reviewer": "deny" })
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(3)
expect(result.map((a) => a.name)).toEqual(["general", "orchestrator-fast", "orchestrator-slow"])
})
test("includes agents with explicit allow", () => {
const ruleset = createRuleset({
"code-reviewer": "allow",
general: "deny",
})
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(3)
expect(result.map((a) => a.name)).toEqual(["code-reviewer", "orchestrator-fast", "orchestrator-slow"])
})
test("includes agents with ask permission (user approval is runtime behavior)", () => {
const ruleset = createRuleset({
"code-reviewer": "ask",
general: "deny",
})
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(3)
expect(result.map((a) => a.name)).toEqual(["code-reviewer", "orchestrator-fast", "orchestrator-slow"])
})
test("includes agents with undefined permission (default allow)", () => {
const ruleset = createRuleset({
general: "deny",
})
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(3)
expect(result.map((a) => a.name)).toEqual(["code-reviewer", "orchestrator-fast", "orchestrator-slow"])
})
test("supports wildcard patterns with deny", () => {
const ruleset = createRuleset({ "orchestrator-*": "deny" })
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(2)
expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer"])
})
test("supports wildcard patterns with allow", () => {
const ruleset = createRuleset({
"*": "allow",
"orchestrator-fast": "deny",
})
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(3)
expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer", "orchestrator-slow"])
})
test("supports wildcard patterns with ask", () => {
const ruleset = createRuleset({
"orchestrator-*": "ask",
})
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(4)
expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer", "orchestrator-fast", "orchestrator-slow"])
})
test("longer pattern takes precedence over shorter pattern", () => {
const ruleset = createRuleset({
"orchestrator-*": "deny",
"orchestrator-fast": "allow",
})
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(3)
expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer", "orchestrator-fast"])
})
test("edge case: all agents denied", () => {
const ruleset = createRuleset({ "*": "deny" })
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(0)
expect(result).toEqual([])
})
test("edge case: mixed patterns with multiple wildcards", () => {
const ruleset = createRuleset({
"*": "ask",
"orchestrator-*": "deny",
"orchestrator-fast": "allow",
})
const result = filterSubagents(mockAgents, ruleset)
expect(result).toHaveLength(3)
expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer", "orchestrator-fast"])
})
test("hidden: true does not affect filtering (hidden only affects autocomplete)", () => {
const agents = [
{ name: "general", mode: "subagent", hidden: true, permission: [], options: {} },
{ name: "code-reviewer", mode: "subagent", hidden: false, permission: [], options: {} },
{ name: "orchestrator", mode: "subagent", permission: [], options: {} },
] as Agent.Info[]
const result = filterSubagents(agents, [])
expect(result).toHaveLength(3)
expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer", "orchestrator"])
})
test("hidden: true agents can be filtered by permission.task deny", () => {
const agents = [
{ name: "general", mode: "subagent", hidden: true, permission: [], options: {} },
{ name: "orchestrator-coder", mode: "subagent", hidden: true, permission: [], options: {} },
] as Agent.Info[]
const ruleset = createRuleset({ general: "deny" })
const result = filterSubagents(agents, ruleset)
expect(result).toHaveLength(1)
expect(result.map((a) => a.name)).toEqual(["orchestrator-coder"])
})
})
describe("PermissionNext.evaluate for permission.task", () => {
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionNext.Ruleset =>
Object.entries(rules).map(([pattern, action]) => ({
@@ -277,12 +139,6 @@ describe("PermissionNext.disabled for task tool", () => {
// Integration tests that load permissions from real config files
describe("permission.task with real config files", () => {
const mockAgents = [
{ name: "general", mode: "subagent", permission: [], options: {} },
{ name: "code-reviewer", mode: "subagent", permission: [], options: {} },
{ name: "orchestrator-fast", mode: "subagent", permission: [], options: {} },
] as Agent.Info[]
test("loads task permissions from opencode.json config", async () => {
await using tmp = await tmpdir({
git: true,
@@ -300,8 +156,10 @@ describe("permission.task with real config files", () => {
fn: async () => {
const config = await Config.get()
const ruleset = PermissionNext.fromConfig(config.permission ?? {})
const result = filterSubagents(mockAgents, ruleset)
expect(result.map((a) => a.name)).toEqual(["general", "orchestrator-fast"])
// 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")
},
})
})
@@ -323,8 +181,10 @@ describe("permission.task with real config files", () => {
fn: async () => {
const config = await Config.get()
const ruleset = PermissionNext.fromConfig(config.permission ?? {})
const result = filterSubagents(mockAgents, ruleset)
expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer"])
// 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")
},
})
})