opencode/packages/opencode/test/cli/run/permission.shared.test.ts
Simon Klee 00f06157ea
cli: add interactive split-footer mode to run
Split interactive run into dedicated runtime, footer, and scrollback components so local and attach mode share one flow, with the related polish and tests folded into one change.
2026-04-20 20:56:08 +02:00

144 lines
3.9 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
import {
createPermissionBodyState,
permissionAlwaysLines,
permissionCancel,
permissionEscape,
permissionInfo,
permissionReject,
permissionRun,
} from "../../../src/cli/cmd/run/permission.shared"
function req(input: Partial<PermissionRequest> = {}): PermissionRequest {
return {
id: "perm-1",
sessionID: "session-1",
permission: "read",
patterns: [],
metadata: {},
always: [],
...input,
}
}
describe("run permission shared", () => {
test("replies immediately for allow once", () => {
const out = permissionRun(createPermissionBodyState("perm-1"), "perm-1", "once")
expect(out.reply).toEqual({
requestID: "perm-1",
reply: "once",
})
})
test("requires confirmation for allow always", () => {
const next = permissionRun(createPermissionBodyState("perm-1"), "perm-1", "always")
expect(next.state.stage).toBe("always")
expect(next.state.selected).toBe("confirm")
expect(next.reply).toBeUndefined()
expect(permissionRun(next.state, "perm-1", "confirm").reply).toEqual({
requestID: "perm-1",
reply: "always",
})
expect(permissionRun(next.state, "perm-1", "cancel").state).toMatchObject({
stage: "permission",
selected: "always",
})
})
test("builds trimmed reject replies and stage transitions", () => {
const next = permissionRun(createPermissionBodyState("perm-1"), "perm-1", "reject")
expect(next.state.stage).toBe("reject")
const out = permissionReject({ ...next.state, message: " use rg " }, "perm-1")
expect(out).toEqual({
requestID: "perm-1",
reply: "reject",
message: "use rg",
})
expect(permissionCancel(next.state)).toMatchObject({
stage: "permission",
selected: "reject",
})
expect(permissionEscape(createPermissionBodyState("perm-1"))).toMatchObject({
stage: "reject",
selected: "reject",
})
expect(permissionEscape({ ...next.state, stage: "always", selected: "confirm" })).toMatchObject({
stage: "permission",
selected: "always",
})
})
test("maps supported permission types into display info", () => {
expect(
permissionInfo(
req({
permission: "bash",
metadata: {
input: {
command: "git status --short",
},
},
}),
),
).toMatchObject({
title: "Shell command",
lines: ["$ git status --short"],
})
expect(
permissionInfo(
req({
permission: "task",
metadata: {
description: "investigate stream",
subagent_type: "general",
},
}),
),
).toMatchObject({
title: "General Task",
lines: ["◉ investigate stream"],
})
expect(
permissionInfo(
req({
permission: "external_directory",
patterns: ["/tmp/work/**/*.ts", "/tmp/work/**/*.tsx"],
}),
),
).toMatchObject({
title: "Access external directory /tmp/work",
lines: ["- /tmp/work/**/*.ts", "- /tmp/work/**/*.tsx"],
})
expect(permissionInfo(req({ permission: "doom_loop" }))).toMatchObject({
title: "Continue after repeated failures",
})
expect(permissionInfo(req({ permission: "custom_tool" }))).toMatchObject({
title: "Call tool custom_tool",
lines: ["Tool: custom_tool"],
})
})
test("formats always-allow copy for wildcard and explicit patterns", () => {
expect(permissionAlwaysLines(req({ permission: "bash", always: ["*"] }))).toEqual([
"This will allow bash until OpenCode is restarted.",
])
expect(permissionAlwaysLines(req({ always: ["src/**/*.ts", "src/**/*.tsx"] }))).toEqual([
"This will allow the following patterns until OpenCode is restarted.",
"- src/**/*.ts",
"- src/**/*.tsx",
])
})
})