question.test.ts 4.5 KB

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