From b974b05453badd06c8be5eec07794d4400c4a154 Mon Sep 17 00:00:00 2001 From: tiffanychum <71036662+tiffanychum@users.noreply.github.com> Date: Sun, 26 Apr 2026 02:17:12 +0800 Subject: [PATCH] test(config): cover nested permission rule order regression (#24335) PR #24308 already fixes the reported behavior by preserving config key order through Effect Schema decode. The existing "permission config preserves user key order" test only asserts the top-level key order; it does not catch a regression in the nested rule object (e.g. the keys inside `edit: { ... }` or `bash: { ... }`), which is exactly where #24335 reproduces. Add a test that mirrors the reporter's full opencode.json end-to-end: load via Config.Service, then assert nested key order is preserved and that Permission.evaluate returns the expected actions for edit, external_directory, and bash with the user-specific allow rules. --- packages/opencode/test/config/config.test.ts | 65 ++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 8512236a3d6c..5ad14b6e1e2c 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -3,6 +3,8 @@ import { Effect, Layer, Option } from "effect" import { NodeFileSystem, NodePath } from "@effect/platform-node" import { Config, ConfigManaged } from "../../src/config" import { ConfigParse } from "../../src/config/parse" +import { Permission } from "../../src/permission" +import os from "os" import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { Instance } from "../../src/project/instance" @@ -1567,6 +1569,69 @@ test("permission config preserves user key order", async () => { }) }) +// Regression for #24335: nested rule key order (the keys *inside* `edit: { ... }`, +// `bash: { ... }`, etc.) must also be preserved end-to-end. The reporter put a +// `"*": "deny"` first and a specific `"~/Documents/Programming/AI/**": "allow"` +// after it expecting the specific allow to win, but evaluation kept denying +// because nested decode reordered the keys. +test("permission config preserves nested rule key order (#24335)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + permission: { + external_directory: { + "~/Documents/Programming/AI/**": "allow", + }, + edit: { + "*": "deny", + "~/Documents/Programming/AI/**": "allow", + }, + bash: { + "*": "deny", + "cd ~/Documents/Programming/AI/*": "allow", + "ls ~/Documents/Programming/AI/*": "allow", + "touch ~/Documents/Programming/AI/*": "allow", + pwd: "allow", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await load() + + const editRule = config.permission!.edit as Record + expect(Object.keys(editRule)).toEqual(["*", "~/Documents/Programming/AI/**"]) + + const bashRule = config.permission!.bash as Record + expect(Object.keys(bashRule)).toEqual([ + "*", + "cd ~/Documents/Programming/AI/*", + "ls ~/Documents/Programming/AI/*", + "touch ~/Documents/Programming/AI/*", + "pwd", + ]) + + const ruleset = Permission.fromConfig(config.permission!) + // edit/external_directory match against expanded absolute paths + expect(Permission.evaluate("edit", `${os.homedir()}/Documents/Programming/AI/file.md`, ruleset).action).toBe( + "allow", + ) + expect(Permission.evaluate("edit", "/some/other/dir/file.md", ruleset).action).toBe("deny") + // bash matches against the literal command source, where `~` is preserved + expect(Permission.evaluate("bash", "pwd", ruleset).action).toBe("allow") + expect(Permission.evaluate("bash", "cd ~/Documents/Programming/AI/x", ruleset).action).toBe("allow") + expect(Permission.evaluate("bash", "rm -rf /", ruleset).action).toBe("deny") + }, + }) +}) + test("Effect config parser preserves permission order while rejecting unknown top-level keys", () => { const config = ConfigParse.effectSchema( Config.Info,