@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,640 @@
1
+ // Gateway-proxy integration test.
2
+ // Starts a discord-digital-twin (fake Discord), a gateway-proxy Rust binary
3
+ // in front of it, and the kimaki bot connecting through the proxy.
4
+ // Validates that messages create threads, bot replies, and multi-tenant
5
+ // guild filtering routes events to the right clients.
6
+ //
7
+ // Requires the gateway-proxy binary at gateway-proxy/target/release/gateway-proxy.
8
+ // If not found, all tests are skipped.
9
+
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import url from 'node:url'
13
+ import net from 'node:net'
14
+ import { spawn, type ChildProcess } from 'node:child_process'
15
+ import { describe, test, expect, beforeAll, afterAll } from 'vitest'
16
+ import {
17
+ ChannelType,
18
+ Client,
19
+ GatewayIntentBits,
20
+ Partials,
21
+ Routes,
22
+ } from 'discord.js'
23
+ import { DigitalDiscord } from 'discord-digital-twin/src'
24
+ import {
25
+ buildDeterministicOpencodeConfig,
26
+ type DeterministicMatcher,
27
+ } from 'opencode-deterministic-provider'
28
+ import { startHranaServer, stopHranaServer } from './hrana-server.js'
29
+ import {
30
+ setBotToken,
31
+ initDatabase,
32
+ closeDatabase,
33
+ setChannelDirectory,
34
+ } from './database.js'
35
+ import { setDataDir } from './config.js'
36
+ import type { VerbosityLevel } from './database.js'
37
+ import { startDiscordBot } from './discord-bot.js'
38
+ import {
39
+ chooseLockPort,
40
+ cleanupTestSessions,
41
+ initTestGitRepo,
42
+ waitForFooterMessage,
43
+ } from './test-utils.js'
44
+ import { stopOpencodeServer } from './opencode.js'
45
+ import { createDiscordRest } from './discord-urls.js'
46
+ import { store } from './store.js'
47
+
48
+ // --- Constants ---
49
+
50
+ const BINARY_PATH = path.resolve(
51
+ process.cwd(),
52
+ '..',
53
+ 'gateway-proxy',
54
+ 'target',
55
+ 'release',
56
+ 'gateway-proxy',
57
+ )
58
+
59
+ const TEST_USER_ID = '900000000000000001'
60
+ const CHANNEL_1_ID = '900000000000000010'
61
+ const CHANNEL_2_ID = '900000000000000020'
62
+ const GUILD_1_ID = '900000000000000100'
63
+ const GUILD_2_ID = '900000000000000200'
64
+
65
+ const binaryExists = fs.existsSync(BINARY_PATH)
66
+
67
+ // --- Helpers ---
68
+
69
+ function getAvailablePort(): Promise<number> {
70
+ return new Promise((resolve, reject) => {
71
+ const srv = net.createServer()
72
+ srv.listen(0, () => {
73
+ const addr = srv.address()
74
+ if (!addr || typeof addr === 'string') {
75
+ srv.close()
76
+ reject(new Error('Failed to get port'))
77
+ return
78
+ }
79
+ const port = addr.port
80
+ srv.close(() => {
81
+ resolve(port)
82
+ })
83
+ })
84
+ })
85
+ }
86
+
87
+ function createRunDirectories() {
88
+ const root = path.resolve(process.cwd(), 'tmp', 'gateway-proxy-e2e')
89
+ fs.mkdirSync(root, { recursive: true })
90
+ const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
91
+ const projectDirectory = path.join(root, 'project')
92
+ fs.mkdirSync(projectDirectory, { recursive: true })
93
+ initTestGitRepo(projectDirectory)
94
+ return { root, dataDir, projectDirectory }
95
+ }
96
+
97
+ function createDiscordJsClient({ restUrl }: { restUrl: string }) {
98
+ return new Client({
99
+ intents: [
100
+ GatewayIntentBits.Guilds,
101
+ GatewayIntentBits.GuildMessages,
102
+ GatewayIntentBits.MessageContent,
103
+ ],
104
+ partials: [Partials.Channel, Partials.Message, Partials.User, Partials.ThreadMember],
105
+ rest: { api: restUrl, version: '10' },
106
+ })
107
+ }
108
+
109
+ function hasStringId(value: unknown): value is { id: string } {
110
+ if (typeof value !== 'object' || value === null) {
111
+ return false
112
+ }
113
+ if (!('id' in value)) {
114
+ return false
115
+ }
116
+ return typeof value.id === 'string'
117
+ }
118
+
119
+ function createMatchers(): DeterministicMatcher[] {
120
+ const defaultReply: DeterministicMatcher = {
121
+ id: 'default-reply',
122
+ priority: 10,
123
+ when: { lastMessageRole: 'user' },
124
+ then: {
125
+ parts: [
126
+ { type: 'stream-start', warnings: [] },
127
+ { type: 'text-start', id: 'reply' },
128
+ { type: 'text-delta', id: 'reply', delta: 'gateway-proxy-reply' },
129
+ { type: 'text-end', id: 'reply' },
130
+ {
131
+ type: 'finish',
132
+ finishReason: 'stop',
133
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
134
+ },
135
+ ],
136
+ },
137
+ }
138
+ return [defaultReply]
139
+ }
140
+
141
+ async function waitForProxyReady({
142
+ port,
143
+ timeoutMs = 30_000,
144
+ }: {
145
+ port: number
146
+ timeoutMs?: number
147
+ }): Promise<void> {
148
+ const start = Date.now()
149
+ while (Date.now() - start < timeoutMs) {
150
+ try {
151
+ const res = await fetch(`http://127.0.0.1:${port}/shard-count`)
152
+ if (res.ok) {
153
+ return
154
+ }
155
+ } catch {
156
+ // not ready yet
157
+ }
158
+ await new Promise((r) => {
159
+ setTimeout(r, 500)
160
+ })
161
+ }
162
+ throw new Error(`gateway-proxy not ready after ${timeoutMs}ms`)
163
+ }
164
+
165
+ function startGatewayProxy({
166
+ configDir,
167
+ port,
168
+ twinPort,
169
+ botToken,
170
+ gatewayUrl,
171
+ }: {
172
+ configDir: string
173
+ port: number
174
+ twinPort: number
175
+ botToken: string
176
+ gatewayUrl: string
177
+ }): { process: ChildProcess; configPath: string } {
178
+ const config = {
179
+ log_level: 'info',
180
+ token: botToken,
181
+ intents: 32511,
182
+ shards: 1,
183
+ port,
184
+ validate_token: true,
185
+ gateway_url: gatewayUrl,
186
+ twilight_http_proxy: `127.0.0.1:${twinPort}`,
187
+ externally_accessible_url: `ws://127.0.0.1:${port}`,
188
+ cache: {
189
+ channels: true,
190
+ presences: false,
191
+ emojis: false,
192
+ current_member: true,
193
+ members: false,
194
+ roles: true,
195
+ scheduled_events: false,
196
+ stage_instances: false,
197
+ stickers: false,
198
+ users: false,
199
+ voice_states: false,
200
+ },
201
+ clients: {
202
+ 'client-a': {
203
+ secret: 'secret-a',
204
+ guilds: [GUILD_1_ID],
205
+ },
206
+ 'client-b': {
207
+ secret: 'secret-b',
208
+ guilds: [GUILD_2_ID],
209
+ },
210
+ },
211
+ }
212
+
213
+ const configPath = path.join(configDir, 'config.json')
214
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
215
+
216
+ const child = spawn(BINARY_PATH, [], {
217
+ cwd: configDir,
218
+ stdio: ['ignore', 'pipe', 'pipe'],
219
+ env: { ...process.env, RUST_LOG: 'debug' },
220
+ })
221
+
222
+ const showLogs = !!process.env['KIMAKI_TEST_LOGS']
223
+ child.stdout?.on('data', (data: Buffer) => {
224
+ const line = data.toString().trim()
225
+ if (line && showLogs) {
226
+ console.log(`[gateway-proxy] ${line}`)
227
+ }
228
+ })
229
+ child.stderr?.on('data', (data: Buffer) => {
230
+ const line = data.toString().trim()
231
+ if (line && showLogs) {
232
+ console.log(`[gateway-proxy] ${line}`)
233
+ }
234
+ })
235
+
236
+ return { process: child, configPath }
237
+ }
238
+
239
+ // --- Test suite ---
240
+
241
+ const describeIf = binaryExists ? describe : describe.skip
242
+
243
+ describeIf('gateway-proxy e2e', () => {
244
+ let discord: DigitalDiscord
245
+ let proxyProcess: ChildProcess
246
+ let botClient: Client
247
+ let directories: ReturnType<typeof createRunDirectories>
248
+ let proxyPort: number
249
+ let previousDefaultVerbosity: VerbosityLevel | undefined
250
+ let firstThreadId: string
251
+ let testStartTime = Date.now()
252
+
253
+ beforeAll(async () => {
254
+ testStartTime = Date.now()
255
+ const lockPort = chooseLockPort({ key: CHANNEL_1_ID })
256
+ directories = createRunDirectories()
257
+ process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
258
+ process.env['KIMAKI_VITEST'] = '1'
259
+ setDataDir(directories.dataDir)
260
+ previousDefaultVerbosity = store.getState().defaultVerbosity
261
+ store.setState({ defaultVerbosity: 'text_only' })
262
+
263
+ const digitalDiscordDbPath = path.join(
264
+ directories.dataDir,
265
+ 'digital-discord.db',
266
+ )
267
+
268
+ proxyPort = await getAvailablePort()
269
+
270
+ // Start digital-twin with 2 guilds, each with a text channel.
271
+ // gatewayUrlOverride makes GET /gateway/bot return the proxy's URL
272
+ // so discord.js clients connect through the proxy, not directly to twin.
273
+ discord = new DigitalDiscord({
274
+ guilds: [
275
+ {
276
+ id: GUILD_1_ID,
277
+ name: 'Guild One',
278
+ ownerId: TEST_USER_ID,
279
+ channels: [
280
+ { id: CHANNEL_1_ID, name: 'general-1', type: ChannelType.GuildText },
281
+ ],
282
+ },
283
+ {
284
+ id: GUILD_2_ID,
285
+ name: 'Guild Two',
286
+ ownerId: TEST_USER_ID,
287
+ channels: [
288
+ { id: CHANNEL_2_ID, name: 'general-2', type: ChannelType.GuildText },
289
+ ],
290
+ },
291
+ ],
292
+ users: [{ id: TEST_USER_ID, username: 'proxy-tester' }],
293
+ gatewayUrlOverride: `ws://127.0.0.1:${proxyPort}`,
294
+ dbUrl: `file:${digitalDiscordDbPath}`,
295
+ })
296
+ await discord.start()
297
+
298
+ // Write opencode.json with deterministic provider
299
+ const providerNpm = url
300
+ .pathToFileURL(
301
+ path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'),
302
+ )
303
+ .toString()
304
+
305
+ const opencodeConfig = buildDeterministicOpencodeConfig({
306
+ providerName: 'deterministic-provider',
307
+ providerNpm,
308
+ model: 'deterministic-v2',
309
+ smallModel: 'deterministic-v2',
310
+ settings: { strict: false, matchers: createMatchers() },
311
+ })
312
+ fs.writeFileSync(
313
+ path.join(directories.projectDirectory, 'opencode.json'),
314
+ JSON.stringify(opencodeConfig, null, 2),
315
+ )
316
+
317
+ // Start gateway-proxy binary pointing at twin
318
+ const proxyConfigDir = path.join(directories.dataDir, 'proxy')
319
+ fs.mkdirSync(proxyConfigDir, { recursive: true })
320
+
321
+ const proxy = startGatewayProxy({
322
+ configDir: proxyConfigDir,
323
+ port: proxyPort,
324
+ twinPort: discord.port,
325
+ botToken: discord.botToken,
326
+ gatewayUrl: discord.gatewayUrl,
327
+ })
328
+ proxyProcess = proxy.process
329
+
330
+ // Wait for proxy to be ready (HTTP server up)
331
+ await waitForProxyReady({ port: proxyPort, timeoutMs: 30_000 })
332
+
333
+ // Initialize kimaki database
334
+ const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
335
+ const hranaResult = await startHranaServer({ dbPath })
336
+ if (hranaResult instanceof Error) {
337
+ throw hranaResult
338
+ }
339
+ process.env['KIMAKI_DB_URL'] = hranaResult
340
+ await initDatabase()
341
+ await setBotToken(discord.botUserId, discord.botToken)
342
+
343
+ // Register channel 1 with kimaki (bot will create sessions for messages here)
344
+ await setChannelDirectory({
345
+ channelId: CHANNEL_1_ID,
346
+ directory: directories.projectDirectory,
347
+ channelType: 'text',
348
+ })
349
+
350
+ // Start the kimaki bot connected through the proxy
351
+ botClient = createDiscordJsClient({ restUrl: discord.restUrl })
352
+
353
+ await startDiscordBot({
354
+ token: discord.botToken,
355
+ appId: discord.botUserId,
356
+ discordClient: botClient,
357
+ })
358
+ }, 120_000)
359
+
360
+ afterAll(async () => {
361
+ if (directories) {
362
+ await cleanupTestSessions({
363
+ projectDirectory: directories.projectDirectory,
364
+ testStartTime,
365
+ })
366
+ }
367
+
368
+ if (botClient) {
369
+ botClient.destroy()
370
+ }
371
+ if (proxyProcess && !proxyProcess.killed) {
372
+ proxyProcess.kill('SIGTERM')
373
+ }
374
+
375
+ await stopOpencodeServer()
376
+ await Promise.all([
377
+ closeDatabase().catch(() => {}),
378
+ stopHranaServer().catch(() => {}),
379
+ discord?.stop().catch(() => {}),
380
+ ])
381
+
382
+ delete process.env['KIMAKI_LOCK_PORT']
383
+ delete process.env['KIMAKI_DB_URL']
384
+ delete process.env['KIMAKI_VITEST']
385
+ if (previousDefaultVerbosity) {
386
+ store.setState({ defaultVerbosity: previousDefaultVerbosity })
387
+ }
388
+ if (directories) {
389
+ fs.rmSync(directories.dataDir, { recursive: true, force: true })
390
+ }
391
+ }, 30_000)
392
+
393
+ test(
394
+ 'message creates thread and bot replies through proxy',
395
+ async () => {
396
+ await discord.channel(CHANNEL_1_ID).user(TEST_USER_ID).sendMessage({
397
+ content: 'hello from gateway proxy test',
398
+ })
399
+
400
+ const thread = await discord.channel(CHANNEL_1_ID).waitForThread({
401
+ timeout: 15_000,
402
+ predicate: (t) => {
403
+ return t.name?.includes('hello from gateway proxy test') ?? false
404
+ },
405
+ })
406
+ expect(thread).toBeDefined()
407
+ expect(thread.id).toBeTruthy()
408
+ firstThreadId = thread.id
409
+
410
+ const reply = await discord.thread(thread.id).waitForBotReply({ timeout: 15_000 })
411
+
412
+ await waitForFooterMessage({
413
+ discord,
414
+ threadId: thread.id,
415
+ timeout: 15_000,
416
+ })
417
+
418
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
419
+ "--- from: user (proxy-tester)
420
+ hello from gateway proxy test
421
+ --- from: assistant (TestBot)
422
+ ⬥ gateway-proxy-reply
423
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
424
+ `)
425
+ expect(reply).toBeDefined()
426
+ expect(reply.content.trim().length).toBeGreaterThan(0)
427
+ },
428
+ 15_000,
429
+ )
430
+
431
+ test(
432
+ 'follow-up message in thread gets bot reply',
433
+ async () => {
434
+ const existingMessages = await discord.thread(firstThreadId).getMessages()
435
+ const existingIds = new Set(existingMessages.map((m) => m.id))
436
+
437
+ await discord.thread(firstThreadId).user(TEST_USER_ID).sendMessage({
438
+ content: 'follow up through proxy',
439
+ })
440
+
441
+ const reply = await discord.thread(firstThreadId).waitForMessage({
442
+ predicate: (m) => !existingIds.has(m.id) && m.author.id === discord.botUserId,
443
+ })
444
+
445
+ await waitForFooterMessage({
446
+ discord,
447
+ threadId: firstThreadId,
448
+ timeout: 4_000,
449
+ afterMessageIncludes: 'follow up through proxy',
450
+ afterAuthorId: TEST_USER_ID,
451
+ })
452
+
453
+ expect(await discord.thread(firstThreadId).text()).toMatchInlineSnapshot(`
454
+ "--- from: user (proxy-tester)
455
+ hello from gateway proxy test
456
+ --- from: assistant (TestBot)
457
+ ⬥ gateway-proxy-reply
458
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
459
+ --- from: user (proxy-tester)
460
+ follow up through proxy
461
+ --- from: assistant (TestBot)
462
+ ⬥ gateway-proxy-reply
463
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
464
+ `)
465
+ expect(reply).toBeDefined()
466
+ expect(reply.content.trim().length).toBeGreaterThan(0)
467
+ },
468
+ 15_000,
469
+ )
470
+
471
+ // Reconnect test lives in gateway-proxy-reconnect.e2e.test.ts.
472
+ // It was here before but kills the proxy mid-suite, breaking shared
473
+ // state (bot/proxy connection) for all subsequent tests.
474
+
475
+ test(
476
+ 'shell command via ! prefix in thread',
477
+ async () => {
478
+ const existingMessages = await discord.thread(firstThreadId).getMessages()
479
+ const existingIds = new Set(existingMessages.map((m) => m.id))
480
+
481
+ await discord.thread(firstThreadId).user(TEST_USER_ID).sendMessage({
482
+ content: '!echo proxy-shell-test',
483
+ })
484
+
485
+ // The bot replies with a loading message then edits it with the result.
486
+ // The predicate waits for the edited version containing "exited with".
487
+ const reply = await discord.thread(firstThreadId).waitForMessage({
488
+ predicate: (m) =>
489
+ !existingIds.has(m.id) &&
490
+ m.author.id === discord.botUserId &&
491
+ m.content.includes('exited with'),
492
+ })
493
+ expect(await discord.thread(firstThreadId).text()).toMatchInlineSnapshot(`
494
+ "--- from: user (proxy-tester)
495
+ hello from gateway proxy test
496
+ --- from: assistant (TestBot)
497
+ ⬥ gateway-proxy-reply
498
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
499
+ --- from: user (proxy-tester)
500
+ follow up through proxy
501
+ --- from: assistant (TestBot)
502
+ ⬥ gateway-proxy-reply
503
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
504
+ --- from: user (proxy-tester)
505
+ !echo proxy-shell-test
506
+ --- from: assistant (TestBot)
507
+ \`echo proxy-shell-test\` exited with 0
508
+ \`\`\`
509
+ proxy-shell-test
510
+ \`\`\`"
511
+ `)
512
+ expect(reply.content).toContain('proxy-shell-test')
513
+ },
514
+ 15_000,
515
+ )
516
+
517
+ test(
518
+ 'second message creates separate thread',
519
+ async () => {
520
+ await discord.channel(CHANNEL_1_ID).user(TEST_USER_ID).sendMessage({
521
+ content: 'second message through proxy',
522
+ })
523
+
524
+ const thread = await discord.channel(CHANNEL_1_ID).waitForThread({
525
+ predicate: (t) =>
526
+ (t.name?.includes('second message through proxy') ?? false) &&
527
+ t.id !== firstThreadId,
528
+ })
529
+ expect(thread).toBeDefined()
530
+ expect(thread.id).not.toBe(firstThreadId)
531
+
532
+ const reply = await discord.thread(thread.id).waitForBotReply()
533
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
534
+ "--- from: user (proxy-tester)
535
+ second message through proxy
536
+ --- from: assistant (TestBot)
537
+ ⬥ gateway-proxy-reply"
538
+ `)
539
+ expect(reply).toBeDefined()
540
+ expect(reply.content.trim().length).toBeGreaterThan(0)
541
+ },
542
+ 15_000,
543
+ )
544
+
545
+ test(
546
+ 'guild-2 message does not create thread (guild isolation)',
547
+ async () => {
548
+ await discord.channel(CHANNEL_2_ID).user(TEST_USER_ID).sendMessage({
549
+ content: 'should not create thread in guild 2',
550
+ })
551
+
552
+ // Brief wait for events to propagate through the local system.
553
+ // The proxy filters guild-2 events away from client-a, so no thread
554
+ // should be created. 100ms is more than enough for local event routing.
555
+ await new Promise((r) => {
556
+ setTimeout(r, 100)
557
+ })
558
+
559
+ const threads = await discord.channel(CHANNEL_2_ID).getThreads()
560
+ expect(threads).toHaveLength(0)
561
+ },
562
+ 5_000,
563
+ )
564
+
565
+ test(
566
+ 'slash command routes INTERACTION_CREATE through proxy',
567
+ async () => {
568
+ const { id: interactionId } = await discord
569
+ .channel(CHANNEL_1_ID)
570
+ .user(TEST_USER_ID)
571
+ .runSlashCommand({
572
+ name: 'run-shell-command',
573
+ options: [{ name: 'command', type: 3, value: 'echo proxy-slash-test' }],
574
+ })
575
+
576
+ const ack = await discord.channel(CHANNEL_1_ID).waitForInteractionAck({
577
+ interactionId,
578
+ })
579
+ expect(ack.acknowledged).toBe(true)
580
+ },
581
+ 15_000,
582
+ )
583
+
584
+ test(
585
+ 'REST client operations work through proxy and enforce guild scope',
586
+ async () => {
587
+ const previousBaseUrl = store.getState().discordBaseUrl
588
+ store.setState({ discordBaseUrl: `http://127.0.0.1:${proxyPort}` })
589
+
590
+ try {
591
+ const botRest = createDiscordRest(discord.botToken)
592
+ const clientRest = createDiscordRest('client-a:secret-a')
593
+
594
+ const posted = await botRest.post(Routes.channelMessages(CHANNEL_1_ID), {
595
+ body: { content: 'rest-proxy-test-message' },
596
+ })
597
+ expect(hasStringId(posted)).toBe(true)
598
+ if (!hasStringId(posted)) {
599
+ throw new Error('Expected REST message create response to include id')
600
+ }
601
+
602
+ const thread = await botRest.post(
603
+ Routes.threads(CHANNEL_1_ID, posted.id),
604
+ {
605
+ body: { name: 'rest-proxy-thread' },
606
+ },
607
+ )
608
+ expect(hasStringId(thread)).toBe(true)
609
+
610
+ const channel = await botRest.get(Routes.channel(CHANNEL_1_ID))
611
+ expect(hasStringId(channel)).toBe(true)
612
+
613
+ const guildChannels = await clientRest.get(
614
+ Routes.guildChannels(GUILD_1_ID),
615
+ )
616
+ expect(Array.isArray(guildChannels)).toBe(true)
617
+
618
+ const forbiddenGuildResponse = await fetch(
619
+ `http://127.0.0.1:${proxyPort}/api/v10${Routes.guildChannels(GUILD_2_ID)}`,
620
+ {
621
+ method: 'GET',
622
+ headers: {
623
+ Authorization: 'Bot client-a:secret-a',
624
+ },
625
+ },
626
+ )
627
+ expect(forbiddenGuildResponse.status).toBe(403)
628
+
629
+ const gatewayInfo = await clientRest.get(Routes.gatewayBot())
630
+ expect(typeof gatewayInfo).toBe('object')
631
+
632
+ const me = await clientRest.get(Routes.user('@me'))
633
+ expect(hasStringId(me)).toBe(true)
634
+ } finally {
635
+ store.setState({ discordBaseUrl: previousBaseUrl })
636
+ }
637
+ },
638
+ 15_000,
639
+ )
640
+ })