tool-todowrite.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Layer } from "effect"
  3. import { Database } from "@opencode-ai/core/database/database"
  4. import { EventV2 } from "@opencode-ai/core/event"
  5. import { PermissionV2 } from "@opencode-ai/core/permission"
  6. import { Project } from "@opencode-ai/core/project"
  7. import { ProjectTable } from "@opencode-ai/core/project/sql"
  8. import { AbsolutePath } from "@opencode-ai/core/schema"
  9. import { SessionV2 } from "@opencode-ai/core/session"
  10. import { SessionTable } from "@opencode-ai/core/session/sql"
  11. import { SessionTodo } from "@opencode-ai/core/session/todo"
  12. import { TodoWriteTool } from "@opencode-ai/core/tool/todowrite"
  13. import { ToolRegistry } from "@opencode-ai/core/tool/registry"
  14. import { testEffect } from "./lib/effect"
  15. import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
  16. const sessionID = SessionV2.ID.make("ses_todowrite_tool_test")
  17. const assertions: PermissionV2.AssertInput[] = []
  18. let deny = false
  19. const permission = Layer.succeed(
  20. PermissionV2.Service,
  21. PermissionV2.Service.of({
  22. assert: (input) =>
  23. Effect.sync(() => assertions.push(input)).pipe(
  24. Effect.andThen(deny ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void),
  25. ),
  26. ask: () => Effect.die("unused"),
  27. reply: () => Effect.die("unused"),
  28. get: () => Effect.die("unused"),
  29. forSession: () => Effect.die("unused"),
  30. list: () => Effect.die("unused"),
  31. }),
  32. )
  33. const database = Database.layerFromPath(":memory:")
  34. const events = EventV2.layer.pipe(Layer.provide(database))
  35. const todos = SessionTodo.layer.pipe(Layer.provide(database), Layer.provide(events))
  36. const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
  37. const tool = TodoWriteTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(todos))
  38. const it = testEffect(Layer.mergeAll(database, events, todos, permission, registry, tool))
  39. const setup = Effect.gen(function* () {
  40. assertions.length = 0
  41. deny = false
  42. const { db } = yield* Database.Service
  43. yield* db
  44. .insert(ProjectTable)
  45. .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
  46. .run()
  47. .pipe(Effect.orDie)
  48. yield* db
  49. .insert(SessionTable)
  50. .values({
  51. id: sessionID,
  52. project_id: Project.ID.global,
  53. slug: "todowrite",
  54. directory: "/project",
  55. title: "todowrite",
  56. version: "test",
  57. })
  58. .run()
  59. .pipe(Effect.orDie)
  60. })
  61. const call = (todos: ReadonlyArray<SessionTodo.Info>, id = "call-todowrite") => ({
  62. sessionID,
  63. ...toolIdentity,
  64. call: { type: "tool-call" as const, id, name: TodoWriteTool.name, input: { todos } },
  65. })
  66. describe("TodoWriteTool", () => {
  67. it.effect("registers, approves the wildcard resource, persists todos, and returns typed output", () =>
  68. Effect.gen(function* () {
  69. yield* setup
  70. const registry = yield* ToolRegistry.Service
  71. const service = yield* SessionTodo.Service
  72. const todoList = [{ content: "Implement slice", status: "in_progress", priority: "high" }]
  73. expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual([TodoWriteTool.name])
  74. expect(yield* settleTool(registry, call(todoList))).toEqual({
  75. result: { type: "text", value: JSON.stringify(todoList, null, 2) },
  76. output: {
  77. structured: { todos: todoList },
  78. content: [{ type: "text", text: JSON.stringify(todoList, null, 2) }],
  79. },
  80. })
  81. expect(assertions).toMatchObject([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }])
  82. expect(yield* service.get(sessionID)).toEqual(todoList)
  83. }),
  84. )
  85. it.effect("does not update persisted todos when permission is denied", () =>
  86. Effect.gen(function* () {
  87. yield* setup
  88. const registry = yield* ToolRegistry.Service
  89. const service = yield* SessionTodo.Service
  90. yield* service.update({ sessionID, todos: [{ content: "keep", status: "pending", priority: "low" }] })
  91. deny = true
  92. expect(
  93. yield* executeTool(registry, call([{ content: "blocked", status: "completed", priority: "high" }])),
  94. ).toEqual({
  95. type: "error",
  96. value: "Unable to update todos",
  97. })
  98. expect(yield* service.get(sessionID)).toEqual([{ content: "keep", status: "pending", priority: "low" }])
  99. expect(assertions).toMatchObject([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }])
  100. }),
  101. )
  102. })