11import { describe , test , expect } from "bun:test"
2- import type { Agent } from "../src/agent/agent"
3- import { filterSubagents } from "../src/tool/task"
42import { PermissionNext } from "../src/permission/next"
53import { Config } from "../src/config/config"
64import { Instance } from "../src/project/instance"
75import { tmpdir } from "./fixture/fixture"
86
9- describe ( "filterSubagents - permission.task filtering" , ( ) => {
10- const createRuleset = ( rules : Record < string , "allow" | "deny" | "ask" > ) : PermissionNext . Ruleset =>
11- Object . entries ( rules ) . map ( ( [ pattern , action ] ) => ( {
12- permission : "task" ,
13- pattern,
14- action,
15- } ) )
16-
17- const mockAgents = [
18- { name : "general" , mode : "subagent" , permission : [ ] , options : { } } ,
19- { name : "code-reviewer" , mode : "subagent" , permission : [ ] , options : { } } ,
20- { name : "orchestrator-fast" , mode : "subagent" , permission : [ ] , options : { } } ,
21- { name : "orchestrator-slow" , mode : "subagent" , permission : [ ] , options : { } } ,
22- ] as Agent . Info [ ]
23-
24- test ( "returns all agents when permissions config is empty" , ( ) => {
25- const result = filterSubagents ( mockAgents , [ ] )
26- expect ( result ) . toHaveLength ( 4 )
27- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "code-reviewer" , "orchestrator-fast" , "orchestrator-slow" ] )
28- } )
29-
30- test ( "excludes agents with explicit deny" , ( ) => {
31- const ruleset = createRuleset ( { "code-reviewer" : "deny" } )
32- const result = filterSubagents ( mockAgents , ruleset )
33- expect ( result ) . toHaveLength ( 3 )
34- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "orchestrator-fast" , "orchestrator-slow" ] )
35- } )
36-
37- test ( "includes agents with explicit allow" , ( ) => {
38- const ruleset = createRuleset ( {
39- "code-reviewer" : "allow" ,
40- general : "deny" ,
41- } )
42- const result = filterSubagents ( mockAgents , ruleset )
43- expect ( result ) . toHaveLength ( 3 )
44- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "code-reviewer" , "orchestrator-fast" , "orchestrator-slow" ] )
45- } )
46-
47- test ( "includes agents with ask permission (user approval is runtime behavior)" , ( ) => {
48- const ruleset = createRuleset ( {
49- "code-reviewer" : "ask" ,
50- general : "deny" ,
51- } )
52- const result = filterSubagents ( mockAgents , ruleset )
53- expect ( result ) . toHaveLength ( 3 )
54- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "code-reviewer" , "orchestrator-fast" , "orchestrator-slow" ] )
55- } )
56-
57- test ( "includes agents with undefined permission (default allow)" , ( ) => {
58- const ruleset = createRuleset ( {
59- general : "deny" ,
60- } )
61- const result = filterSubagents ( mockAgents , ruleset )
62- expect ( result ) . toHaveLength ( 3 )
63- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "code-reviewer" , "orchestrator-fast" , "orchestrator-slow" ] )
64- } )
65-
66- test ( "supports wildcard patterns with deny" , ( ) => {
67- const ruleset = createRuleset ( { "orchestrator-*" : "deny" } )
68- const result = filterSubagents ( mockAgents , ruleset )
69- expect ( result ) . toHaveLength ( 2 )
70- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "code-reviewer" ] )
71- } )
72-
73- test ( "supports wildcard patterns with allow" , ( ) => {
74- const ruleset = createRuleset ( {
75- "*" : "allow" ,
76- "orchestrator-fast" : "deny" ,
77- } )
78- const result = filterSubagents ( mockAgents , ruleset )
79- expect ( result ) . toHaveLength ( 3 )
80- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "code-reviewer" , "orchestrator-slow" ] )
81- } )
82-
83- test ( "supports wildcard patterns with ask" , ( ) => {
84- const ruleset = createRuleset ( {
85- "orchestrator-*" : "ask" ,
86- } )
87- const result = filterSubagents ( mockAgents , ruleset )
88- expect ( result ) . toHaveLength ( 4 )
89- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "code-reviewer" , "orchestrator-fast" , "orchestrator-slow" ] )
90- } )
91-
92- test ( "longer pattern takes precedence over shorter pattern" , ( ) => {
93- const ruleset = createRuleset ( {
94- "orchestrator-*" : "deny" ,
95- "orchestrator-fast" : "allow" ,
96- } )
97- const result = filterSubagents ( mockAgents , ruleset )
98- expect ( result ) . toHaveLength ( 3 )
99- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "code-reviewer" , "orchestrator-fast" ] )
100- } )
101-
102- test ( "edge case: all agents denied" , ( ) => {
103- const ruleset = createRuleset ( { "*" : "deny" } )
104- const result = filterSubagents ( mockAgents , ruleset )
105- expect ( result ) . toHaveLength ( 0 )
106- expect ( result ) . toEqual ( [ ] )
107- } )
108-
109- test ( "edge case: mixed patterns with multiple wildcards" , ( ) => {
110- const ruleset = createRuleset ( {
111- "*" : "ask" ,
112- "orchestrator-*" : "deny" ,
113- "orchestrator-fast" : "allow" ,
114- } )
115- const result = filterSubagents ( mockAgents , ruleset )
116- expect ( result ) . toHaveLength ( 3 )
117- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "code-reviewer" , "orchestrator-fast" ] )
118- } )
119-
120- test ( "hidden: true does not affect filtering (hidden only affects autocomplete)" , ( ) => {
121- const agents = [
122- { name : "general" , mode : "subagent" , hidden : true , permission : [ ] , options : { } } ,
123- { name : "code-reviewer" , mode : "subagent" , hidden : false , permission : [ ] , options : { } } ,
124- { name : "orchestrator" , mode : "subagent" , permission : [ ] , options : { } } ,
125- ] as Agent . Info [ ]
126-
127- const result = filterSubagents ( agents , [ ] )
128- expect ( result ) . toHaveLength ( 3 )
129- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "code-reviewer" , "orchestrator" ] )
130- } )
131-
132- test ( "hidden: true agents can be filtered by permission.task deny" , ( ) => {
133- const agents = [
134- { name : "general" , mode : "subagent" , hidden : true , permission : [ ] , options : { } } ,
135- { name : "orchestrator-coder" , mode : "subagent" , hidden : true , permission : [ ] , options : { } } ,
136- ] as Agent . Info [ ]
137-
138- const ruleset = createRuleset ( { general : "deny" } )
139- const result = filterSubagents ( agents , ruleset )
140- expect ( result ) . toHaveLength ( 1 )
141- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "orchestrator-coder" ] )
142- } )
143- } )
144-
1457describe ( "PermissionNext.evaluate for permission.task" , ( ) => {
1468 const createRuleset = ( rules : Record < string , "allow" | "deny" | "ask" > ) : PermissionNext . Ruleset =>
1479 Object . entries ( rules ) . map ( ( [ pattern , action ] ) => ( {
@@ -277,12 +139,6 @@ describe("PermissionNext.disabled for task tool", () => {
277139
278140// Integration tests that load permissions from real config files
279141describe ( "permission.task with real config files" , ( ) => {
280- const mockAgents = [
281- { name : "general" , mode : "subagent" , permission : [ ] , options : { } } ,
282- { name : "code-reviewer" , mode : "subagent" , permission : [ ] , options : { } } ,
283- { name : "orchestrator-fast" , mode : "subagent" , permission : [ ] , options : { } } ,
284- ] as Agent . Info [ ]
285-
286142 test ( "loads task permissions from opencode.json config" , async ( ) => {
287143 await using tmp = await tmpdir ( {
288144 git : true ,
@@ -300,8 +156,10 @@ describe("permission.task with real config files", () => {
300156 fn : async ( ) => {
301157 const config = await Config . get ( )
302158 const ruleset = PermissionNext . fromConfig ( config . permission ?? { } )
303- const result = filterSubagents ( mockAgents , ruleset )
304- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "orchestrator-fast" ] )
159+ // general and orchestrator-fast should be allowed, code-reviewer denied
160+ expect ( PermissionNext . evaluate ( "task" , "general" , ruleset ) . action ) . toBe ( "allow" )
161+ expect ( PermissionNext . evaluate ( "task" , "orchestrator-fast" , ruleset ) . action ) . toBe ( "allow" )
162+ expect ( PermissionNext . evaluate ( "task" , "code-reviewer" , ruleset ) . action ) . toBe ( "deny" )
305163 } ,
306164 } )
307165 } )
@@ -323,8 +181,10 @@ describe("permission.task with real config files", () => {
323181 fn : async ( ) => {
324182 const config = await Config . get ( )
325183 const ruleset = PermissionNext . fromConfig ( config . permission ?? { } )
326- const result = filterSubagents ( mockAgents , ruleset )
327- expect ( result . map ( ( a ) => a . name ) ) . toEqual ( [ "general" , "code-reviewer" ] )
184+ // general and code-reviewer should be ask, orchestrator-* denied
185+ expect ( PermissionNext . evaluate ( "task" , "general" , ruleset ) . action ) . toBe ( "ask" )
186+ expect ( PermissionNext . evaluate ( "task" , "code-reviewer" , ruleset ) . action ) . toBe ( "ask" )
187+ expect ( PermissionNext . evaluate ( "task" , "orchestrator-fast" , ruleset ) . action ) . toBe ( "deny" )
328188 } ,
329189 } )
330190 } )
0 commit comments