tool-todowrite.test.ts 4.2 KB

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