tool-todowrite.test.ts 4.5 KB

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