tool-todowrite.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  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 registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
  34. const tool = TodoWriteTool.layer.pipe(
  35. Layer.provide(registry),
  36. Layer.provide(permission),
  37. Layer.provide(SessionTodo.defaultLayer),
  38. )
  39. const it = testEffect(
  40. Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, SessionTodo.defaultLayer, permission, registry, tool),
  41. )
  42. const setup = Effect.gen(function* () {
  43. assertions.length = 0
  44. deny = false
  45. const { db } = yield* Database.Service
  46. yield* db
  47. .insert(ProjectTable)
  48. .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
  49. .run()
  50. .pipe(Effect.orDie)
  51. yield* db
  52. .insert(SessionTable)
  53. .values({
  54. id: sessionID,
  55. project_id: Project.ID.global,
  56. slug: "todowrite",
  57. directory: "/project",
  58. title: "todowrite",
  59. version: "test",
  60. })
  61. .run()
  62. .pipe(Effect.orDie)
  63. })
  64. const call = (todos: ReadonlyArray<SessionTodo.Info>, id = "call-todowrite") => ({
  65. sessionID,
  66. ...toolIdentity,
  67. call: { type: "tool-call" as const, id, name: TodoWriteTool.name, input: { todos } },
  68. })
  69. describe("TodoWriteTool", () => {
  70. it.effect("registers, approves the wildcard resource, persists todos, and returns typed output", () =>
  71. Effect.gen(function* () {
  72. yield* setup
  73. const registry = yield* ToolRegistry.Service
  74. const service = yield* SessionTodo.Service
  75. const todoList = [{ content: "Implement slice", status: "in_progress", priority: "high" }]
  76. expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual([TodoWriteTool.name])
  77. expect(yield* settleTool(registry, call(todoList))).toEqual({
  78. result: { type: "text", value: JSON.stringify(todoList, null, 2) },
  79. output: {
  80. structured: { todos: todoList },
  81. content: [{ type: "text", text: JSON.stringify(todoList, null, 2) }],
  82. },
  83. })
  84. expect(assertions).toMatchObject([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }])
  85. expect(yield* service.get(sessionID)).toEqual(todoList)
  86. }),
  87. )
  88. it.effect("does not update persisted todos when permission is denied", () =>
  89. Effect.gen(function* () {
  90. yield* setup
  91. const registry = yield* ToolRegistry.Service
  92. const service = yield* SessionTodo.Service
  93. yield* service.update({ sessionID, todos: [{ content: "keep", status: "pending", priority: "low" }] })
  94. deny = true
  95. expect(
  96. yield* executeTool(registry, call([{ content: "blocked", status: "completed", priority: "high" }])),
  97. ).toEqual({
  98. type: "error",
  99. value: "Unable to update todos",
  100. })
  101. expect(yield* service.get(sessionID)).toEqual([{ content: "keep", status: "pending", priority: "low" }])
  102. expect(assertions).toMatchObject([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }])
  103. }),
  104. )
  105. })