| 1 | import * as NodeAssert from "node:assert/strict"; |
| 2 | |
| 3 | import * as RegExpUtils from "effect/RegExp"; |
| 4 | import { describe, it } from "vite-plus/test"; |
| 5 | |
| 6 | import * as OpenCodeRuntime from "./OpenCodeRuntime.ts"; |
| 7 | |
| 8 | function 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 | |
| 21 | describe("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 | |
| 79 | describe("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 | }); |