permission.shared.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { describe, expect, test } from "bun:test"
  2. import {
  3. createPermissionBodyState,
  4. permissionAlwaysLines,
  5. permissionCancel,
  6. permissionEscape,
  7. permissionInfo,
  8. permissionReject,
  9. permissionRun,
  10. } from "../../src/mini/permission.shared"
  11. import type { MiniPermissionRequest } from "../../src/mini/types"
  12. import { canonicalToolPart } from "./fixture/tool-part"
  13. function req(input: Partial<MiniPermissionRequest> = {}): MiniPermissionRequest {
  14. return {
  15. id: "perm-1",
  16. sessionID: "session-1",
  17. action: "read",
  18. resources: [],
  19. metadata: {},
  20. save: [],
  21. ...input,
  22. }
  23. }
  24. function body() {
  25. return createPermissionBodyState(req())
  26. }
  27. describe("run permission shared", () => {
  28. test("replies immediately for allow once", () => {
  29. const out = permissionRun(body(), "perm-1", "once")
  30. expect(out.reply).toEqual({
  31. sessionID: "session-1",
  32. requestID: "perm-1",
  33. reply: "once",
  34. })
  35. })
  36. test("requires confirmation for allow always", () => {
  37. const next = permissionRun(body(), "perm-1", "always")
  38. expect(next.state.stage).toBe("always")
  39. expect(next.state.selected).toBe("confirm")
  40. expect(next.reply).toBeUndefined()
  41. expect(permissionRun(next.state, "perm-1", "confirm").reply).toEqual({
  42. sessionID: "session-1",
  43. requestID: "perm-1",
  44. reply: "always",
  45. })
  46. expect(permissionRun(next.state, "perm-1", "cancel").state).toMatchObject({
  47. stage: "permission",
  48. selected: "always",
  49. })
  50. })
  51. test("builds trimmed reject replies and stage transitions", () => {
  52. const next = permissionRun(body(), "perm-1", "reject")
  53. expect(next.state.stage).toBe("reject")
  54. const out = permissionReject({ ...next.state, message: " use rg " }, "perm-1")
  55. expect(out).toEqual({
  56. sessionID: "session-1",
  57. requestID: "perm-1",
  58. reply: "reject",
  59. message: "use rg",
  60. })
  61. expect(permissionCancel(next.state)).toMatchObject({
  62. stage: "permission",
  63. selected: "reject",
  64. })
  65. expect(permissionEscape(body())).toMatchObject({
  66. stage: "reject",
  67. selected: "reject",
  68. })
  69. expect(permissionEscape({ ...next.state, stage: "always", selected: "confirm" })).toMatchObject({
  70. stage: "permission",
  71. selected: "always",
  72. })
  73. })
  74. test("maps supported permission types into display info", () => {
  75. expect(
  76. permissionInfo(
  77. req({
  78. action: "shell",
  79. source: { type: "tool", messageID: "msg-shell", callID: "call-shell" },
  80. tool: canonicalToolPart(
  81. "shell",
  82. {
  83. status: "running",
  84. input: { command: "git status --short" },
  85. metadata: {},
  86. },
  87. "call-shell",
  88. ),
  89. }),
  90. ),
  91. ).toMatchObject({
  92. title: "Shell command",
  93. lines: ["$ git status --short"],
  94. })
  95. expect(
  96. permissionInfo(
  97. req({
  98. action: "external_directory",
  99. resources: ["/tmp/work/**/*.ts", "/tmp/work/**/*.tsx"],
  100. }),
  101. ),
  102. ).toMatchObject({
  103. title: "Access external directory /tmp/work",
  104. lines: ["- /tmp/work/**/*.ts", "- /tmp/work/**/*.tsx"],
  105. })
  106. expect(permissionInfo(req({ action: "doom_loop" }))).toMatchObject({
  107. title: "Continue after repeated failures",
  108. })
  109. expect(permissionInfo(req({ action: "custom_tool" }))).toMatchObject({
  110. title: "Call tool custom_tool",
  111. lines: ["Tool: custom_tool"],
  112. })
  113. })
  114. test("prefers canonical request metadata over source tool metadata", () => {
  115. expect(
  116. permissionInfo(
  117. req({
  118. action: "websearch",
  119. metadata: { provider: "parallel" },
  120. source: { type: "tool", messageID: "msg-search", callID: "call-search" },
  121. tool: canonicalToolPart(
  122. "websearch",
  123. {
  124. status: "running",
  125. input: { query: "current releases" },
  126. metadata: { provider: "exa", retained: true },
  127. },
  128. "call-search",
  129. ),
  130. }),
  131. ),
  132. ).toMatchObject({
  133. title: 'Parallel Web Search "current releases"',
  134. lines: ["Query: current releases"],
  135. })
  136. })
  137. test("uses source patch text when an edit has no generated diff", () => {
  138. const patch = '*** Begin Patch\n*** Update File: src/index.ts\n@@\n-old\n+const arrow = "→"\n*** End Patch'
  139. const request = req({
  140. action: "edit",
  141. resources: ["src/index.ts"],
  142. source: { type: "tool", messageID: "msg-edit", callID: "call-edit" },
  143. tool: canonicalToolPart(
  144. "edit",
  145. {
  146. status: "running",
  147. input: { patchText: patch },
  148. metadata: {},
  149. },
  150. "call-edit",
  151. ),
  152. })
  153. expect(permissionInfo(request)).toMatchObject({
  154. title: "Edit src/index.ts",
  155. diff: undefined,
  156. patch,
  157. })
  158. expect(permissionInfo(request, undefined, true)).toMatchObject({
  159. title: "Edit src/index.ts",
  160. lines: [patch],
  161. diff: undefined,
  162. patch: undefined,
  163. })
  164. })
  165. test("formats always-allow copy for wildcard and explicit patterns", () => {
  166. expect(permissionAlwaysLines(req({ action: "bash", save: ["*"] }))).toEqual([
  167. "This will allow bash until OpenCode is restarted.",
  168. ])
  169. expect(permissionAlwaysLines(req({ save: ["src/**/*.ts", "src/**/*.tsx"] }))).toEqual([
  170. "This will allow the following patterns until OpenCode is restarted.",
  171. "- src/**/*.ts",
  172. "- src/**/*.tsx",
  173. ])
  174. })
  175. })