tool-todowrite.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  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: ReadonlyArray<SessionTodo.Info> = [
  76. { content: "Implement slice", status: "in_progress", priority: "high" },
  77. ]
  78. expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual([TodoWriteTool.name])
  79. expect(yield* settleTool(registry, call(todoList))).toEqual({
  80. result: { type: "text", value: JSON.stringify(todoList, null, 2) },
  81. output: {
  82. structured: { todos: todoList },
  83. content: [{ type: "text", text: JSON.stringify(todoList, null, 2) }],
  84. },
  85. })
  86. expect(assertions).toMatchObject([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }])
  87. expect(yield* service.get(sessionID)).toEqual(todoList)
  88. }),
  89. )
  90. it.effect("does not update persisted todos when permission is denied", () =>
  91. Effect.gen(function* () {
  92. yield* setup
  93. const registry = yield* ToolRegistry.Service
  94. const service = yield* SessionTodo.Service
  95. yield* service.update({ sessionID, todos: [{ content: "keep", status: "pending", priority: "low" }] })
  96. deny = true
  97. expect(
  98. yield* executeTool(registry, call([{ content: "blocked", status: "completed", priority: "high" }])),
  99. ).toEqual({
  100. type: "error",
  101. value: "Unable to update todos",
  102. })
  103. expect(yield* service.get(sessionID)).toEqual([{ content: "keep", status: "pending", priority: "low" }])
  104. expect(assertions).toMatchObject([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }])
  105. }),
  106. )
  107. })