Skip to content

Commit 8b61430

Browse files
committed
test: add comprehensive permission tests for edit and bash
Added comprehensive unit tests covering all three permission modes (allow/deny/ask) for edit and bash permissions: Edit permission tests (edit.test.ts): - permission='allow' - edits succeed without asking - permission='deny' - edits are blocked without asking - permission='ask' with approval - asks and allows edit - permission='ask' with denial - asks and blocks edit Bash permission tests (bash.test.ts): - permission='allow' via wildcard - commands succeed without asking - permission='deny' via wildcard - commands are blocked without asking - permission='ask' via wildcard - asks and allows command - undefined permission (no wildcard match) - defaults to deny Also updated .opencode/opencode.jsonc to set external_directory='ask' for testing. All 204 tests passing.
1 parent 220da28 commit 8b61430

File tree

3 files changed

+438
-0
lines changed

3 files changed

+438
-0
lines changed

.opencode/opencode.jsonc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@
88
},
99
},
1010
},
11+
"permission": {
12+
"external_directory": "ask"
13+
}
1114
}

packages/opencode/test/tool/bash.test.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,186 @@ describe("tool.bash", () => {
5959
},
6060
})
6161
})
62+
63+
test("should allow commands when bash permission='allow' via wildcard", async () => {
64+
const { Agent } = await import("../../src/agent/agent")
65+
const originalGet = Agent.get
66+
67+
// Mock Agent.get to return agent with bash='allow' via wildcard
68+
Agent.get = mock(async () => ({
69+
permission: {
70+
edit: "allow" as const,
71+
bash: { "*": "allow" as const },
72+
webfetch: "allow" as const,
73+
external_directory: "ask" as const,
74+
doom_loop: "ask" as const,
75+
},
76+
})) as any
77+
78+
const originalAsk = Permission.ask
79+
const permissionAskMock = mock(async () => {})
80+
Permission.ask = permissionAskMock
81+
82+
try {
83+
await Instance.provide({
84+
directory: projectRoot,
85+
fn: async () => {
86+
const bash = await BashTool.init()
87+
const result = await bash.execute(
88+
{
89+
command: "echo 'permission test'",
90+
description: "Test bash permission",
91+
},
92+
ctx,
93+
)
94+
95+
expect(result.metadata.exit).toBe(0)
96+
expect(result.metadata.output).toContain("permission test")
97+
98+
// Verify Permission.ask was NOT called
99+
expect(permissionAskMock).not.toHaveBeenCalled()
100+
},
101+
})
102+
} finally {
103+
Agent.get = originalGet
104+
Permission.ask = originalAsk
105+
}
106+
})
107+
108+
test("should deny commands when bash permission='deny' via wildcard", async () => {
109+
const { Agent } = await import("../../src/agent/agent")
110+
const originalGet = Agent.get
111+
112+
// Mock Agent.get to return agent with bash='deny' via wildcard
113+
Agent.get = mock(async () => ({
114+
permission: {
115+
edit: "allow" as const,
116+
bash: { "*": "deny" as const },
117+
webfetch: "allow" as const,
118+
external_directory: "ask" as const,
119+
doom_loop: "ask" as const,
120+
},
121+
})) as any
122+
123+
const originalAsk = Permission.ask
124+
const permissionAskMock = mock(async () => {})
125+
Permission.ask = permissionAskMock
126+
127+
try {
128+
await Instance.provide({
129+
directory: projectRoot,
130+
fn: async () => {
131+
const bash = await BashTool.init()
132+
await expect(
133+
bash.execute(
134+
{
135+
command: "echo 'should be denied'",
136+
description: "Test bash permission deny",
137+
},
138+
ctx,
139+
),
140+
).rejects.toThrow("Permission denied: Command not allowed by bash permissions")
141+
142+
// Verify Permission.ask was NOT called
143+
expect(permissionAskMock).not.toHaveBeenCalled()
144+
},
145+
})
146+
} finally {
147+
Agent.get = originalGet
148+
Permission.ask = originalAsk
149+
}
150+
})
151+
152+
test("should ask for permission when bash permission='ask' via wildcard", async () => {
153+
const { Agent } = await import("../../src/agent/agent")
154+
const originalGet = Agent.get
155+
156+
// Mock Agent.get to return agent with bash='ask' via wildcard
157+
Agent.get = mock(async () => ({
158+
permission: {
159+
edit: "allow" as const,
160+
bash: { "*": "ask" as const },
161+
webfetch: "allow" as const,
162+
external_directory: "ask" as const,
163+
doom_loop: "ask" as const,
164+
},
165+
})) as any
166+
167+
const originalAsk = Permission.ask
168+
const permissionAskMock = mock(async (input: any) => {
169+
expect(input.type).toBe("bash")
170+
// Resolve without throwing to simulate user approval
171+
})
172+
Permission.ask = permissionAskMock
173+
174+
try {
175+
await Instance.provide({
176+
directory: projectRoot,
177+
fn: async () => {
178+
const bash = await BashTool.init()
179+
const result = await bash.execute(
180+
{
181+
command: "echo 'ask permission'",
182+
description: "Test bash permission ask",
183+
},
184+
ctx,
185+
)
186+
187+
expect(result.metadata.exit).toBe(0)
188+
expect(result.metadata.output).toContain("ask permission")
189+
190+
// Verify Permission.ask WAS called
191+
expect(permissionAskMock).toHaveBeenCalled()
192+
},
193+
})
194+
} finally {
195+
Agent.get = originalGet
196+
Permission.ask = originalAsk
197+
}
198+
})
199+
200+
test("should deny when no wildcard match (undefined permission)", async () => {
201+
const { Agent } = await import("../../src/agent/agent")
202+
const originalGet = Agent.get
203+
204+
// Mock Agent.get to return agent with specific command allowed, but not echo
205+
Agent.get = mock(async () => ({
206+
permission: {
207+
edit: "allow" as const,
208+
bash: { "ls *": "allow" as const }, // Only ls is allowed
209+
webfetch: "allow" as const,
210+
external_directory: "ask" as const,
211+
doom_loop: "ask" as const,
212+
},
213+
})) as any
214+
215+
const originalAsk = Permission.ask
216+
const permissionAskMock = mock(async () => {})
217+
Permission.ask = permissionAskMock
218+
219+
try {
220+
await Instance.provide({
221+
directory: projectRoot,
222+
fn: async () => {
223+
const bash = await BashTool.init()
224+
// echo doesn't match any pattern, should default to deny
225+
await expect(
226+
bash.execute(
227+
{
228+
command: "echo 'no match'",
229+
description: "Test undefined permission",
230+
},
231+
ctx,
232+
),
233+
).rejects.toThrow("Permission denied: Command not allowed by bash permissions")
234+
235+
// Verify Permission.ask was NOT called
236+
expect(permissionAskMock).not.toHaveBeenCalled()
237+
},
238+
})
239+
} finally {
240+
Agent.get = originalGet
241+
Permission.ask = originalAsk
242+
}
243+
})
62244
})

0 commit comments

Comments
 (0)