agent.test.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Exit, Scope } from "effect"
  3. import { AgentV2 } from "@opencode-ai/core/agent"
  4. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  5. import { Location } from "@opencode-ai/core/location"
  6. import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
  7. import { AbsolutePath } from "@opencode-ai/core/schema"
  8. import { location } from "./fixture/location"
  9. import { testEffect } from "./lib/effect"
  10. import { agentHost, host } from "./plugin/host"
  11. const it = testEffect(AppNodeBuilder.build(AgentV2.node))
  12. describe("AgentV2", () => {
  13. it.effect("starts without agents", () =>
  14. Effect.gen(function* () {
  15. const agent = yield* AgentV2.Service
  16. expect(yield* agent.all()).toEqual([])
  17. expect(yield* agent.get(AgentV2.ID.make("build"))).toBeUndefined()
  18. }),
  19. )
  20. it.effect("materializes replayable agent transforms", () =>
  21. Effect.gen(function* () {
  22. const agent = yield* AgentV2.Service
  23. const id = AgentV2.ID.make("reviewer")
  24. yield* agent.transform((editor) =>
  25. editor.update(id, (info) => {
  26. info.description = "Reviews code"
  27. info.mode = "subagent"
  28. }),
  29. )
  30. expect(yield* agent.get(id)).toMatchObject({ id, description: "Reviews code", mode: "subagent" })
  31. expect((yield* agent.all()).map((info) => info.id)).toEqual([id])
  32. }),
  33. )
  34. it.effect("rebuilds state when a transform is replaced", () =>
  35. Effect.gen(function* () {
  36. const agent = yield* AgentV2.Service
  37. const id = AgentV2.ID.make("reviewer")
  38. let description = "Old description"
  39. let hidden = true
  40. yield* agent.transform((editor) =>
  41. editor.update(id, (info) => {
  42. info.description = description
  43. info.hidden = hidden
  44. }),
  45. )
  46. description = "New description"
  47. hidden = false
  48. yield* agent.reload()
  49. expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
  50. }),
  51. )
  52. it.effect("removes a transform when its scope closes", () =>
  53. Effect.gen(function* () {
  54. const agent = yield* AgentV2.Service
  55. const id = AgentV2.ID.make("scoped")
  56. const scope = yield* Scope.make()
  57. yield* agent.transform((editor) => editor.update(id, () => {})).pipe(Scope.provide(scope))
  58. expect(yield* agent.get(id)).toBeDefined()
  59. yield* Scope.close(scope, Exit.void)
  60. expect(yield* agent.get(id)).toBeUndefined()
  61. }),
  62. )
  63. it.effect("applies direct agent updates", () =>
  64. Effect.gen(function* () {
  65. const agent = yield* AgentV2.Service
  66. const id = AgentV2.ID.make("build")
  67. yield* agent.transform((editor) =>
  68. editor.update(id, (info) => {
  69. info.mode = "primary"
  70. info.hidden = true
  71. }),
  72. )
  73. expect(yield* agent.get(id)).toMatchObject({ id, mode: "primary", hidden: true })
  74. }),
  75. )
  76. it.effect("creates agents with runtime defaults and supports direct removal", () =>
  77. Effect.gen(function* () {
  78. const agent = yield* AgentV2.Service
  79. const id = AgentV2.ID.make("custom")
  80. yield* agent.transform((editor) => editor.update(id, () => {}))
  81. expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id))
  82. yield* agent.transform((editor) => editor.remove(id))
  83. expect(yield* agent.get(id)).toBeUndefined()
  84. }),
  85. )
  86. it.effect("does not ambiently opt built-in agents into bash", () =>
  87. Effect.gen(function* () {
  88. const agent = yield* AgentV2.Service
  89. yield* AgentPlugin.Plugin.effect(
  90. host({
  91. agent: agentHost(agent),
  92. }),
  93. ).pipe(
  94. Effect.provideService(
  95. Location.Service,
  96. Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
  97. ),
  98. )
  99. const agents = yield* agent.all()
  100. expect(agents.map((item) => String(item.id)).sort()).toEqual([
  101. "build",
  102. "compaction",
  103. "explore",
  104. "general",
  105. "plan",
  106. "summary",
  107. "title",
  108. ])
  109. for (const item of agents) {
  110. expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false)
  111. }
  112. }),
  113. )
  114. })