question.test.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { test, expect } from "bun:test"
  2. import { Question } from "../../src/question"
  3. import { Instance } from "../../src/project/instance"
  4. import { QuestionID } from "../../src/question/schema"
  5. import { tmpdir } from "../fixture/fixture"
  6. import { SessionID } from "../../src/session/schema"
  7. /** Reject all pending questions so dangling Deferred fibers don't hang the test. */
  8. async function rejectAll() {
  9. const pending = await Question.list()
  10. for (const req of pending) {
  11. await Question.reject(req.id)
  12. }
  13. }
  14. test("ask - returns pending promise", async () => {
  15. await using tmp = await tmpdir({ git: true })
  16. await Instance.provide({
  17. directory: tmp.path,
  18. fn: async () => {
  19. const promise = Question.ask({
  20. sessionID: SessionID.make("ses_test"),
  21. questions: [
  22. {
  23. question: "What would you like to do?",
  24. header: "Action",
  25. options: [
  26. { label: "Option 1", description: "First option" },
  27. { label: "Option 2", description: "Second option" },
  28. ],
  29. },
  30. ],
  31. })
  32. expect(promise).toBeInstanceOf(Promise)
  33. await rejectAll()
  34. await promise.catch(() => {})
  35. },
  36. })
  37. })
  38. test("ask - adds to pending list", async () => {
  39. await using tmp = await tmpdir({ git: true })
  40. await Instance.provide({
  41. directory: tmp.path,
  42. fn: async () => {
  43. const questions = [
  44. {
  45. question: "What would you like to do?",
  46. header: "Action",
  47. options: [
  48. { label: "Option 1", description: "First option" },
  49. { label: "Option 2", description: "Second option" },
  50. ],
  51. },
  52. ]
  53. const askPromise = Question.ask({
  54. sessionID: SessionID.make("ses_test"),
  55. questions,
  56. })
  57. const pending = await Question.list()
  58. expect(pending.length).toBe(1)
  59. expect(pending[0].questions).toEqual(questions)
  60. await rejectAll()
  61. await askPromise.catch(() => {})
  62. },
  63. })
  64. })
  65. // reply tests
  66. test("reply - resolves the pending ask with answers", async () => {
  67. await using tmp = await tmpdir({ git: true })
  68. await Instance.provide({
  69. directory: tmp.path,
  70. fn: async () => {
  71. const questions = [
  72. {
  73. question: "What would you like to do?",
  74. header: "Action",
  75. options: [
  76. { label: "Option 1", description: "First option" },
  77. { label: "Option 2", description: "Second option" },
  78. ],
  79. },
  80. ]
  81. const askPromise = Question.ask({
  82. sessionID: SessionID.make("ses_test"),
  83. questions,
  84. })
  85. const pending = await Question.list()
  86. const requestID = pending[0].id
  87. await Question.reply({
  88. requestID,
  89. answers: [["Option 1"]],
  90. })
  91. const answers = await askPromise
  92. expect(answers).toEqual([["Option 1"]])
  93. },
  94. })
  95. })
  96. test("reply - removes from pending list", async () => {
  97. await using tmp = await tmpdir({ git: true })
  98. await Instance.provide({
  99. directory: tmp.path,
  100. fn: async () => {
  101. const askPromise = Question.ask({
  102. sessionID: SessionID.make("ses_test"),
  103. questions: [
  104. {
  105. question: "What would you like to do?",
  106. header: "Action",
  107. options: [
  108. { label: "Option 1", description: "First option" },
  109. { label: "Option 2", description: "Second option" },
  110. ],
  111. },
  112. ],
  113. })
  114. const pending = await Question.list()
  115. expect(pending.length).toBe(1)
  116. await Question.reply({
  117. requestID: pending[0].id,
  118. answers: [["Option 1"]],
  119. })
  120. await askPromise
  121. const pendingAfter = await Question.list()
  122. expect(pendingAfter.length).toBe(0)
  123. },
  124. })
  125. })
  126. test("reply - does nothing for unknown requestID", async () => {
  127. await using tmp = await tmpdir({ git: true })
  128. await Instance.provide({
  129. directory: tmp.path,
  130. fn: async () => {
  131. await Question.reply({
  132. requestID: QuestionID.make("que_unknown"),
  133. answers: [["Option 1"]],
  134. })
  135. // Should not throw
  136. },
  137. })
  138. })
  139. // reject tests
  140. test("reject - throws RejectedError", async () => {
  141. await using tmp = await tmpdir({ git: true })
  142. await Instance.provide({
  143. directory: tmp.path,
  144. fn: async () => {
  145. const askPromise = Question.ask({
  146. sessionID: SessionID.make("ses_test"),
  147. questions: [
  148. {
  149. question: "What would you like to do?",
  150. header: "Action",
  151. options: [
  152. { label: "Option 1", description: "First option" },
  153. { label: "Option 2", description: "Second option" },
  154. ],
  155. },
  156. ],
  157. })
  158. const pending = await Question.list()
  159. await Question.reject(pending[0].id)
  160. await expect(askPromise).rejects.toBeInstanceOf(Question.RejectedError)
  161. },
  162. })
  163. })
  164. test("reject - removes from pending list", async () => {
  165. await using tmp = await tmpdir({ git: true })
  166. await Instance.provide({
  167. directory: tmp.path,
  168. fn: async () => {
  169. const askPromise = Question.ask({
  170. sessionID: SessionID.make("ses_test"),
  171. questions: [
  172. {
  173. question: "What would you like to do?",
  174. header: "Action",
  175. options: [
  176. { label: "Option 1", description: "First option" },
  177. { label: "Option 2", description: "Second option" },
  178. ],
  179. },
  180. ],
  181. })
  182. const pending = await Question.list()
  183. expect(pending.length).toBe(1)
  184. await Question.reject(pending[0].id)
  185. askPromise.catch(() => {}) // Ignore rejection
  186. const pendingAfter = await Question.list()
  187. expect(pendingAfter.length).toBe(0)
  188. },
  189. })
  190. })
  191. test("reject - does nothing for unknown requestID", async () => {
  192. await using tmp = await tmpdir({ git: true })
  193. await Instance.provide({
  194. directory: tmp.path,
  195. fn: async () => {
  196. await Question.reject(QuestionID.make("que_unknown"))
  197. // Should not throw
  198. },
  199. })
  200. })
  201. // multiple questions tests
  202. test("ask - handles multiple questions", async () => {
  203. await using tmp = await tmpdir({ git: true })
  204. await Instance.provide({
  205. directory: tmp.path,
  206. fn: async () => {
  207. const questions = [
  208. {
  209. question: "What would you like to do?",
  210. header: "Action",
  211. options: [
  212. { label: "Build", description: "Build the project" },
  213. { label: "Test", description: "Run tests" },
  214. ],
  215. },
  216. {
  217. question: "Which environment?",
  218. header: "Env",
  219. options: [
  220. { label: "Dev", description: "Development" },
  221. { label: "Prod", description: "Production" },
  222. ],
  223. },
  224. ]
  225. const askPromise = Question.ask({
  226. sessionID: SessionID.make("ses_test"),
  227. questions,
  228. })
  229. const pending = await Question.list()
  230. await Question.reply({
  231. requestID: pending[0].id,
  232. answers: [["Build"], ["Dev"]],
  233. })
  234. const answers = await askPromise
  235. expect(answers).toEqual([["Build"], ["Dev"]])
  236. },
  237. })
  238. })
  239. // list tests
  240. test("list - returns all pending requests", async () => {
  241. await using tmp = await tmpdir({ git: true })
  242. await Instance.provide({
  243. directory: tmp.path,
  244. fn: async () => {
  245. const p1 = Question.ask({
  246. sessionID: SessionID.make("ses_test1"),
  247. questions: [
  248. {
  249. question: "Question 1?",
  250. header: "Q1",
  251. options: [{ label: "A", description: "A" }],
  252. },
  253. ],
  254. })
  255. const p2 = Question.ask({
  256. sessionID: SessionID.make("ses_test2"),
  257. questions: [
  258. {
  259. question: "Question 2?",
  260. header: "Q2",
  261. options: [{ label: "B", description: "B" }],
  262. },
  263. ],
  264. })
  265. const pending = await Question.list()
  266. expect(pending.length).toBe(2)
  267. await rejectAll()
  268. p1.catch(() => {})
  269. p2.catch(() => {})
  270. },
  271. })
  272. })
  273. test("list - returns empty when no pending", async () => {
  274. await using tmp = await tmpdir({ git: true })
  275. await Instance.provide({
  276. directory: tmp.path,
  277. fn: async () => {
  278. const pending = await Question.list()
  279. expect(pending.length).toBe(0)
  280. },
  281. })
  282. })