1import * as NodeAssert from "node:assert/strict";
2
3import * as RegExpUtils from "effect/RegExp";
4import { describe, it } from "vite-plus/test";
5
6import * as OpenCodeRuntime from "./OpenCodeRuntime.ts";
7
8function actionFor(
9 runtimeMode: Parameters<typeof OpenCodeRuntime.buildOpenCodePermissionRules>[0],
10 permission: string,
11 target = "*",
12) {
13 // OpenCode uses the last matching rule. Its wildcards match directory separators.
14 return OpenCodeRuntime.buildOpenCodePermissionRules(runtimeMode).findLast(
15 (rule) =>
16 (rule.permission === "*" || rule.permission === permission) &&
17 new RegExp(`^${RegExpUtils.escape(rule.pattern).replaceAll("\\*", ".*")}$`, "s").test(target),
18 )?.action;
19}
20
21describe("buildOpenCodePermissionRules", () => {
22 it("pre-approves edits once the user has chosen to auto-accept them", () => {
23 NodeAssert.equal(actionFor("auto-accept-edits", "edit"), "allow");
24 });
25
26 it("still asks before editing when approval is required", () => {
27 NodeAssert.equal(actionFor("approval-required", "edit"), "ask");
28 });
29
30 // Documented in docs/user/permission-modes.md: providers without an AI
31 // reviewer, OpenCode among them, fall back to Supervised for "auto".
32 it("leaves auto asking, as the docs say it does without a reviewer", () => {
33 NodeAssert.equal(actionFor("auto", "edit"), "ask");
34 });
35
36 it("allows workspace reads and task updates without asking in supervised modes", () => {
37 for (const runtimeMode of ["approval-required", "auto-accept-edits", "auto"] as const) {
38 for (const permission of ["read", "glob", "grep", "lsp", "skill", "todowrite"]) {
39 NodeAssert.equal(actionFor(runtimeMode, permission, "src/index.ts"), "allow");
40 }
41 }
42 });
43
44 it("preserves OpenCode's environment-file approval rules", () => {
45 for (const runtimeMode of ["approval-required", "auto-accept-edits", "auto"] as const) {
46 for (const target of [
47 ".env",
48 ".env.local",
49 "config/service.env",
50 "config/service.env.local",
51 ]) {
52 NodeAssert.equal(actionFor(runtimeMode, "read", target), "ask");
53 }
54 for (const target of [".env.example", "config/service.env.example"]) {
55 NodeAssert.equal(actionFor(runtimeMode, "read", target), "allow");
56 }
57 }
58 });
59
60 it("still asks before commands, network access, external directories and unknown tools", () => {
61 for (const runtimeMode of ["approval-required", "auto-accept-edits", "auto"] as const) {
62 NodeAssert.equal(actionFor(runtimeMode, "bash"), "ask");
63 NodeAssert.equal(actionFor(runtimeMode, "webfetch"), "ask");
64 NodeAssert.equal(actionFor(runtimeMode, "websearch"), "ask");
65 NodeAssert.equal(actionFor(runtimeMode, "external_directory"), "ask");
66 NodeAssert.equal(actionFor(runtimeMode, "doom_loop"), "ask");
67 NodeAssert.equal(actionFor(runtimeMode, "custom_tool"), "ask");
68 }
69 });
70
71 it("allows everything only under full access", () => {
72 NodeAssert.deepEqual(OpenCodeRuntime.buildOpenCodePermissionRules("full-access"), [
73 { permission: "*", pattern: "*", action: "allow" },
74 { permission: "external_directory", pattern: "*", action: "allow" },
75 ]);
76 });
77});
78
79describe("toOpenCodePermissionReply", () => {
80 it.each([
81 ["accept", "once"],
82 ["acceptForSession", "always"],
83 ["acceptAlways", "always"],
84 ["decline", "reject"],
85 ["cancel", "reject"],
86 ] as const)("maps %s to %s", (decision, reply) => {
87 NodeAssert.equal(OpenCodeRuntime.toOpenCodePermissionReply(decision), reply);
88 });
89});