Files
openchamber/packages/ui/src/sync/vscode-permission-auto-accept.test.ts
T
Bohdan Triapitsyn 0202e88eb4 fix(vscode): restore permission auto-accept parity
Add a VS Code-specific foreground permission responder while preserving the server-owned implementation for web, desktop, and mobile runtimes.

Persist the authoritative VS Code policy in extension global state and expose matching GET/PUT bridge routes. Broadcast policy updates to the sidebar, session editor panels, and agent manager so every active webview observes the same explicit per-session policy.

Resolve missing child-session lineage through OpenCode, honor nearest explicit ancestor overrides, deduplicate concurrent requests, retry transient replies, and reconcile pending permissions after enablement, bootstrap, and reconnect. Treat resolved requests as handled and route notification suppression through the same responder outcome.

Keep post-toggle reconciliation failures non-fatal after policy persistence and fail closed when lineage or replies cannot be confirmed. Document that auto-accept intentionally cannot run while every OpenChamber webview is closed or suspended.
2026-07-17 12:59:41 +03:00

128 lines
4.6 KiB
TypeScript

import { describe, expect, mock, test } from "bun:test"
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
import { createVSCodePermissionAutoAcceptRuntime } from "./vscode-permission-auto-accept"
const permission = { id: "perm-1", sessionID: "child" } as PermissionRequest
const session = (id: string, parentID?: string) => ({ id, parentID }) as Session
describe("VS Code permission auto-accept runtime", () => {
test("loads missing child lineage and inherits the nearest enabled policy", async () => {
let replyCalls = 0
let getSessionCalls = 0
const reply = mock(async () => { replyCalls += 1 })
const getSession = mock(async (id: string) => {
getSessionCalls += 1
return session(id, id === "child" ? "root" : undefined)
})
const runtime = createVSCodePermissionAutoAcceptRuntime({
getPolicy: () => ({ root: true }),
getSessions: () => new Map(),
getSession,
listPendingPermissions: async () => [],
getPermissionState: async () => "ok",
reply,
wait: async () => undefined,
})
expect(await runtime.processPermission(permission, "/repo")).toBe(true)
expect(getSessionCalls).toBe(1)
expect(replyCalls).toBe(1)
})
test("honors an explicit child disable over an enabled parent", async () => {
let replyCalls = 0
const reply = mock(async () => { replyCalls += 1 })
const runtime = createVSCodePermissionAutoAcceptRuntime({
getPolicy: () => ({ root: true, child: false }),
getSessions: () => new Map([["child", session("child", "root")]]),
getSession: async () => session("root"),
listPendingPermissions: async () => [],
getPermissionState: async () => "ok",
reply,
wait: async () => undefined,
})
expect(await runtime.processPermission(permission)).toBe(false)
expect(replyCalls).toBe(0)
})
test("fails closed when lineage cannot be loaded", async () => {
let replyCalls = 0
const reply = mock(async () => { replyCalls += 1 })
const runtime = createVSCodePermissionAutoAcceptRuntime({
getPolicy: () => ({ root: true }),
getSessions: () => new Map(),
getSession: async () => { throw new Error("offline") },
listPendingPermissions: async () => [],
getPermissionState: async () => "ok",
reply,
wait: async () => undefined,
})
expect(await runtime.processPermission(permission)).toBe(false)
expect(replyCalls).toBe(0)
})
test("deduplicates concurrent events and retries failed replies", async () => {
let attempts = 0
const reply = mock(async () => {
attempts += 1
if (attempts < 2) throw new Error("transient")
})
const runtime = createVSCodePermissionAutoAcceptRuntime({
getPolicy: () => ({ child: true }),
getSessions: () => new Map(),
getSession: async () => session("child"),
listPendingPermissions: async () => [],
getPermissionState: async () => "ok",
reply,
wait: async () => undefined,
})
const first = runtime.processPermission(permission)
const second = runtime.processPermission(permission)
expect(await first).toBe(true)
expect(await second).toBe(true)
expect(attempts).toBe(2)
})
test("reconciles existing pending permissions immediately after enablement", async () => {
const replied: string[] = []
const runtime = createVSCodePermissionAutoAcceptRuntime({
getPolicy: () => ({ root: true, disabled: false }),
getSessions: () => new Map([
["child", session("child", "root")],
["disabled", session("disabled", "root")],
]),
getSession: async (id) => session(id),
listPendingPermissions: async () => [
permission,
{ ...permission, id: "perm-disabled", sessionID: "disabled" },
],
getPermissionState: async () => "ok",
reply: async (_sessionId, requestId) => { replied.push(requestId) },
wait: async () => undefined,
})
await runtime.reconcilePending("/repo")
expect(replied).toEqual(["perm-1"])
})
test("treats an already resolved permission as handled without replying", async () => {
let replyCalls = 0
const runtime = createVSCodePermissionAutoAcceptRuntime({
getPolicy: () => ({ child: true }),
getSessions: () => new Map(),
getSession: async () => session("child"),
listPendingPermissions: async () => [],
getPermissionState: async () => "resolved",
reply: async () => { replyCalls += 1 },
wait: async () => undefined,
})
expect(await runtime.processPermission({ ...permission, id: "resolved" })).toBe(true)
expect(replyCalls).toBe(0)
})
})