kimaki 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/dist/agent-model.e2e.test.js +250 -164
  2. package/dist/anthropic-account-identity.js +9 -1
  3. package/dist/anthropic-auth-plugin.js +10 -13
  4. package/dist/anthropic-auth-plugin.test.js +17 -0
  5. package/dist/anthropic-auth-state.js +1 -0
  6. package/dist/bash-tool-schema-plugin.js +32 -0
  7. package/dist/bash-tool-schema-plugin.test.js +49 -0
  8. package/dist/bin.js +73 -19
  9. package/dist/cli-commands/maintenance.js +46 -7
  10. package/dist/cli-commands/project.js +11 -27
  11. package/dist/cli-commands/send.js +30 -1
  12. package/dist/cli-commands/session.js +322 -90
  13. package/dist/cli-commands/task.js +34 -15
  14. package/dist/cli-parsing.test.js +83 -4
  15. package/dist/cli-runner.js +44 -8
  16. package/dist/cli-runner.test.js +27 -0
  17. package/dist/cli-send-thread.e2e.test.js +9 -9
  18. package/dist/cli.js +32 -0
  19. package/dist/commands/abort.js +5 -1
  20. package/dist/commands/action-buttons.js +2 -1
  21. package/dist/commands/add-project.js +4 -7
  22. package/dist/commands/agent.js +164 -32
  23. package/dist/commands/ask-question.js +23 -33
  24. package/dist/commands/ask-question.test.js +43 -1
  25. package/dist/commands/btw.js +28 -6
  26. package/dist/commands/file-upload.js +2 -1
  27. package/dist/commands/fork-subagent.js +2 -4
  28. package/dist/commands/fork.js +4 -4
  29. package/dist/commands/last-sessions.js +17 -10
  30. package/dist/commands/login.js +6 -0
  31. package/dist/commands/merge-worktree.js +47 -14
  32. package/dist/commands/model-variant.js +8 -1
  33. package/dist/commands/model.js +16 -9
  34. package/dist/commands/multioauth.js +1 -0
  35. package/dist/commands/new-worktree.js +170 -102
  36. package/dist/commands/queue.js +3 -2
  37. package/dist/commands/resume.js +4 -4
  38. package/dist/commands/session.js +78 -63
  39. package/dist/commands/user-command.js +6 -8
  40. package/dist/commands/worktrees.js +5 -13
  41. package/dist/config.js +16 -1
  42. package/dist/cpu-profiler.js +211 -0
  43. package/dist/cpu-profiler.test.js +160 -0
  44. package/dist/database.js +152 -3
  45. package/dist/db.js +33 -0
  46. package/dist/db.test.js +267 -1
  47. package/dist/default-channel-provisioning.test.js +148 -0
  48. package/dist/discord-bot.js +517 -488
  49. package/dist/discord-command-registration.js +25 -10
  50. package/dist/discord-command-registration.test.js +17 -0
  51. package/dist/discord-utils.js +97 -5
  52. package/dist/discord-utils.test.js +151 -1
  53. package/dist/errors.js +10 -0
  54. package/dist/exec-async.js +17 -5
  55. package/dist/external-directory-permissions.e2e.test.js +11 -7
  56. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  57. package/dist/external-opencode-sync.js +94 -17
  58. package/dist/external-opencode-sync.test.js +206 -0
  59. package/dist/extract-sdk-error-message.test.js +13 -0
  60. package/dist/file-edit-log.js +197 -0
  61. package/dist/file-edit-log.test.js +258 -0
  62. package/dist/format-tables.js +52 -0
  63. package/dist/format-tables.test.js +93 -0
  64. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  65. package/dist/gateway-proxy.e2e.test.js +18 -18
  66. package/dist/git-worktree-core.js +182 -17
  67. package/dist/interaction-handler.js +416 -398
  68. package/dist/ipc-tools-plugin.js +68 -3
  69. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  70. package/dist/kimaki-opencode-plugin.js +9 -3
  71. package/dist/kimaki-workspace-adaptor.js +33 -10
  72. package/dist/logger.js +11 -1
  73. package/dist/markdown.js +3 -1
  74. package/dist/markdown.test.js +11 -3
  75. package/dist/message-formatting.js +181 -51
  76. package/dist/message-formatting.test.js +529 -22
  77. package/dist/message-preprocessing.js +105 -1
  78. package/dist/oauth-rotation-shared.js +10 -0
  79. package/dist/openai-auth-plugin.js +1 -0
  80. package/dist/openai-auth-state.js +1 -0
  81. package/dist/opencode-command.js +28 -0
  82. package/dist/opencode-command.test.js +109 -1
  83. package/dist/opencode-interrupt-plugin.js +4 -0
  84. package/dist/opencode-interrupt-plugin.test.js +24 -0
  85. package/dist/opencode.js +82 -15
  86. package/dist/project-add.test.js +62 -0
  87. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  88. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  89. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  90. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  91. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  92. package/dist/queue-advanced-e2e-setup.js +78 -5
  93. package/dist/queue-advanced-footer.e2e.test.js +60 -269
  94. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  95. package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
  96. package/dist/queue-advanced-question.e2e.test.js +32 -27
  97. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  98. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  99. package/dist/queue-delete-message.e2e.test.js +3 -3
  100. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  101. package/dist/queue-interrupt-drain.e2e.test.js +122 -12
  102. package/dist/queue-question-select-drain.e2e.test.js +34 -132
  103. package/dist/runtime-lifecycle.e2e.test.js +21 -51
  104. package/dist/schema.js +35 -1
  105. package/dist/session-handler/event-stream-state.js +391 -50
  106. package/dist/session-handler/event-stream-state.test.js +736 -8
  107. package/dist/session-handler/model-utils.js +172 -5
  108. package/dist/session-handler/model-utils.test.js +445 -0
  109. package/dist/session-handler/opencode-session-event-log.js +30 -1
  110. package/dist/session-handler/thread-ingress.test.js +41 -0
  111. package/dist/session-handler/thread-runtime-state.js +2 -0
  112. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  113. package/dist/session-handler/thread-session-runtime.js +574 -299
  114. package/dist/session-search.js +123 -4
  115. package/dist/session-search.test.js +248 -1
  116. package/dist/session-sleep.test.js +224 -0
  117. package/dist/session-title-rename.test.js +37 -39
  118. package/dist/sleep.e2e.test.js +494 -0
  119. package/dist/startup-time.e2e.test.js +2 -1
  120. package/dist/store.js +3 -0
  121. package/dist/system-message.js +102 -16
  122. package/dist/system-message.test.js +192 -15
  123. package/dist/task-runner.js +96 -5
  124. package/dist/task-schedule.js +87 -1
  125. package/dist/task-schedule.test.js +149 -1
  126. package/dist/test-utils.js +17 -9
  127. package/dist/thinking-utils.js +10 -0
  128. package/dist/thinking-utils.test.js +35 -0
  129. package/dist/thread-message-queue.e2e.test.js +65 -213
  130. package/dist/undo-redo.e2e.test.js +17 -9
  131. package/dist/voice-attachment.js +23 -1
  132. package/dist/voice-handler.js +21 -4
  133. package/dist/voice-message.e2e.test.js +144 -30
  134. package/dist/voice-routing-input.e2e.test.js +195 -0
  135. package/dist/voice-routing-question.e2e.test.js +125 -0
  136. package/dist/voice.js +28 -8
  137. package/dist/voice.test.js +96 -30
  138. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  139. package/dist/worktree-lifecycle.e2e.test.js +55 -36
  140. package/dist/worktrees.js +226 -443
  141. package/dist/worktrees.test.js +435 -58
  142. package/dist/xai-auth-plugin.js +1 -0
  143. package/dist/xai-auth-state.js +1 -0
  144. package/package.json +8 -6
  145. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  146. package/skills/critique/SKILL.md +4 -4
  147. package/skills/errore/SKILL.md +60 -218
  148. package/skills/goke/SKILL.md +150 -10
  149. package/skills/holocron/SKILL.md +154 -17
  150. package/skills/npm-package/SKILL.md +55 -35
  151. package/skills/profano/SKILL.md +17 -2
  152. package/skills/spiceflow/SKILL.md +39 -3
  153. package/skills/strada/SKILL.md +39 -0
  154. package/skills/zele/SKILL.md +19 -0
  155. package/src/agent-model.e2e.test.ts +287 -186
  156. package/src/anthropic-account-identity.ts +20 -1
  157. package/src/anthropic-auth-plugin.test.ts +19 -0
  158. package/src/anthropic-auth-plugin.ts +12 -16
  159. package/src/anthropic-auth-state.ts +1 -0
  160. package/src/bash-tool-schema-plugin.test.ts +55 -0
  161. package/src/bash-tool-schema-plugin.ts +46 -0
  162. package/src/bin.ts +78 -26
  163. package/src/cli-commands/maintenance.ts +59 -13
  164. package/src/cli-commands/project.ts +14 -35
  165. package/src/cli-commands/send.ts +31 -1
  166. package/src/cli-commands/session.ts +374 -114
  167. package/src/cli-commands/task.ts +34 -15
  168. package/src/cli-parsing.test.ts +103 -4
  169. package/src/cli-runner.test.ts +49 -0
  170. package/src/cli-runner.ts +44 -7
  171. package/src/cli-send-thread.e2e.test.ts +9 -11
  172. package/src/cli.ts +53 -0
  173. package/src/commands/abort.ts +6 -1
  174. package/src/commands/action-buttons.ts +2 -1
  175. package/src/commands/add-project.ts +4 -8
  176. package/src/commands/agent.ts +205 -29
  177. package/src/commands/ask-question.test.ts +48 -1
  178. package/src/commands/ask-question.ts +23 -33
  179. package/src/commands/btw.ts +40 -5
  180. package/src/commands/file-upload.ts +2 -1
  181. package/src/commands/fork-subagent.ts +2 -3
  182. package/src/commands/fork.ts +4 -3
  183. package/src/commands/last-sessions.ts +10 -4
  184. package/src/commands/login.ts +6 -0
  185. package/src/commands/merge-worktree.ts +54 -16
  186. package/src/commands/model-variant.ts +9 -1
  187. package/src/commands/model.ts +20 -9
  188. package/src/commands/multioauth.ts +1 -0
  189. package/src/commands/new-worktree.ts +138 -55
  190. package/src/commands/queue.ts +3 -2
  191. package/src/commands/resume.ts +4 -3
  192. package/src/commands/session.ts +94 -70
  193. package/src/commands/user-command.ts +6 -6
  194. package/src/commands/worktrees.ts +5 -12
  195. package/src/config.ts +18 -1
  196. package/src/cpu-profiler.test.ts +183 -0
  197. package/src/cpu-profiler.ts +293 -0
  198. package/src/database.ts +235 -3
  199. package/src/db.test.ts +293 -0
  200. package/src/db.ts +38 -0
  201. package/src/default-channel-provisioning.test.ts +155 -0
  202. package/src/discord-bot.ts +78 -36
  203. package/src/discord-command-registration.test.ts +20 -0
  204. package/src/discord-command-registration.ts +42 -12
  205. package/src/discord-utils.test.ts +176 -0
  206. package/src/discord-utils.ts +152 -7
  207. package/src/errors.ts +12 -0
  208. package/src/exec-async.ts +20 -11
  209. package/src/external-directory-permissions.e2e.test.ts +11 -7
  210. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  211. package/src/external-opencode-sync.test.ts +248 -0
  212. package/src/external-opencode-sync.ts +122 -19
  213. package/src/extract-sdk-error-message.test.ts +18 -0
  214. package/src/file-edit-log.test.ts +287 -0
  215. package/src/file-edit-log.ts +283 -0
  216. package/src/format-tables.test.ts +101 -0
  217. package/src/format-tables.ts +75 -0
  218. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  219. package/src/gateway-proxy.e2e.test.ts +18 -17
  220. package/src/git-worktree-core.ts +247 -20
  221. package/src/interaction-handler.ts +36 -13
  222. package/src/ipc-tools-plugin.ts +82 -5
  223. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  224. package/src/kimaki-opencode-plugin.ts +9 -3
  225. package/src/kimaki-workspace-adaptor.ts +40 -11
  226. package/src/logger.ts +14 -4
  227. package/src/markdown.test.ts +13 -3
  228. package/src/markdown.ts +4 -1
  229. package/src/message-formatting.test.ts +672 -25
  230. package/src/message-formatting.ts +242 -53
  231. package/src/message-preprocessing.ts +116 -1
  232. package/src/oauth-rotation-shared.ts +10 -0
  233. package/src/openai-auth-plugin.ts +1 -0
  234. package/src/openai-auth-state.ts +1 -0
  235. package/src/opencode-command.test.ts +140 -0
  236. package/src/opencode-command.ts +38 -0
  237. package/src/opencode-interrupt-plugin.test.ts +32 -0
  238. package/src/opencode-interrupt-plugin.ts +3 -0
  239. package/src/opencode.ts +115 -15
  240. package/src/project-add.test.ts +80 -0
  241. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  242. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  243. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  244. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  245. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  246. package/src/queue-advanced-e2e-setup.ts +83 -4
  247. package/src/queue-advanced-footer.e2e.test.ts +67 -315
  248. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  249. package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
  250. package/src/queue-advanced-question.e2e.test.ts +36 -30
  251. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  252. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  253. package/src/queue-delete-message.e2e.test.ts +3 -3
  254. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  255. package/src/queue-interrupt-drain.e2e.test.ts +133 -12
  256. package/src/queue-question-select-drain.e2e.test.ts +36 -161
  257. package/src/runtime-lifecycle.e2e.test.ts +21 -54
  258. package/src/schema.sql +15 -2
  259. package/src/schema.ts +37 -1
  260. package/src/session-handler/event-stream-state.test.ts +790 -7
  261. package/src/session-handler/event-stream-state.ts +568 -53
  262. package/src/session-handler/model-utils.test.ts +537 -0
  263. package/src/session-handler/model-utils.ts +281 -5
  264. package/src/session-handler/opencode-session-event-log.ts +31 -1
  265. package/src/session-handler/thread-ingress.test.ts +54 -0
  266. package/src/session-handler/thread-runtime-state.ts +3 -0
  267. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  268. package/src/session-handler/thread-session-runtime.ts +712 -358
  269. package/src/session-search.test.ts +281 -0
  270. package/src/session-search.ts +241 -7
  271. package/src/session-sleep.test.ts +294 -0
  272. package/src/session-title-rename.test.ts +54 -48
  273. package/src/sleep.e2e.test.ts +565 -0
  274. package/src/startup-time.e2e.test.ts +5 -1
  275. package/src/store.ts +22 -0
  276. package/src/system-message.test.ts +226 -15
  277. package/src/system-message.ts +130 -16
  278. package/src/task-runner.ts +138 -3
  279. package/src/task-schedule.test.ts +170 -0
  280. package/src/task-schedule.ts +141 -1
  281. package/src/test-utils.ts +21 -10
  282. package/src/thinking-utils.test.ts +46 -0
  283. package/src/thinking-utils.ts +21 -0
  284. package/src/thread-message-queue.e2e.test.ts +65 -245
  285. package/src/undo-redo.e2e.test.ts +17 -9
  286. package/src/voice-attachment.ts +29 -1
  287. package/src/voice-handler.ts +23 -4
  288. package/src/voice-message.e2e.test.ts +149 -29
  289. package/src/voice-routing-input.e2e.test.ts +203 -0
  290. package/src/voice-routing-question.e2e.test.ts +131 -0
  291. package/src/voice.test.ts +107 -29
  292. package/src/voice.ts +34 -6
  293. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  294. package/src/worktree-lifecycle.e2e.test.ts +55 -35
  295. package/src/worktrees.test.ts +505 -64
  296. package/src/worktrees.ts +257 -603
  297. package/src/xai-auth-plugin.ts +1 -0
  298. package/src/xai-auth-state.ts +1 -0
  299. package/dist/message-finish-field.e2e.test.js +0 -169
  300. package/dist/subagent-rate-limit-plugin.js +0 -184
  301. package/src/message-finish-field.e2e.test.ts +0 -196
  302. package/src/subagent-rate-limit-plugin.ts +0 -228
@@ -2,7 +2,8 @@
2
2
  // Split from thread-queue-advanced.e2e.test.ts for parallelization.
3
3
  import { describe, test, expect } from 'vitest';
4
4
  import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
5
- import { waitForFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
5
+ import { store } from './store.js';
6
+ import { getMessageVisibleText, isFooterMessage, waitForFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
6
7
  const TEXT_CHANNEL_ID = '200000000000001001';
7
8
  const e2eTest = describe;
8
9
  e2eTest('queue advanced: footer emission', () => {
@@ -33,17 +34,48 @@ e2eTest('queue advanced: footer emission', () => {
33
34
  "--- from: user (queue-advanced-tester)
34
35
  Reply with exactly: footer-check
35
36
  --- from: assistant (TestBot)
36
- *using deterministic-provider/deterministic-v2*
37
- ⬥ ok
38
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
37
+ > *using deterministic-provider/deterministic-v2*
38
+ > ok
39
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
39
40
  `);
40
41
  const foundFooter = footerMessages.some((m) => {
41
42
  return m.author.id === ctx.discord.botUserId
42
- && m.content.startsWith('*')
43
- && m.content.includes('⋅');
43
+ && isFooterMessage({ message: m, botUserId: ctx.discord.botUserId });
44
44
  });
45
45
  expect(foundFooter).toBe(true);
46
46
  }, 8_000);
47
+ test('skip-footer-mentions omits the thread creator mention', async () => {
48
+ store.setState({ footerMentionsEnabled: false });
49
+ try {
50
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
51
+ content: 'Reply with exactly: footer-without-mention',
52
+ });
53
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
54
+ timeout: 4_000,
55
+ predicate: (t) => {
56
+ return t.name === 'Reply with exactly: footer-without-mention';
57
+ },
58
+ });
59
+ const th = ctx.discord.thread(thread.id);
60
+ await th.waitForBotReply({ timeout: 4_000 });
61
+ await waitForFooterMessage({
62
+ discord: ctx.discord,
63
+ threadId: thread.id,
64
+ timeout: 4_000,
65
+ });
66
+ expect(await th.text()).toMatchInlineSnapshot(`
67
+ "--- from: user (queue-advanced-tester)
68
+ Reply with exactly: footer-without-mention
69
+ --- from: assistant (TestBot)
70
+ > *using deterministic-provider/deterministic-v2*
71
+ > ok
72
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*"
73
+ `);
74
+ }
75
+ finally {
76
+ store.setState({ footerMentionsEnabled: true });
77
+ }
78
+ }, 8_000);
47
79
  test('footer appears after second message in same session', async () => {
48
80
  await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
49
81
  content: 'Reply with exactly: footer-multi-setup',
@@ -83,21 +115,20 @@ e2eTest('queue advanced: footer emission', () => {
83
115
  const msgs = await th.getMessages();
84
116
  const footerCount = msgs.filter((m) => {
85
117
  return m.author.id === ctx.discord.botUserId
86
- && m.content.startsWith('*')
87
- && m.content.includes('⋅');
118
+ && isFooterMessage({ message: m, botUserId: ctx.discord.botUserId });
88
119
  }).length;
89
120
  expect(await th.text()).toMatchInlineSnapshot(`
90
121
  "--- from: user (queue-advanced-tester)
91
122
  Reply with exactly: footer-multi-setup
92
123
  --- from: assistant (TestBot)
93
- *using deterministic-provider/deterministic-v2*
94
- ⬥ ok
95
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
124
+ > *using deterministic-provider/deterministic-v2*
125
+ > ok
126
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
96
127
  --- from: user (queue-advanced-tester)
97
128
  Reply with exactly: footer-multi-second
98
129
  --- from: assistant (TestBot)
99
- ⬥ ok
100
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
130
+ > ok
131
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
101
132
  `);
102
133
  if (footerCount >= 2) {
103
134
  expect(footerCount).toBeGreaterThanOrEqual(2);
@@ -111,9 +142,7 @@ e2eTest('queue advanced: footer emission', () => {
111
142
  });
112
143
  const latestMsgs = await th.getMessages();
113
144
  const count = latestMsgs.filter((m) => {
114
- return m.author.id === ctx.discord.botUserId
115
- && m.content.startsWith('*')
116
- && m.content.includes('⋅');
145
+ return isFooterMessage({ message: m, botUserId: ctx.discord.botUserId });
117
146
  }).length;
118
147
  if (count >= 2) {
119
148
  found = true;
@@ -174,7 +203,7 @@ e2eTest('queue advanced: footer emission', () => {
174
203
  if (idx <= followupUserIdx) {
175
204
  return false;
176
205
  }
177
- return m.author.id === ctx.discord.botUserId && m.content.includes('ok');
206
+ return m.author.id === ctx.discord.botUserId && getMessageVisibleText(m).includes('ok');
178
207
  });
179
208
  await waitForFooterMessage({
180
209
  discord: ctx.discord,
@@ -187,18 +216,18 @@ e2eTest('queue advanced: footer emission', () => {
187
216
  "--- from: user (queue-advanced-tester)
188
217
  Reply with exactly: interrupt-footer-setup
189
218
  --- from: assistant (TestBot)
190
- *using deterministic-provider/deterministic-v2*
191
- ⬥ ok
192
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
219
+ > *using deterministic-provider/deterministic-v2*
220
+ > ok
221
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
193
222
  --- from: user (queue-advanced-tester)
194
223
  PLUGIN_TIMEOUT_SLEEP_MARKER
195
224
  --- from: assistant (TestBot)
196
- ⬥ starting sleep 100
225
+ > starting sleep 100
197
226
  --- from: user (queue-advanced-tester)
198
227
  Reply with exactly: interrupt-footer-followup
199
228
  --- from: assistant (TestBot)
200
- ⬥ ok
201
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
229
+ > ok
230
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
202
231
  `);
203
232
  expect(followupUserIdx).toBeGreaterThanOrEqual(0);
204
233
  expect(okReplyIdx).toBeGreaterThan(followupUserIdx);
@@ -207,107 +236,11 @@ e2eTest('queue advanced: footer emission', () => {
207
236
  return false;
208
237
  }
209
238
  return m.author.id === ctx.discord.botUserId
210
- && m.content.startsWith('*')
211
- && m.content.includes('⋅');
239
+ && isFooterMessage({ message: m, botUserId: ctx.discord.botUserId });
212
240
  });
213
241
  expect(footerBetween).toBe(false);
214
242
  }, 15_000);
215
- test('plugin timeout interrupt aborts slow sleep and avoids intermediate footer', async () => {
216
- await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
217
- content: 'Reply with exactly: plugin-timeout-setup',
218
- });
219
- const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
220
- timeout: 4_000,
221
- predicate: (t) => {
222
- return t.name === 'Reply with exactly: plugin-timeout-setup';
223
- },
224
- });
225
- const th = ctx.discord.thread(thread.id);
226
- await th.waitForBotReply({ timeout: 4_000 });
227
- await waitForBotMessageContaining({
228
- discord: ctx.discord,
229
- threadId: thread.id,
230
- userId: TEST_USER_ID,
231
- text: '*project',
232
- timeout: 4_000,
233
- });
234
- await th.user(TEST_USER_ID).sendMessage({
235
- content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
236
- });
237
- await waitForBotMessageContaining({
238
- discord: ctx.discord,
239
- threadId: thread.id,
240
- userId: TEST_USER_ID,
241
- text: 'starting sleep 100',
242
- afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
243
- timeout: 4_000,
244
- });
245
- await th.user(TEST_USER_ID).sendMessage({
246
- content: 'Reply with exactly: plugin-timeout-after',
247
- });
248
- const messages = await waitForBotMessageContaining({
249
- discord: ctx.discord,
250
- threadId: thread.id,
251
- userId: TEST_USER_ID,
252
- text: 'ok',
253
- afterUserMessageIncludes: 'plugin-timeout-after',
254
- timeout: 12_000,
255
- });
256
- const messagesWithFooter = await waitForFooterMessage({
257
- discord: ctx.discord,
258
- threadId: thread.id,
259
- timeout: 12_000,
260
- afterMessageIncludes: 'plugin-timeout-after',
261
- afterAuthorId: TEST_USER_ID,
262
- });
263
- const afterIndex = messagesWithFooter.findIndex((message) => {
264
- return (message.author.id === TEST_USER_ID
265
- && message.content.includes('plugin-timeout-after'));
266
- });
267
- expect(await th.text()).toMatchInlineSnapshot(`
268
- "--- from: user (queue-advanced-tester)
269
- Reply with exactly: plugin-timeout-setup
270
- --- from: assistant (TestBot)
271
- *using deterministic-provider/deterministic-v2*
272
- ⬥ ok
273
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
274
- --- from: user (queue-advanced-tester)
275
- PLUGIN_TIMEOUT_SLEEP_MARKER
276
- --- from: assistant (TestBot)
277
- ⬥ starting sleep 100
278
- --- from: user (queue-advanced-tester)
279
- Reply with exactly: plugin-timeout-after
280
- --- from: assistant (TestBot)
281
- ⬥ ok
282
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
283
- `);
284
- expect(afterIndex).toBeGreaterThanOrEqual(0);
285
- const okReplyIndex = messagesWithFooter.findIndex((message, index) => {
286
- if (index <= afterIndex) {
287
- return false;
288
- }
289
- return message.author.id === ctx.discord.botUserId && message.content.includes('ok');
290
- });
291
- expect(okReplyIndex).toBeGreaterThan(afterIndex);
292
- const footerBeforeReply = messagesWithFooter.some((message, index) => {
293
- if (index <= afterIndex || index >= okReplyIndex) {
294
- return false;
295
- }
296
- if (message.author.id !== ctx.discord.botUserId) {
297
- return false;
298
- }
299
- return message.content.startsWith('*') && message.content.includes('⋅');
300
- });
301
- expect(footerBeforeReply).toBe(false);
302
- }, 15_000);
303
- test('tool-call assistant message gets footer when it completes normally', async () => {
304
- // Reproduces the bug: model responds with text + tool call,
305
- // finish="tool-calls", message gets completed timestamp. Then the tool
306
- // result triggers a follow-up text response in a second assistant message.
307
- // The second message gets a footer, but the first (tool-call) message
308
- // should ALSO get a footer since it completed normally.
309
- // This matches the real-world scenario where an agent calls a bash tool
310
- // (e.g. `kimaki send`) and then follows up with a summary text.
243
+ test('tool-call turn emits a footer after the final follow-up', async () => {
311
244
  const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
312
245
  return thread.id;
313
246
  }));
@@ -337,159 +270,17 @@ e2eTest('queue advanced: footer emission', () => {
337
270
  threadId: thread.id,
338
271
  timeout: 4_000,
339
272
  });
340
- // Poll until both footers have arrived — the first footer (after the
341
- // tool-call step) and the second (after the text follow-up) are emitted
342
- // by sequential handleNaturalAssistantCompletion calls but the second
343
- // may not have hit the Discord thread by the time we first check.
344
- const deadline = Date.now() + 4_000;
345
- let footerCount = 0;
346
- while (Date.now() < deadline) {
347
- const msgs = await th.getMessages();
348
- footerCount = msgs.filter((m) => {
349
- return m.author.id === ctx.discord.botUserId
350
- && m.content.startsWith('*')
351
- && m.content.includes('⋅');
352
- }).length;
353
- if (footerCount >= 2) {
354
- break;
355
- }
356
- await new Promise((resolve) => {
357
- setTimeout(resolve, 100);
358
- });
359
- }
360
273
  expect(await th.text()).toMatchInlineSnapshot(`
361
274
  "--- from: user (queue-advanced-tester)
362
275
  TOOL_CALL_FOOTER_MARKER
363
276
  --- from: assistant (TestBot)
364
- *using deterministic-provider/deterministic-v2*
365
- ⬥ running tool
277
+ > *using deterministic-provider/deterministic-v2*
278
+ > running tool
279
+
366
280
  ┣ bash _echo tool-call-footer-test_
367
- ⬥ tool call completed
368
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
369
- `);
370
- // Only ONE footer at the end — the tool-call step's footer is NOT
371
- // emitted mid-turn. The final text follow-up gets the footer.
372
- expect(footerCount).toBe(1);
373
- }, 10_000);
374
- test('multi-step tool chain should only have one footer at the end', async () => {
375
- // Model does 3 sequential tool calls (each a separate assistant message
376
- // with finish="tool-calls") then a final text response. Only the final
377
- // text response should get a footer — intermediate tool-call steps
378
- // should NOT get footers since they're mid-turn work.
379
- const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
380
- return thread.id;
381
- }));
382
- await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
383
- content: 'MULTI_TOOL_FOOTER_MARKER',
384
- });
385
- const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
386
- timeout: 6_000,
387
- predicate: (t) => {
388
- return !existingThreadIds.has(t.id);
389
- },
390
- });
391
- const th = ctx.discord.thread(thread.id);
392
- // Wait for the final text response after all 3 tool steps
393
- await waitForBotMessageContaining({
394
- discord: ctx.discord,
395
- threadId: thread.id,
396
- userId: TEST_USER_ID,
397
- text: 'all done, fixed 3 files',
398
- timeout: 6_000,
399
- });
400
- // Wait for the footer after the final response
401
- await waitForFooterMessage({
402
- discord: ctx.discord,
403
- threadId: thread.id,
404
- timeout: 6_000,
405
- });
406
- // Give any spurious extra footers time to arrive
407
- await new Promise((resolve) => {
408
- setTimeout(resolve, 500);
409
- });
410
- const messages = await th.getMessages();
411
- const footerCount = messages.filter((m) => {
412
- return m.author.id === ctx.discord.botUserId
413
- && m.content.startsWith('*')
414
- && m.content.includes('⋅');
415
- }).length;
416
- expect(await th.text()).toMatchInlineSnapshot(`
417
- "--- from: user (queue-advanced-tester)
418
- MULTI_TOOL_FOOTER_MARKER
419
- --- from: assistant (TestBot)
420
- *using deterministic-provider/deterministic-v2*
421
- ⬥ investigating the issue
422
- ┣ bash _echo search-done_
423
- ┣ bash _echo read-done_
424
- ┣ bash _echo fix-done_
425
- ⬥ all done, fixed 3 files
426
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
281
+
282
+ > tool call completed
283
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
427
284
  `);
428
- // Only ONE footer should appear — after the final text response.
429
- // Intermediate tool-call steps should NOT get footers.
430
- expect(footerCount).toBe(1);
431
285
  }, 10_000);
432
- test('3 sequential tool-call steps produce exactly 1 footer, not 3', async () => {
433
- // This is the most obvious reproduction of the multi-footer bug:
434
- // the model runs 3 sequential tool-call steps (each a SEPARATE
435
- // assistant message with finish="tool-calls"), then a final text.
436
- // With a naive fix that treats tool-calls as natural completions,
437
- // you'd see 4 footers (one per assistant message). Only the final
438
- // text response should produce a footer.
439
- const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
440
- return thread.id;
441
- }));
442
- await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
443
- content: 'MULTI_STEP_CHAIN_MARKER',
444
- });
445
- const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
446
- timeout: 6_000,
447
- predicate: (t) => {
448
- return !existingThreadIds.has(t.id);
449
- },
450
- });
451
- const th = ctx.discord.thread(thread.id);
452
- // Wait for the final text after all 3 sequential tool steps
453
- await waitForBotMessageContaining({
454
- discord: ctx.discord,
455
- threadId: thread.id,
456
- userId: TEST_USER_ID,
457
- text: 'chain complete: all 3 steps done',
458
- timeout: 10_000,
459
- });
460
- // Wait for footer
461
- await waitForFooterMessage({
462
- discord: ctx.discord,
463
- threadId: thread.id,
464
- timeout: 6_000,
465
- });
466
- // Give any spurious extra footers time to arrive
467
- await new Promise((resolve) => {
468
- setTimeout(resolve, 500);
469
- });
470
- const messages = await th.getMessages();
471
- const footerCount = messages.filter((m) => {
472
- return m.author.id === ctx.discord.botUserId
473
- && m.content.startsWith('*')
474
- && m.content.includes('⋅');
475
- }).length;
476
- expect(await th.text()).toMatchInlineSnapshot(`
477
- "--- from: user (queue-advanced-tester)
478
- MULTI_STEP_CHAIN_MARKER
479
- --- from: assistant (TestBot)
480
- *using deterministic-provider/deterministic-v2*
481
- ⬥ chain step 1: reading config
482
- ┣ bash _echo chain-step-1-output_
483
- ⬥ chain step 2: analyzing results
484
- ┣ bash _echo chain-step-2-output_
485
- ⬥ chain step 3: applying fix
486
- ┣ bash _echo chain-step-3-output_
487
- ⬥ chain complete: all 3 steps done
488
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
489
- `);
490
- // The critical assertion: only 1 footer at the very end.
491
- // With the naive "allow tool-calls as natural completion" fix,
492
- // this would be 4 (one per assistant message). We want 1.
493
- expect(footerCount).toBe(1);
494
- }, 15_000);
495
286
  });
@@ -5,7 +5,7 @@ import { describe, test, expect } from 'vitest';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
8
- import { waitForBotMessageContaining, waitForBotReplyAfterUserMessage, waitForFooterMessage, waitForMessageById, } from './test-utils.js';
8
+ import { isFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, waitForFooterMessage, waitForMessageById, } from './test-utils.js';
9
9
  import { getThreadState } from './session-handler/thread-runtime-state.js';
10
10
  import { getSessionModel } from './database.js';
11
11
  import { initializeOpencodeForDirectory } from './opencode.js';
@@ -242,17 +242,15 @@ describe('queue advanced: /model with interrupt recovery', () => {
242
242
  afterAuthorId: TEST_USER_ID,
243
243
  });
244
244
  const footer = [...finalMessages].reverse().find((message) => {
245
- return message.author.id === ctx.discord.botUserId
246
- && message.content.startsWith('*')
247
- && message.content.includes('⋅');
245
+ return isFooterMessage({ message, botUserId: ctx.discord.botUserId });
248
246
  });
249
247
  expect(await th.text()).toMatchInlineSnapshot(`
250
248
  "--- from: user (queue-model-switch-tester)
251
249
  Reply with exactly: model-switcher-setup
252
250
  --- from: assistant (TestBot)
253
- *using deterministic-provider/deterministic-v2*
254
- ⬥ ok
255
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
251
+ > *using deterministic-provider/deterministic-v2*
252
+ > ok
253
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
256
254
  Model set for this session:
257
255
  **Deterministic Provider** / **deterministic-v3**
258
256
  \`deterministic-provider/deterministic-v3\`
@@ -261,13 +259,13 @@ describe('queue advanced: /model with interrupt recovery', () => {
261
259
  --- from: user (queue-model-switch-tester)
262
260
  PLUGIN_TIMEOUT_SLEEP_MARKER
263
261
  --- from: assistant (TestBot)
264
- ⬥ ok
265
- ⬥ starting sleep 100
262
+ > ok
263
+ > starting sleep 100
266
264
  --- from: user (queue-model-switch-tester)
267
265
  Reply with exactly: model-switcher-followup
268
266
  --- from: assistant (TestBot)
269
- ⬥ ok
270
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v3*"
267
+ > ok
268
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v3* <@200000000000000991>"
271
269
  `);
272
270
  expect(footer).toBeDefined();
273
271
  expect(footer?.content).toContain('deterministic-v3');
@@ -32,7 +32,7 @@ describe('queue advanced: typing around permissions', () => {
32
32
  // every directory, so opt into the --restrict-directories behaviour here.
33
33
  restrictExternalDirectories: true,
34
34
  });
35
- test('permission prompt pauses typing until user click, then typing resumes for long follow-up step', async () => {
35
+ test('permission prompt pauses typing until user click, then typing resumes', async () => {
36
36
  await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
37
37
  content: 'PERMISSION_TYPING_MARKER',
38
38
  });
@@ -56,7 +56,7 @@ describe('queue advanced: typing around permissions', () => {
56
56
  timeout: 4_000,
57
57
  });
58
58
  th.clearTypingEvents();
59
- await th.waitForTypingEvent({ timeout: 2_000 }).then(() => {
59
+ await th.waitForTypingEvent({ timeout: 700 }).then(() => {
60
60
  throw new Error('Typing should stay paused while permission UI is pending');
61
61
  }, () => {
62
62
  return undefined;
@@ -69,19 +69,19 @@ describe('queue advanced: typing around permissions', () => {
69
69
  interactionId: interaction.id,
70
70
  timeout: 4_000,
71
71
  });
72
- const resumedTyping = await th.waitForTypingEvent({ timeout: 9_000 });
72
+ const resumedTyping = await th.waitForTypingEvent({ timeout: 2_000 });
73
73
  expect(resumedTyping).toBeDefined();
74
74
  await waitForBotMessageContaining({
75
75
  discord: ctx.discord,
76
76
  threadId: thread.id,
77
77
  userId: TEST_USER_ID,
78
78
  text: 'permission-flow-done',
79
- timeout: 12_000,
79
+ timeout: 4_000,
80
80
  });
81
81
  await waitForFooterMessage({
82
82
  discord: ctx.discord,
83
83
  threadId: thread.id,
84
- timeout: 12_000,
84
+ timeout: 4_000,
85
85
  afterMessageIncludes: 'permission-flow-done',
86
86
  afterAuthorId: ctx.discord.botUserId,
87
87
  });
@@ -89,8 +89,9 @@ describe('queue advanced: typing around permissions', () => {
89
89
  "--- from: user (queue-permission-tester)
90
90
  PERMISSION_TYPING_MARKER
91
91
  --- from: assistant (TestBot)
92
- *using deterministic-provider/deterministic-v2*
93
- ⬥ requesting external read permission
92
+ > *using deterministic-provider/deterministic-v2*
93
+ > requesting external read permission
94
+
94
95
  ┣ read *.zprofile*
95
96
  ⚠️ **Permission Required**
96
97
  **Type:** \`external_directory\`
@@ -98,25 +99,24 @@ describe('queue advanced: typing around permissions', () => {
98
99
  **Pattern:** \`/Users/morse/*\`
99
100
  ✅ Permission **accepted**
100
101
  [user clicks button]
101
- ⬥ permission-flow-done
102
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
102
+
103
+ > permission-flow-done
104
+ > *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
103
105
  `);
104
106
  const timeline = await th.text({
105
107
  showTyping: true,
106
108
  showInteractions: true,
107
109
  });
108
110
  const clickPosition = timeline.indexOf('[user clicks button]');
109
- const donePosition = timeline.indexOf('⬥ permission-flow-done');
111
+ const donePosition = timeline.indexOf('permission-flow-done');
110
112
  const footerPosition = timeline.lastIndexOf('*project ⋅');
111
113
  expect(clickPosition).toBeGreaterThanOrEqual(0);
112
114
  expect(donePosition).toBeGreaterThan(clickPosition);
113
115
  expect(footerPosition).toBeGreaterThan(donePosition);
114
116
  const afterClick = timeline.slice(clickPosition, donePosition);
115
- const afterDone = timeline.slice(donePosition, footerPosition);
116
117
  expect(afterClick).toContain('[bot typing]');
117
- expect(afterDone).toContain('[bot typing]');
118
118
  expect(timeline.slice(footerPosition)).not.toContain('[bot typing]');
119
- }, 20_000);
119
+ }, 12_000);
120
120
  test('manual thread message dismisses pending permission and sends the new prompt', async () => {
121
121
  const initialPrompt = 'PERMISSION_TYPING_MARKER dismiss-flow';
122
122
  const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
@@ -175,12 +175,12 @@ describe('queue advanced: typing around permissions', () => {
175
175
  afterAuthorId: ctx.discord.botUserId,
176
176
  });
177
177
  const timeline = await th.text({ showInteractions: true });
178
- const normalizedTimeline = timeline.replace('⬥ requesting external read permission\n', '');
178
+ const normalizedTimeline = timeline.replace('requesting external read permission\n', '');
179
179
  expect(normalizedTimeline).toContain('PERMISSION_TYPING_MARKER dismiss-flow');
180
180
  expect(normalizedTimeline).toContain('Permission dismissed - user sent a new message.');
181
181
  expect(normalizedTimeline).toContain('Reply with exactly: post-permission-user-message');
182
182
  const followupUserPosition = normalizedTimeline.indexOf('Reply with exactly: post-permission-user-message');
183
- const followupReplyPosition = normalizedTimeline.indexOf('⬥ ok', followupUserPosition);
183
+ const followupReplyPosition = normalizedTimeline.indexOf('ok', followupUserPosition);
184
184
  const followupFooterPosition = normalizedTimeline.indexOf('*project ⋅', followupReplyPosition);
185
185
  expect(followupUserPosition).toBeGreaterThanOrEqual(0);
186
186
  expect(followupReplyPosition).toBeGreaterThan(followupUserPosition);
@@ -4,12 +4,11 @@
4
4
  // get consumed as a tool result answer (which lost voice/image content).
5
5
  import { describe, test, expect, afterEach } from 'vitest';
6
6
  import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js';
7
- import { waitForBotMessageContaining, waitForFooterMessage } from './test-utils.js';
7
+ import { waitForBotMessageContaining, waitForBotReplyAfterUserMessage, waitForFooterMessage, } from './test-utils.js';
8
8
  import { store } from './store.js';
9
9
  import { getOpencodeClient } from './opencode.js';
10
10
  import { getThreadSession } from './database.js';
11
11
  const TEXT_CHANNEL_ID = '200000000000001007';
12
- const VOICE_CHANNEL_ID = '200000000000001017';
13
12
  function setDeterministicTranscription(config) {
14
13
  store.setState({
15
14
  test: { deterministicTranscription: config },
@@ -128,45 +127,55 @@ describe('queue advanced: question tool answer', () => {
128
127
  // 1. Dismiss the pending question (cleanup context)
129
128
  // 2. Abort the blocked session so OpenCode unblocks
130
129
  // 3. Enqueue the message as a normal user prompt (not consumed as answer)
130
+ const answer = 'Reply with exactly: question-text-answer-done';
131
131
  await th.user(TEST_USER_ID).sendMessage({
132
- content: 'my text answer',
132
+ content: answer,
133
133
  });
134
- // Give time for question cleanup to propagate
135
- await new Promise((r) => {
136
- setTimeout(r, 1_000);
134
+ await waitForBotReplyAfterUserMessage({
135
+ discord: ctx.discord,
136
+ threadId: thread.id,
137
+ userId: TEST_USER_ID,
138
+ userMessageIncludes: answer,
139
+ timeout: 8_000,
140
+ });
141
+ await waitForBotMessageContaining({
142
+ discord: ctx.discord,
143
+ threadId: thread.id,
144
+ userId: TEST_USER_ID,
145
+ text: 'ok',
146
+ afterUserMessageIncludes: answer,
147
+ timeout: 8_000,
148
+ });
149
+ await waitForFooterMessage({
150
+ discord: ctx.discord,
151
+ threadId: thread.id,
152
+ timeout: 8_000,
153
+ afterMessageIncludes: 'ok',
154
+ afterAuthorId: ctx.discord.botUserId,
137
155
  });
138
156
  const timeline = await th.text({ showInteractions: true });
139
157
  // The user's text answer must appear in Discord
140
- expect(timeline).toContain('my text answer');
158
+ expect(timeline).toContain(answer);
159
+ expect(timeline).toContain('ok');
141
160
  // The original question must have appeared
142
161
  expect(timeline).toContain('Which option do you prefer?');
143
162
  // The user's marker message triggered the question
144
163
  expect(timeline).toContain('QUESTION_TEXT_ANSWER_MARKER');
145
164
  }, 20_000);
146
- });
147
- describe('queue advanced: voice message during pending question', () => {
148
- const ctx = setupQueueAdvancedSuite({
149
- channelId: VOICE_CHANNEL_ID,
150
- channelName: 'qa-question-voice-e2e',
151
- dirName: 'qa-question-voice-e2e',
152
- username: 'queue-question-tester',
153
- });
154
- afterEach(() => {
155
- setDeterministicTranscription(null);
156
- });
157
165
  test('voice message during pending question dismisses question and transcribes normally', async () => {
158
166
  // This is the exact bug scenario: user sends a voice message while a
159
167
  // question dropdown is pending. Voice messages have empty message.content
160
168
  // (audio is in attachments, transcription happens later). The old code
161
169
  // passed "" as the question answer and consumed the message — the voice
162
170
  // content was completely lost.
163
- await ctx.discord.channel(VOICE_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
164
- content: 'QUESTION_TEXT_ANSWER_MARKER',
171
+ const marker = 'QUESTION_TEXT_ANSWER_MARKER voice';
172
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
173
+ content: marker,
165
174
  });
166
- const thread = await ctx.discord.channel(VOICE_CHANNEL_ID).waitForThread({
175
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
167
176
  timeout: 8_000,
168
177
  predicate: (t) => {
169
- return t.name === 'QUESTION_TEXT_ANSWER_MARKER';
178
+ return t.name === marker;
170
179
  },
171
180
  });
172
181
  const th = ctx.discord.thread(thread.id);
@@ -188,10 +197,6 @@ describe('queue advanced: voice message during pending question', () => {
188
197
  await th.user(TEST_USER_ID).sendVoiceMessage({
189
198
  content: 'VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL',
190
199
  });
191
- // Give time for question cleanup to propagate
192
- await new Promise((r) => {
193
- setTimeout(r, 1_000);
194
- });
195
200
  // Voice content should be transcribed and appear as the next user message,
196
201
  // processed after the model responds to the empty question answer.
197
202
  await waitForBotMessageContaining({
@@ -254,7 +259,7 @@ describe('queue advanced: voice message during pending question', () => {
254
259
  expect(timeline).toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL');
255
260
  expect(timeline).toContain('🎤 Transcribing voice message...');
256
261
  expect(timeline).toContain('📝 **Transcribed message:** I want option Alpha please');
257
- expect(timeline).toContain('⬥ ok');
262
+ expect(timeline).toContain('ok');
258
263
  // Voice content must be present as a real transcribed message, not lost
259
264
  expect(timeline).toContain('I want option Alpha please');
260
265
  }, 20_000);