question.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Fiber, Layer, Queue } from "effect"
  3. import { QuestionTool } from "../../src/tool/question"
  4. import { Question } from "../../src/question"
  5. import { SessionID, MessageID } from "../../src/session/schema"
  6. import { Agent } from "../../src/agent/agent"
  7. import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
  8. import { Truncate } from "@/tool/truncate"
  9. import { testEffect } from "../lib/effect"
  10. import { Bus } from "../../src/bus"
  11. const ctx = {
  12. sessionID: SessionID.make("ses_test-session"),
  13. messageID: MessageID.make("msg_test-message"),
  14. callID: "test-call",
  15. agent: "test-agent",
  16. abort: AbortSignal.any([]),
  17. messages: [],
  18. metadata: () => Effect.void,
  19. ask: () => Effect.void,
  20. }
  21. const it = testEffect(
  22. Layer.mergeAll(
  23. Question.layer.pipe(Layer.provideMerge(Bus.layer)),
  24. CrossSpawnSpawner.defaultLayer,
  25. Truncate.defaultLayer,
  26. Agent.defaultLayer,
  27. ),
  28. )
  29. const pending = Effect.fn("QuestionToolTest.pending")(function* (question: Question.Interface) {
  30. const bus = yield* Bus.Service
  31. const asked = yield* Queue.unbounded<void>()
  32. const off = yield* bus.subscribeCallback(Question.Event.Asked, () => Queue.offerUnsafe(asked, undefined))
  33. yield* Effect.addFinalizer(() => Effect.sync(off))
  34. for (;;) {
  35. const items = yield* question.list()
  36. const item = items[0]
  37. if (item) return item
  38. yield* Queue.take(asked).pipe(Effect.timeout("2 seconds"))
  39. }
  40. })
  41. describe("tool.question", () => {
  42. it.instance("should successfully execute with valid question parameters", () =>
  43. Effect.gen(function* () {
  44. const question = yield* Question.Service
  45. const toolInfo = yield* QuestionTool
  46. const tool = yield* toolInfo.init()
  47. const questions = [
  48. {
  49. question: "What is your favorite color?",
  50. header: "Color",
  51. options: [
  52. { label: "Red", description: "The color of passion" },
  53. { label: "Blue", description: "The color of sky" },
  54. ],
  55. multiple: false,
  56. },
  57. ]
  58. const fiber = yield* tool.execute({ questions }, ctx).pipe(Effect.forkScoped)
  59. const item = yield* pending(question)
  60. yield* question.reply({ requestID: item.id, answers: [["Red"]] })
  61. const result = yield* Fiber.join(fiber)
  62. expect(result.title).toBe("Asked 1 question")
  63. }),
  64. )
  65. it.instance("should now pass with a header longer than 12 but less than 30 chars", () =>
  66. Effect.gen(function* () {
  67. const question = yield* Question.Service
  68. const toolInfo = yield* QuestionTool
  69. const tool = yield* toolInfo.init()
  70. const questions = [
  71. {
  72. question: "What is your favorite animal?",
  73. header: "This Header is Over 12",
  74. options: [{ label: "Dog", description: "Man's best friend" }],
  75. },
  76. ]
  77. const fiber = yield* tool.execute({ questions }, ctx).pipe(Effect.forkScoped)
  78. const item = yield* pending(question)
  79. yield* question.reply({ requestID: item.id, answers: [["Dog"]] })
  80. const result = yield* Fiber.join(fiber)
  81. expect(result.output).toContain(`"What is your favorite animal?"="Dog"`)
  82. }),
  83. )
  84. // intentionally removed the zod validation due to tool call errors, hoping prompting is gonna be good enough
  85. // test("should throw an Error for header exceeding 30 characters", async () => {
  86. // const tool = await QuestionTool.init()
  87. // const questions = [
  88. // {
  89. // question: "What is your favorite animal?",
  90. // header: "This Header is Definitely More Than Thirty Characters Long",
  91. // options: [{ label: "Dog", description: "Man's best friend" }],
  92. // },
  93. // ]
  94. // try {
  95. // await tool.execute({ questions }, ctx)
  96. // // If it reaches here, the test should fail
  97. // expect(true).toBe(false)
  98. // } catch (e: any) {
  99. // expect(e).toBeInstanceOf(Error)
  100. // expect(e.cause).toBeInstanceOf(z.ZodError)
  101. // }
  102. // })
  103. // test("should throw an Error for label exceeding 30 characters", async () => {
  104. // const tool = await QuestionTool.init()
  105. // const questions = [
  106. // {
  107. // question: "A question with a very long label",
  108. // header: "Long Label",
  109. // options: [
  110. // { label: "This is a very, very, very long label that will exceed the limit", description: "A description" },
  111. // ],
  112. // },
  113. // ]
  114. // try {
  115. // await tool.execute({ questions }, ctx)
  116. // // If it reaches here, the test should fail
  117. // expect(true).toBe(false)
  118. // } catch (e: any) {
  119. // expect(e).toBeInstanceOf(Error)
  120. // expect(e.cause).toBeInstanceOf(z.ZodError)
  121. // }
  122. // })
  123. })