agent.test.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Schema } from "effect"
  3. import { AgentV2 } from "@opencode-ai/core/agent"
  4. import { Config } from "@opencode-ai/core/config"
  5. import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
  6. import { PermissionV2 } from "@opencode-ai/core/permission"
  7. import { testEffect } from "../lib/effect"
  8. const it = testEffect(AgentV2.locationLayer)
  9. const decode = Schema.decodeUnknownSync(Config.Info)
  10. describe("ConfigAgentPlugin.Plugin", () => {
  11. it.effect("applies global permissions between built-in and agent-specific permissions", () =>
  12. Effect.gen(function* () {
  13. const agents = yield* AgentV2.Service
  14. const build = AgentV2.ID.make("build")
  15. const defaults = yield* agents.transform()
  16. yield* defaults((editor) =>
  17. editor.update(build, (agent) => {
  18. agent.mode = "primary"
  19. agent.permissions.push({ permission: "bash", pattern: "*", action: "allow" })
  20. }),
  21. )
  22. const config = Config.Service.of({
  23. directories: () => Effect.succeed([]),
  24. get: () =>
  25. Effect.succeed([
  26. new Config.Loaded({
  27. source: { type: "memory" },
  28. info: decode({
  29. permissions: [{ permission: "bash", pattern: "*", action: "ask" }],
  30. agents: {
  31. build: {
  32. permissions: [{ permission: "bash", pattern: "git *", action: "allow" }],
  33. },
  34. reviewer: {
  35. model: "openrouter/openai/gpt-5",
  36. description: "Review changes",
  37. mode: "subagent",
  38. permissions: [{ permission: "edit", pattern: "*", action: "deny" }],
  39. },
  40. removed: { description: "Removed later" },
  41. },
  42. }),
  43. }),
  44. new Config.Loaded({
  45. source: { type: "memory" },
  46. info: decode({
  47. agents: {
  48. reviewer: { variant: "high", hidden: true },
  49. removed: { disabled: true },
  50. },
  51. }),
  52. }),
  53. ]),
  54. })
  55. yield* ConfigAgentPlugin.Plugin.effect.pipe(
  56. Effect.provideService(Config.Service, config),
  57. Effect.provideService(AgentV2.Service, agents),
  58. )
  59. const buildAgent = yield* agents.get(build)
  60. if (!buildAgent) throw new Error("expected configured build agent")
  61. expect(buildAgent.permissions).toEqual([
  62. { permission: "bash", pattern: "*", action: "allow" },
  63. { permission: "bash", pattern: "*", action: "ask" },
  64. { permission: "bash", pattern: "git *", action: "allow" },
  65. ])
  66. expect(PermissionV2.evaluate("bash", "git status", buildAgent.permissions).action).toBe("allow")
  67. expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).action).toBe("ask")
  68. const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
  69. if (!reviewer) throw new Error("expected configured reviewer agent")
  70. expect(reviewer).toMatchObject({
  71. description: "Review changes",
  72. mode: "subagent",
  73. hidden: true,
  74. model: { providerID: "openrouter", id: "openai/gpt-5", variant: "high" },
  75. })
  76. expect(reviewer.permissions).toEqual([
  77. { permission: "bash", pattern: "*", action: "ask" },
  78. { permission: "edit", pattern: "*", action: "deny" },
  79. ])
  80. expect(yield* agents.get(AgentV2.ID.make("removed"))).toBeUndefined()
  81. }),
  82. )
  83. it.effect("maps configured agent fields and preserves an unspecified model variant", () =>
  84. Effect.gen(function* () {
  85. const agents = yield* AgentV2.Service
  86. const config = Config.Service.of({
  87. directories: () => Effect.succeed([]),
  88. get: () =>
  89. Effect.succeed([
  90. new Config.Loaded({
  91. source: { type: "memory" },
  92. info: decode({
  93. agents: {
  94. reviewer: {
  95. model: "anthropic/claude-sonnet",
  96. system: "Review carefully.",
  97. description: "Reviews changes",
  98. mode: "subagent",
  99. hidden: true,
  100. color: "warning",
  101. steps: 12,
  102. options: {
  103. headers: { first: "one", shared: "first" },
  104. body: { enabled: true },
  105. aisdk: { provider: { profile: "review" }, request: { effort: "medium" } },
  106. },
  107. },
  108. },
  109. }),
  110. }),
  111. new Config.Loaded({
  112. source: { type: "memory" },
  113. info: decode({
  114. agents: {
  115. reviewer: {
  116. options: {
  117. headers: { shared: "last", second: "two" },
  118. body: { retries: 2 },
  119. aisdk: { request: { effort: "high" } },
  120. },
  121. },
  122. },
  123. }),
  124. }),
  125. ]),
  126. })
  127. yield* ConfigAgentPlugin.Plugin.effect.pipe(
  128. Effect.provideService(Config.Service, config),
  129. Effect.provideService(AgentV2.Service, agents),
  130. )
  131. const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
  132. if (!reviewer) throw new Error("expected configured reviewer agent")
  133. expect(reviewer).toMatchObject({
  134. system: "Review carefully.",
  135. description: "Reviews changes",
  136. mode: "subagent",
  137. hidden: true,
  138. color: "warning",
  139. steps: 12,
  140. model: { providerID: "anthropic", id: "claude-sonnet", variant: undefined },
  141. })
  142. expect(reviewer.options).toEqual({
  143. headers: { first: "one", shared: "last", second: "two" },
  144. body: { enabled: true, retries: 2 },
  145. aisdk: { provider: { profile: "review" }, request: { effort: "high" } },
  146. })
  147. }),
  148. )
  149. it.effect("removes a built-in agent disabled by configuration", () =>
  150. Effect.gen(function* () {
  151. const agents = yield* AgentV2.Service
  152. const build = AgentV2.ID.make("build")
  153. const defaults = yield* agents.transform()
  154. yield* defaults((editor) => editor.update(build, () => {}))
  155. const config = Config.Service.of({
  156. directories: () => Effect.succeed([]),
  157. get: () =>
  158. Effect.succeed([
  159. new Config.Loaded({
  160. source: { type: "memory" },
  161. info: decode({ agents: { build: { disabled: true } } }),
  162. }),
  163. ]),
  164. })
  165. yield* ConfigAgentPlugin.Plugin.effect.pipe(
  166. Effect.provideService(Config.Service, config),
  167. Effect.provideService(AgentV2.Service, agents),
  168. )
  169. expect(yield* agents.get(build)).toBeUndefined()
  170. }),
  171. )
  172. })