@meetquinn/animactl 0.1.2 → 0.1.3

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 (289) hide show
  1. package/README.md +11 -6
  2. package/dist/server/{runtime/activity-payload.d.ts → activities/format.d.ts} +4 -1
  3. package/dist/server/activities/format.d.ts.map +1 -0
  4. package/dist/server/{runtime/activity-payload.js → activities/format.js} +16 -1
  5. package/dist/server/agents/agent-skills.d.ts +7 -3
  6. package/dist/server/agents/agent-skills.d.ts.map +1 -1
  7. package/dist/server/agents/agent-skills.js +243 -38
  8. package/dist/server/agents/agent-slack.service.d.ts +15 -0
  9. package/dist/server/agents/agent-slack.service.d.ts.map +1 -1
  10. package/dist/server/agents/agent-slack.service.js +43 -6
  11. package/dist/server/agents/default-skills.d.ts +9 -0
  12. package/dist/server/agents/default-skills.d.ts.map +1 -0
  13. package/dist/server/agents/default-skills.js +52 -0
  14. package/dist/server/cli/runtime-cli.d.ts +7 -1
  15. package/dist/server/cli/runtime-cli.d.ts.map +1 -1
  16. package/dist/server/cli/runtime-cli.js +107 -3
  17. package/dist/server/cli/services-cli.d.ts.map +1 -1
  18. package/dist/server/cli/services-cli.js +39 -0
  19. package/dist/server/inbox/slack-subscriber.d.ts +2 -0
  20. package/dist/server/inbox/slack-subscriber.d.ts.map +1 -1
  21. package/dist/server/inbox/slack-subscriber.js +28 -2
  22. package/dist/server/inbox/slack-subscription.service.d.ts.map +1 -1
  23. package/dist/server/inbox/slack-subscription.service.js +13 -1
  24. package/dist/server/inbox/wake-queue.service.d.ts +3 -1
  25. package/dist/server/inbox/wake-queue.service.d.ts.map +1 -1
  26. package/dist/server/inbox/wake-queue.service.js +5 -3
  27. package/dist/server/provider-usage/http.d.ts +1 -0
  28. package/dist/server/provider-usage/http.d.ts.map +1 -1
  29. package/dist/server/provider-usage/http.js +23 -1
  30. package/dist/server/provider-usage/provider-usage.service.d.ts +1 -4
  31. package/dist/server/provider-usage/provider-usage.service.d.ts.map +1 -1
  32. package/dist/server/provider-usage/provider-usage.service.js +1 -18
  33. package/dist/server/provider-usage/providers/claude.d.ts +1 -1
  34. package/dist/server/provider-usage/providers/claude.d.ts.map +1 -1
  35. package/dist/server/provider-usage/providers/claude.js +1 -1
  36. package/dist/server/provider-usage/providers/codex.d.ts +1 -1
  37. package/dist/server/provider-usage/providers/codex.d.ts.map +1 -1
  38. package/dist/server/provider-usage/providers/codex.js +1 -1
  39. package/dist/server/provider-usage/providers/kimi.d.ts +1 -1
  40. package/dist/server/provider-usage/providers/kimi.d.ts.map +1 -1
  41. package/dist/server/provider-usage/providers/kimi.js +4 -4
  42. package/dist/server/provider-usage/result.d.ts +5 -0
  43. package/dist/server/provider-usage/result.d.ts.map +1 -0
  44. package/dist/server/provider-usage/result.js +18 -0
  45. package/dist/server/providers/active-runtime.d.ts +1 -1
  46. package/dist/server/providers/active-runtime.d.ts.map +1 -1
  47. package/dist/server/providers/claude-events.d.ts +1 -1
  48. package/dist/server/providers/claude-events.d.ts.map +1 -1
  49. package/dist/server/providers/claude-events.js +2 -2
  50. package/dist/server/providers/claude.d.ts +1 -1
  51. package/dist/server/providers/claude.d.ts.map +1 -1
  52. package/dist/server/providers/claude.js +2 -2
  53. package/dist/server/providers/codex-app-server.d.ts +1 -1
  54. package/dist/server/providers/codex-app-server.d.ts.map +1 -1
  55. package/dist/server/providers/codex-app-server.js +1 -1
  56. package/dist/server/providers/codex-events.js +1 -1
  57. package/dist/server/providers/codex.d.ts +1 -1
  58. package/dist/server/providers/codex.d.ts.map +1 -1
  59. package/dist/server/providers/codex.js +2 -2
  60. package/dist/server/{runtime/provider-contract.d.ts → providers/contract.d.ts} +1 -1
  61. package/dist/server/providers/contract.d.ts.map +1 -0
  62. package/dist/server/providers/factory.d.ts +1 -1
  63. package/dist/server/providers/factory.d.ts.map +1 -1
  64. package/dist/server/providers/kimi-events.d.ts +1 -1
  65. package/dist/server/providers/kimi-events.d.ts.map +1 -1
  66. package/dist/server/providers/kimi-events.js +1 -1
  67. package/dist/server/providers/kimi.d.ts +1 -1
  68. package/dist/server/providers/kimi.d.ts.map +1 -1
  69. package/dist/server/providers/kimi.js +3 -2
  70. package/dist/server/providers/reasoning-events.js +1 -1
  71. package/dist/server/runtime/active-run-control.d.ts +19 -0
  72. package/dist/server/runtime/active-run-control.d.ts.map +1 -0
  73. package/dist/server/runtime/active-run-control.js +89 -0
  74. package/dist/server/runtime/activity.js +1 -1
  75. package/dist/server/runtime/agent-runner.d.ts +1 -1
  76. package/dist/server/runtime/agent-runner.d.ts.map +1 -1
  77. package/dist/server/runtime/delivery-prompt.d.ts +6 -0
  78. package/dist/server/runtime/delivery-prompt.d.ts.map +1 -1
  79. package/dist/server/runtime/delivery-prompt.js +53 -34
  80. package/dist/server/runtime/followup-appender.d.ts +20 -0
  81. package/dist/server/runtime/followup-appender.d.ts.map +1 -0
  82. package/dist/server/runtime/followup-appender.js +128 -0
  83. package/dist/server/runtime/host.d.ts +2 -0
  84. package/dist/server/runtime/host.d.ts.map +1 -1
  85. package/dist/server/runtime/host.js +8 -0
  86. package/dist/server/runtime/provider-runner.d.ts +16 -0
  87. package/dist/server/runtime/provider-runner.d.ts.map +1 -0
  88. package/dist/server/runtime/provider-runner.js +86 -0
  89. package/dist/server/runtime/runtime-bridge.d.ts +1 -1
  90. package/dist/server/runtime/runtime-bridge.d.ts.map +1 -1
  91. package/dist/server/runtime/runtime-session.service.d.ts +1 -1
  92. package/dist/server/runtime/runtime-session.service.d.ts.map +1 -1
  93. package/dist/server/runtime/runtime-session.service.js +1 -1
  94. package/dist/server/runtime/runtime-worker.d.ts +2 -10
  95. package/dist/server/runtime/runtime-worker.d.ts.map +1 -1
  96. package/dist/server/runtime/runtime-worker.js +60 -286
  97. package/dist/server/runtime-management/managed-runtime.d.ts.map +1 -0
  98. package/dist/server/runtime-management/runtime-release.d.ts.map +1 -0
  99. package/dist/server/{runtime → runtime-management}/runtime-upgrade.d.ts +1 -0
  100. package/dist/server/runtime-management/runtime-upgrade.d.ts.map +1 -0
  101. package/dist/server/{runtime → runtime-management}/runtime-upgrade.js +14 -0
  102. package/dist/server/services/system.service.d.ts +41 -0
  103. package/dist/server/services/system.service.d.ts.map +1 -0
  104. package/dist/server/services/system.service.js +154 -0
  105. package/dist/server/settings/settings.service.d.ts +2 -1
  106. package/dist/server/settings/settings.service.d.ts.map +1 -1
  107. package/dist/server/settings/settings.service.js +9 -1
  108. package/dist/server/slack/shortcuts.d.ts +0 -3
  109. package/dist/server/slack/shortcuts.d.ts.map +1 -1
  110. package/dist/server/slack/shortcuts.js +0 -3
  111. package/dist/server/slack-interactions/shortcut-ids.d.ts +4 -0
  112. package/dist/server/slack-interactions/shortcut-ids.d.ts.map +1 -0
  113. package/dist/server/slack-interactions/shortcut-ids.js +3 -0
  114. package/dist/server/slack-interactions/shortcut-views.d.ts +65 -0
  115. package/dist/server/slack-interactions/shortcut-views.d.ts.map +1 -0
  116. package/dist/server/slack-interactions/shortcut-views.js +211 -0
  117. package/dist/server/{slack → slack-interactions}/shortcut.service.d.ts +2 -47
  118. package/dist/server/slack-interactions/shortcut.service.d.ts.map +1 -0
  119. package/dist/server/slack-interactions/shortcut.service.js +227 -0
  120. package/dist/server/storage/jsonl-log.d.ts +9 -0
  121. package/dist/server/storage/jsonl-log.d.ts.map +1 -1
  122. package/dist/server/storage/jsonl-log.js +36 -1
  123. package/dist/server/storage/schema/server.store.d.ts +5 -0
  124. package/dist/server/storage/schema/server.store.d.ts.map +1 -1
  125. package/dist/server/storage/schema/server.store.js +2 -1
  126. package/dist/server/storage/schema/wake-queue.store.d.ts +15 -1
  127. package/dist/server/storage/schema/wake-queue.store.d.ts.map +1 -1
  128. package/dist/server/storage/schema/wake-queue.store.js +5 -3
  129. package/dist/server/tests/agent-config.test.d.ts +2 -0
  130. package/dist/server/tests/agent-config.test.d.ts.map +1 -0
  131. package/dist/server/tests/agent-config.test.js +229 -0
  132. package/dist/server/tests/agent-runtime.test.d.ts +2 -0
  133. package/dist/server/tests/agent-runtime.test.d.ts.map +1 -0
  134. package/dist/server/tests/agent-runtime.test.js +1110 -0
  135. package/dist/server/tests/agent-seed-memory.test.d.ts +2 -0
  136. package/dist/server/tests/agent-seed-memory.test.d.ts.map +1 -0
  137. package/dist/server/tests/agent-seed-memory.test.js +16 -0
  138. package/dist/server/tests/agent-skills.test.d.ts +2 -0
  139. package/dist/server/tests/agent-skills.test.d.ts.map +1 -0
  140. package/dist/server/tests/agent-skills.test.js +140 -0
  141. package/dist/server/tests/anima-home.d.ts +2 -0
  142. package/dist/server/tests/anima-home.d.ts.map +1 -0
  143. package/dist/server/tests/anima-home.js +4 -0
  144. package/dist/server/tests/cli-file.test.d.ts +2 -0
  145. package/dist/server/tests/cli-file.test.d.ts.map +1 -0
  146. package/dist/server/tests/cli-file.test.js +596 -0
  147. package/dist/server/tests/cli-message.test.d.ts +2 -0
  148. package/dist/server/tests/cli-message.test.d.ts.map +1 -0
  149. package/dist/server/tests/cli-message.test.js +1196 -0
  150. package/dist/server/tests/client-error-routes.test.d.ts +2 -0
  151. package/dist/server/tests/client-error-routes.test.d.ts.map +1 -0
  152. package/dist/server/tests/client-error-routes.test.js +163 -0
  153. package/dist/server/tests/config.test.d.ts +2 -0
  154. package/dist/server/tests/config.test.d.ts.map +1 -0
  155. package/dist/server/tests/config.test.js +497 -0
  156. package/dist/server/tests/default-skills.test.d.ts +2 -0
  157. package/dist/server/tests/default-skills.test.d.ts.map +1 -0
  158. package/dist/server/tests/default-skills.test.js +57 -0
  159. package/dist/server/tests/helpers/inbox.d.ts +10 -0
  160. package/dist/server/tests/helpers/inbox.d.ts.map +1 -0
  161. package/dist/server/tests/helpers/inbox.js +21 -0
  162. package/dist/server/tests/helpers/slack.d.ts +11 -0
  163. package/dist/server/tests/helpers/slack.d.ts.map +1 -0
  164. package/dist/server/tests/helpers/slack.js +17 -0
  165. package/dist/server/tests/helpers/state.d.ts +19 -0
  166. package/dist/server/tests/helpers/state.d.ts.map +1 -0
  167. package/dist/server/tests/helpers/state.js +69 -0
  168. package/dist/server/tests/inbox-slack-events.test.d.ts +2 -0
  169. package/dist/server/tests/inbox-slack-events.test.d.ts.map +1 -0
  170. package/dist/server/tests/inbox-slack-events.test.js +271 -0
  171. package/dist/server/tests/inbox.test.d.ts +2 -0
  172. package/dist/server/tests/inbox.test.d.ts.map +1 -0
  173. package/dist/server/tests/inbox.test.js +144 -0
  174. package/dist/server/tests/interactive-ask.test.d.ts +2 -0
  175. package/dist/server/tests/interactive-ask.test.d.ts.map +1 -0
  176. package/dist/server/tests/interactive-ask.test.js +387 -0
  177. package/dist/server/tests/kb.test.d.ts +2 -0
  178. package/dist/server/tests/kb.test.d.ts.map +1 -0
  179. package/dist/server/tests/kb.test.js +371 -0
  180. package/dist/server/tests/message.service.test.d.ts +2 -0
  181. package/dist/server/tests/message.service.test.d.ts.map +1 -0
  182. package/dist/server/tests/message.service.test.js +151 -0
  183. package/dist/server/tests/messages.test.d.ts +2 -0
  184. package/dist/server/tests/messages.test.d.ts.map +1 -0
  185. package/dist/server/tests/messages.test.js +12 -0
  186. package/dist/server/tests/prompt-attachments.test.d.ts +2 -0
  187. package/dist/server/tests/prompt-attachments.test.d.ts.map +1 -0
  188. package/dist/server/tests/prompt-attachments.test.js +274 -0
  189. package/dist/server/tests/provider-usage.test.d.ts +2 -0
  190. package/dist/server/tests/provider-usage.test.d.ts.map +1 -0
  191. package/dist/server/tests/provider-usage.test.js +97 -0
  192. package/dist/server/tests/reminders.test.d.ts +2 -0
  193. package/dist/server/tests/reminders.test.d.ts.map +1 -0
  194. package/dist/server/tests/reminders.test.js +383 -0
  195. package/dist/server/tests/runtime-cli.test.d.ts +2 -0
  196. package/dist/server/tests/runtime-cli.test.d.ts.map +1 -0
  197. package/dist/server/tests/runtime-cli.test.js +21 -0
  198. package/dist/server/tests/runtime-upgrade.test.d.ts +2 -0
  199. package/dist/server/tests/runtime-upgrade.test.d.ts.map +1 -0
  200. package/dist/server/tests/runtime-upgrade.test.js +307 -0
  201. package/dist/server/tests/runtime-worker.test.d.ts +2 -0
  202. package/dist/server/tests/runtime-worker.test.d.ts.map +1 -0
  203. package/dist/server/tests/runtime-worker.test.js +1021 -0
  204. package/dist/server/tests/runtime.test.d.ts +2 -0
  205. package/dist/server/tests/runtime.test.d.ts.map +1 -0
  206. package/dist/server/tests/runtime.test.js +354 -0
  207. package/dist/server/tests/services.test.d.ts +2 -0
  208. package/dist/server/tests/services.test.d.ts.map +1 -0
  209. package/dist/server/tests/services.test.js +636 -0
  210. package/dist/server/tests/slack-files.test.d.ts +2 -0
  211. package/dist/server/tests/slack-files.test.d.ts.map +1 -0
  212. package/dist/server/tests/slack-files.test.js +118 -0
  213. package/dist/server/tests/slack-shortcuts.test.d.ts +2 -0
  214. package/dist/server/tests/slack-shortcuts.test.d.ts.map +1 -0
  215. package/dist/server/tests/slack-shortcuts.test.js +310 -0
  216. package/dist/server/tests/slack.test.d.ts +2 -0
  217. package/dist/server/tests/slack.test.d.ts.map +1 -0
  218. package/dist/server/tests/slack.test.js +213 -0
  219. package/dist/server/tests/state-cache.test.d.ts +2 -0
  220. package/dist/server/tests/state-cache.test.d.ts.map +1 -0
  221. package/dist/server/tests/state-cache.test.js +184 -0
  222. package/dist/server/tests/subscriptions.test.d.ts +2 -0
  223. package/dist/server/tests/subscriptions.test.d.ts.map +1 -0
  224. package/dist/server/tests/subscriptions.test.js +292 -0
  225. package/dist/server/tests/system-service.test.d.ts +2 -0
  226. package/dist/server/tests/system-service.test.d.ts.map +1 -0
  227. package/dist/server/tests/system-service.test.js +20 -0
  228. package/dist/server/tests/url-routes.test.d.ts +2 -0
  229. package/dist/server/tests/url-routes.test.d.ts.map +1 -0
  230. package/dist/server/tests/url-routes.test.js +217 -0
  231. package/dist/server/tests/web-api.test.d.ts +2 -0
  232. package/dist/server/tests/web-api.test.d.ts.map +1 -0
  233. package/dist/server/tests/web-api.test.js +1759 -0
  234. package/dist/server/web/app.d.ts.map +1 -1
  235. package/dist/server/web/app.js +2 -0
  236. package/dist/server/web/client-error-routes.d.ts +3 -0
  237. package/dist/server/web/client-error-routes.d.ts.map +1 -0
  238. package/dist/server/web/client-error-routes.js +101 -0
  239. package/dist/server/web/system-routes.d.ts.map +1 -1
  240. package/dist/server/web/system-routes.js +21 -131
  241. package/dist/shared/agent-config.d.ts +8 -18
  242. package/dist/shared/agent-config.d.ts.map +1 -1
  243. package/dist/shared/agent-config.js +3 -4
  244. package/dist/shared/inbox.d.ts +27 -0
  245. package/dist/shared/inbox.d.ts.map +1 -1
  246. package/dist/shared/inbox.js +1 -0
  247. package/dist/shared/server-info.d.ts +6 -5
  248. package/dist/shared/server-info.d.ts.map +1 -1
  249. package/dist/shared/server-info.js +3 -1
  250. package/dist/shared/server-settings.d.ts +6 -0
  251. package/dist/shared/server-settings.d.ts.map +1 -1
  252. package/dist/shared/server-settings.js +1 -0
  253. package/dist/shared/skills.d.ts +24 -3
  254. package/dist/shared/skills.d.ts.map +1 -1
  255. package/dist/web/assets/activity-DHjip8tY.js +7 -0
  256. package/dist/web/assets/clipboard-D2NSC52v.js +1 -0
  257. package/dist/web/assets/{external-link-Dt0qTpmV.js → external-link-Bhr1WdRS.js} +1 -1
  258. package/dist/web/assets/{file-text-Dd9ys6py.js → file-text-CYMA_SMA.js} +1 -1
  259. package/dist/web/assets/{index-CzAQnbuO.js → index-CVpew409.js} +5 -5
  260. package/dist/web/assets/index-mh0uJTkY.css +2 -0
  261. package/dist/web/assets/kb-DDVpYkT9.js +35 -0
  262. package/dist/web/assets/profile-CBy6GNzp.js +1 -0
  263. package/dist/web/assets/reminders-Bbwb9QNT.js +1 -0
  264. package/dist/web/favicon.svg +1 -1
  265. package/dist/web/index.html +2 -2
  266. package/package.json +1 -1
  267. package/templates/runtime-standing-prompt.md +16 -2
  268. package/templates/skills/find-skills/SKILL.md +133 -0
  269. package/dist/server/runtime/activity-payload.d.ts.map +0 -1
  270. package/dist/server/runtime/activity-text.d.ts +0 -4
  271. package/dist/server/runtime/activity-text.d.ts.map +0 -1
  272. package/dist/server/runtime/activity-text.js +0 -16
  273. package/dist/server/runtime/managed-runtime.d.ts.map +0 -1
  274. package/dist/server/runtime/provider-contract.d.ts.map +0 -1
  275. package/dist/server/runtime/runtime-release.d.ts.map +0 -1
  276. package/dist/server/runtime/runtime-upgrade.d.ts.map +0 -1
  277. package/dist/server/slack/shortcut.service.d.ts.map +0 -1
  278. package/dist/server/slack/shortcut.service.js +0 -493
  279. package/dist/web/assets/activity-BP0C81IV.js +0 -7
  280. package/dist/web/assets/globe-DmDZSgDP.js +0 -1
  281. package/dist/web/assets/index-DKecrvN0.css +0 -2
  282. package/dist/web/assets/kb-3wFeP2Jt.js +0 -32
  283. package/dist/web/assets/profile-D7wPZXW_.js +0 -1
  284. package/dist/web/assets/reminders-DOhroVwo.js +0 -1
  285. /package/dist/server/{runtime/provider-contract.js → providers/contract.js} +0 -0
  286. /package/dist/server/{runtime → runtime-management}/managed-runtime.d.ts +0 -0
  287. /package/dist/server/{runtime → runtime-management}/managed-runtime.js +0 -0
  288. /package/dist/server/{runtime → runtime-management}/runtime-release.d.ts +0 -0
  289. /package/dist/server/{runtime → runtime-management}/runtime-release.js +0 -0
@@ -0,0 +1,118 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { slackFileFromRaw } from '../slack/slack.helper.js';
7
+ import { safeFilename } from '../storage/safe-filename.js';
8
+ import { MANUAL_FETCH_BYTES, downloadSlackFile } from '../slack/slack-file.service.js';
9
+ test('manual Slack file fetch allows large user-requested downloads', () => {
10
+ assert.ok(MANUAL_FETCH_BYTES >= 500 * 1024 * 1024);
11
+ });
12
+ test('slackFileFromRaw extracts known fields and prefers download URL', () => {
13
+ const file = slackFileFromRaw({
14
+ id: 'F1',
15
+ name: 'screenshot.png',
16
+ mimetype: 'image/png',
17
+ size: 1024,
18
+ url_private: 'https://files.slack.com/private',
19
+ url_private_download: 'https://files.slack.com/download',
20
+ });
21
+ assert.equal(file?.id, 'F1');
22
+ assert.equal(file?.name, 'screenshot.png');
23
+ assert.equal(file?.mimetype, 'image/png');
24
+ assert.equal(file?.sizeBytes, 1024);
25
+ assert.equal(file?.urlPrivate, 'https://files.slack.com/download');
26
+ });
27
+ test('safeFilename strips path separators and falls back to "file"', () => {
28
+ assert.equal(safeFilename('foo/bar.txt'), 'foo_bar.txt');
29
+ assert.equal(safeFilename(''), 'file');
30
+ assert.equal(safeFilename(' '), 'file');
31
+ });
32
+ test('downloadSlackFile writes the response body and honors Bearer auth + size cap', async () => {
33
+ const dir = await mkdtemp(join(tmpdir(), 'anima-files-'));
34
+ try {
35
+ const destPath = join(dir, 'cache/F1/data.txt');
36
+ const seen = {};
37
+ const fetchImpl = (async (url, init) => {
38
+ seen.url = url;
39
+ seen.auth = init?.headers?.['Authorization'];
40
+ const body = Buffer.from('hello');
41
+ return new Response(body, { status: 200, headers: { 'content-length': String(body.length) } });
42
+ });
43
+ const result = await downloadSlackFile({
44
+ destPath,
45
+ fetchImpl,
46
+ maxBytes: 1024,
47
+ token: 'xoxb-token',
48
+ urlPrivate: 'https://files.slack.com/example',
49
+ });
50
+ assert.equal(result.sizeBytes, 5);
51
+ assert.equal(seen.url, 'https://files.slack.com/example');
52
+ assert.equal(seen.auth, 'Bearer xoxb-token');
53
+ assert.equal(await readFile(destPath, 'utf8'), 'hello');
54
+ const oversized = (async () => new Response(Buffer.from('xxxxxxxxxxxxx'), {
55
+ status: 200,
56
+ headers: { 'content-length': '13' },
57
+ }));
58
+ await assert.rejects(downloadSlackFile({
59
+ destPath: join(dir, 'cache/F2/data.txt'),
60
+ fetchImpl: oversized,
61
+ maxBytes: 4,
62
+ token: 't',
63
+ urlPrivate: 'https://x',
64
+ }), /too large/);
65
+ const failing = (async () => new Response('nope', {
66
+ status: 403,
67
+ statusText: 'Forbidden',
68
+ }));
69
+ await assert.rejects(downloadSlackFile({
70
+ destPath: join(dir, 'cache/F3/data.txt'),
71
+ fetchImpl: failing,
72
+ maxBytes: 1024,
73
+ token: 't',
74
+ urlPrivate: 'https://x',
75
+ }), /HTTP 403/);
76
+ }
77
+ finally {
78
+ await rm(dir, { force: true, recursive: true });
79
+ }
80
+ });
81
+ test('downloadSlackFile rejects when streamed body exceeds maxBytes despite missing content-length', async () => {
82
+ const dir = await mkdtemp(join(tmpdir(), 'anima-files-'));
83
+ try {
84
+ const fetchImpl = (async () => new Response(Buffer.from('x'.repeat(100)), { status: 200, headers: {} }));
85
+ await assert.rejects(downloadSlackFile({
86
+ destPath: join(dir, 'F1/data.bin'),
87
+ fetchImpl,
88
+ maxBytes: 10,
89
+ token: 't',
90
+ urlPrivate: 'https://x',
91
+ }), /too large/);
92
+ }
93
+ finally {
94
+ await rm(dir, { force: true, recursive: true });
95
+ }
96
+ });
97
+ // Sanity-check that fixtures generated by Slack don't disappear when we round-trip them
98
+ test('writeFile + downloadSlackFile roundtrip with a temp source url is consistent', async () => {
99
+ const dir = await mkdtemp(join(tmpdir(), 'anima-files-'));
100
+ try {
101
+ const source = join(dir, 'source.txt');
102
+ await writeFile(source, 'roundtrip\n');
103
+ const fetchImpl = (async () => new Response(await readFile(source), { status: 200, headers: { 'content-length': '10' } }));
104
+ const destPath = join(dir, 'cache/F9/data.txt');
105
+ const result = await downloadSlackFile({
106
+ destPath,
107
+ fetchImpl,
108
+ maxBytes: 1024,
109
+ token: 't',
110
+ urlPrivate: 'https://x',
111
+ });
112
+ assert.equal(result.sizeBytes, 10);
113
+ assert.equal(await readFile(destPath, 'utf8'), 'roundtrip\n');
114
+ }
115
+ finally {
116
+ await rm(dir, { force: true, recursive: true });
117
+ }
118
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=slack-shortcuts.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-shortcuts.test.d.ts","sourceRoot":"","sources":["../../../server/tests/slack-shortcuts.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,310 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { WakeQueueService } from '../inbox/wake-queue.service.js';
7
+ import { SlackShortcutService } from '../slack-interactions/shortcut.service.js';
8
+ import { SLACK_STOP_CONFIRM_VIEW_CALLBACK_ID } from '../slack-interactions/shortcut-ids.js';
9
+ import { SLACK_SHORTCUTS, ensureSlackShortcutManifest, hasCommandsScope, inspectSlackShortcutManifest, parseOauthScopesHeader, slackShortcutManifestUpdateYaml, } from '../slack/shortcuts.js';
10
+ import { withAnimaHome } from './anima-home.js';
11
+ test('shortcut manifest helper adds commands scope and required shortcuts idempotently', () => {
12
+ const manifest = {
13
+ display_information: { name: 'Iris' },
14
+ features: {
15
+ app_home: {
16
+ home_tab_enabled: false,
17
+ messages_tab_enabled: true,
18
+ },
19
+ shortcuts: [
20
+ {
21
+ callback_id: 'existing.action',
22
+ description: 'Keep me',
23
+ name: 'Existing',
24
+ type: 'message',
25
+ },
26
+ {
27
+ callback_id: 'anima.stop',
28
+ description: 'Old stop shortcut',
29
+ name: 'Stop current turn',
30
+ type: 'global',
31
+ },
32
+ {
33
+ callback_id: 'anima.status',
34
+ description: 'Old status shortcut',
35
+ name: 'Show status',
36
+ type: 'global',
37
+ },
38
+ {
39
+ callback_id: 'anima.toggle_enabled',
40
+ description: 'Old enable shortcut',
41
+ name: 'Disable or enable',
42
+ type: 'global',
43
+ },
44
+ {
45
+ callback_id: 'anima.reminders',
46
+ description: 'Old reminders shortcut',
47
+ name: 'Show reminders',
48
+ type: 'global',
49
+ },
50
+ ],
51
+ },
52
+ oauth_config: {
53
+ scopes: {
54
+ bot: ['chat:write', 'users:read'],
55
+ },
56
+ },
57
+ };
58
+ assert.deepEqual(inspectSlackShortcutManifest(manifest), {
59
+ commandsScope: false,
60
+ missingShortcutCallbackIds: SLACK_SHORTCUTS.map((shortcut) => shortcut.callback_id),
61
+ ready: false,
62
+ });
63
+ const first = ensureSlackShortcutManifest(manifest);
64
+ assert.equal(first.updated, true);
65
+ assert.deepEqual(first.status, {
66
+ commandsScope: true,
67
+ missingShortcutCallbackIds: [],
68
+ ready: true,
69
+ });
70
+ const features = first.manifest.features;
71
+ const shortcuts = features.shortcuts;
72
+ assert.equal(shortcuts.length, 1 + SLACK_SHORTCUTS.length);
73
+ assert.ok(shortcuts.some((shortcut) => shortcut.callback_id === 'existing.action'));
74
+ assert.ok(shortcuts.some((shortcut) => shortcut.callback_id === 'anima.home' && shortcut.name === 'Home'));
75
+ assert.ok(shortcuts.some((shortcut) => shortcut.callback_id === 'anima.hand_to_agent' && shortcut.type === 'message'));
76
+ assert.equal(shortcuts.some((shortcut) => shortcut.callback_id === 'anima.reminders'), false);
77
+ assert.equal(shortcuts.some((shortcut) => shortcut.callback_id === 'anima.status'), false);
78
+ assert.equal(shortcuts.some((shortcut) => shortcut.callback_id === 'anima.stop'), false);
79
+ assert.equal(shortcuts.some((shortcut) => shortcut.callback_id === 'anima.toggle_enabled'), false);
80
+ const scopes = first.manifest.oauth_config.scopes.bot;
81
+ assert.deepEqual(scopes, ['chat:write', 'commands', 'users:read']);
82
+ const second = ensureSlackShortcutManifest(first.manifest);
83
+ assert.equal(second.updated, false);
84
+ assert.deepEqual(second.manifest, first.manifest);
85
+ });
86
+ test('shortcut manifest helper corrects wrong shortcut type without byte-equality matching', () => {
87
+ const manifest = {
88
+ features: {
89
+ shortcuts: [
90
+ {
91
+ callback_id: 'anima.hand_to_agent',
92
+ description: 'Wrong type',
93
+ name: 'Hand to agent',
94
+ type: 'global',
95
+ },
96
+ ],
97
+ },
98
+ oauth_config: { scopes: { bot: ['commands'] } },
99
+ };
100
+ const before = inspectSlackShortcutManifest(manifest);
101
+ assert.equal(before.commandsScope, true);
102
+ assert.ok(before.missingShortcutCallbackIds.includes('anima.hand_to_agent'));
103
+ const updated = ensureSlackShortcutManifest(manifest);
104
+ const shortcuts = updated.manifest.features.shortcuts;
105
+ assert.ok(shortcuts.some((shortcut) => shortcut.callback_id === 'anima.hand_to_agent' && shortcut.type === 'message'));
106
+ assert.equal(updated.status.ready, true);
107
+ });
108
+ test('oauth scope header parser detects commands scope', () => {
109
+ assert.deepEqual(parseOauthScopesHeader('chat:write, commands,users:read'), ['chat:write', 'commands', 'users:read']);
110
+ assert.equal(hasCommandsScope(parseOauthScopesHeader('chat:write,users:read')), false);
111
+ assert.equal(hasCommandsScope(parseOauthScopesHeader('chat:write,commands')), true);
112
+ });
113
+ test('shortcut manifest update YAML describes the manual migration block', () => {
114
+ const yaml = slackShortcutManifestUpdateYaml();
115
+ assert.match(yaml, /oauth_config:\n scopes:\n bot:\n - commands/);
116
+ assert.match(yaml, /callback_id: anima.home/);
117
+ assert.match(yaml, /callback_id: anima.hand_to_agent/);
118
+ assert.doesNotMatch(yaml, /callback_id: anima.reminders/);
119
+ assert.doesNotMatch(yaml, /callback_id: anima.status/);
120
+ assert.doesNotMatch(yaml, /callback_id: anima.stop/);
121
+ assert.doesNotMatch(yaml, /callback_id: anima.toggle_enabled/);
122
+ });
123
+ test('home shortcut opens the agent home modal without queueing agent work', async () => {
124
+ const client = fakeWebClient();
125
+ const activities = [];
126
+ const stopped = [];
127
+ const service = new SlackShortcutService({
128
+ activityRecorder: {
129
+ record: async (agentId, input) => {
130
+ activities.push({ agentId, input });
131
+ return { activityId: 'actv_test', createdAt: '2026-05-26T12:00:00.000Z', ...input };
132
+ },
133
+ },
134
+ agentService: fakeAgentService({ id: 'scout', displayName: 'Scout' }),
135
+ now: () => new Date('2026-05-26T12:10:00.000Z'),
136
+ reminderServiceForAgent: fakeReminderService([
137
+ {
138
+ nextDueAt: '2026-05-26T13:00:00.000Z',
139
+ reminderId: 'reminder-1',
140
+ title: 'Check build',
141
+ },
142
+ ]),
143
+ runtimeService: {
144
+ getStatus: async () => ({
145
+ agentId: 'scout',
146
+ currentItemId: 'item-123',
147
+ currentItemStartedAt: '2026-05-26T12:00:00.000Z',
148
+ itemCount: 3,
149
+ queueDepth: 2,
150
+ }),
151
+ stopCurrentItem: async (agentId) => {
152
+ stopped.push(agentId);
153
+ },
154
+ },
155
+ });
156
+ await service.handleShortcut({
157
+ agentId: 'scout',
158
+ body: { callback_id: 'anima.home', trigger_id: 'trigger-1', user: { id: 'U1' } },
159
+ client: client.client,
160
+ });
161
+ assert.equal(client.opened.length, 1);
162
+ const modal = client.opened[0]?.view;
163
+ assert.ok(modal);
164
+ assert.equal(modal.title.text, 'Home');
165
+ assert.equal(modal.callback_id, SLACK_STOP_CONFIRM_VIEW_CALLBACK_ID);
166
+ assert.equal(modal.submit?.text, 'Stop');
167
+ const modalText = modal.blocks.map((block) => 'text' in block ? block.text.text : '').join('\n');
168
+ assert.match(modalText, /\*Scout\*/);
169
+ assert.match(modalText, /\*Working\*/);
170
+ assert.match(modalText, /10m/);
171
+ assert.match(modalText, /Reminders/);
172
+ assert.match(modalText, /Check build/);
173
+ const resultView = await service.confirmStop({
174
+ agentId: 'scout',
175
+ userId: 'U1',
176
+ view: { private_metadata: modal.private_metadata },
177
+ });
178
+ assert.deepEqual(stopped, ['scout']);
179
+ assert.equal(resultView.title.text, 'Stop requested');
180
+ assert.deepEqual(activities.map((activity) => activity.input), [
181
+ {
182
+ payload: { itemId: 'item-123', outcome: 'stop_requested', userId: 'U1' },
183
+ type: 'anima.shortcut.stop',
184
+ },
185
+ ]);
186
+ });
187
+ test('home shortcut omits Stop when the agent is idle', async () => {
188
+ const client = fakeWebClient();
189
+ const service = new SlackShortcutService({
190
+ agentService: fakeAgentService({ id: 'scout', displayName: 'Scout' }),
191
+ reminderServiceForAgent: fakeReminderService([]),
192
+ runtimeService: {
193
+ getStatus: async () => ({
194
+ agentId: 'scout',
195
+ itemCount: 3,
196
+ queueDepth: 0,
197
+ }),
198
+ stopCurrentItem: async () => undefined,
199
+ },
200
+ });
201
+ await service.handleShortcut({
202
+ agentId: 'scout',
203
+ body: { callback_id: 'anima.home', trigger_id: 'trigger-1', user: { id: 'U1' } },
204
+ client: client.client,
205
+ });
206
+ const modal = client.opened[0]?.view;
207
+ assert.ok(modal);
208
+ assert.equal(modal.title.text, 'Home');
209
+ assert.equal(modal.callback_id, undefined);
210
+ assert.equal(modal.submit, undefined);
211
+ const modalText = modal.blocks.map((block) => 'text' in block ? block.text.text : '').join('\n');
212
+ assert.match(modalText, /\*Scout\*/);
213
+ assert.match(modalText, /\*Idle\*/);
214
+ assert.match(modalText, /None scheduled/);
215
+ });
216
+ test('message shortcut hands the source message to the agent thread and responds ephemerally', async () => {
217
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-shortcuts-'));
218
+ await writeMinimalAgentConfig(stateDir, 'scout');
219
+ const fetchCalls = [];
220
+ const previousFetch = globalThis.fetch;
221
+ globalThis.fetch = (async (url, init) => {
222
+ fetchCalls.push({ body: init?.body, url: String(url) });
223
+ return new Response('{}', { status: 200 });
224
+ });
225
+ try {
226
+ await withAnimaHome(stateDir, async () => {
227
+ const service = new SlackShortcutService({
228
+ activityRecorder: {
229
+ record: async (_agentId, input) => ({ activityId: 'actv_test', createdAt: '2026-05-26T12:00:00.000Z', ...input }),
230
+ },
231
+ });
232
+ await service.handMessageToAgent({
233
+ agentId: 'scout',
234
+ body: {
235
+ callback_id: 'anima.hand_to_agent',
236
+ channel: { id: 'C1', name: 'course-team' },
237
+ message: { text: 'Please turn this into a task.', ts: '1779790000.123456', user: 'U_SOURCE' },
238
+ response_url: 'https://hooks.slack.test/shortcut-response',
239
+ team: { id: 'T1' },
240
+ user: { id: 'U_HANDOFF' },
241
+ },
242
+ });
243
+ const item = await new WakeQueueService('scout').find('slack-shortcut-handoff:T1:C1:1779790000.123456');
244
+ assert.ok(item);
245
+ assert.equal(item.kind, 'slack');
246
+ assert.equal(item.channelId, 'C1');
247
+ assert.equal(item.threadTs, '1779790000.123456');
248
+ assert.equal(item.messageTs, '1779790000.123456');
249
+ assert.match(item.text, /<@U_HANDOFF> used the Slack message shortcut/);
250
+ assert.match(item.text, /Please turn this into a task\./);
251
+ });
252
+ }
253
+ finally {
254
+ globalThis.fetch = previousFetch;
255
+ }
256
+ assert.equal(fetchCalls.length, 1);
257
+ assert.equal(fetchCalls[0]?.url, 'https://hooks.slack.test/shortcut-response');
258
+ assert.match(String(fetchCalls[0]?.body), /Handed to the agent/);
259
+ });
260
+ function fakeWebClient() {
261
+ const opened = [];
262
+ return {
263
+ client: {
264
+ views: {
265
+ open: async (input) => {
266
+ opened.push(input);
267
+ return { ok: true };
268
+ },
269
+ },
270
+ },
271
+ opened,
272
+ };
273
+ }
274
+ function fakeAgentService(input) {
275
+ return {
276
+ serviceFor: () => ({
277
+ getConfig: async () => ({
278
+ enabled: true,
279
+ id: input.id,
280
+ profile: { displayName: input.displayName, role: '' },
281
+ provider: { kind: 'claude-code', model: 'sonnet' },
282
+ slack: { appToken: 'xapp-test', botToken: 'xoxb-test', connected: true, teamId: 'T1' },
283
+ homePath: `/tmp/${input.id}`,
284
+ }),
285
+ }),
286
+ };
287
+ }
288
+ function fakeReminderService(reminders) {
289
+ return () => ({
290
+ listReminders: async () => reminders.map((reminder) => ({
291
+ createdAt: '2026-05-26T12:00:00.000Z',
292
+ firedCount: 0,
293
+ instructions: '',
294
+ schedule: { kind: 'once' },
295
+ status: 'scheduled',
296
+ updatedAt: '2026-05-26T12:00:00.000Z',
297
+ ...reminder,
298
+ })),
299
+ });
300
+ }
301
+ async function writeMinimalAgentConfig(stateDir, agentId) {
302
+ await mkdir(join(stateDir, 'agents', agentId), { recursive: true });
303
+ await writeFile(join(stateDir, 'config.json'), '{}\n', 'utf8');
304
+ await writeFile(join(stateDir, 'agents', agentId, 'config.json'), JSON.stringify({
305
+ id: agentId,
306
+ profile: { displayName: 'Scout', role: '' },
307
+ provider: { kind: 'claude-code', model: 'sonnet' },
308
+ slack: { appToken: 'xapp-test', botToken: 'xoxb-test', teamId: 'T1' },
309
+ }, null, 2), 'utf8');
310
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=slack.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack.test.d.ts","sourceRoot":"","sources":["../../../server/tests/slack.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,213 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { SlackProfileResolver } from '../inbox/slack-profiles.js';
7
+ import { SlackWorkspaceDirectoryService } from '../slack/workspace-directory.service.js';
8
+ import { resolveSlackChannelArgument } from '../tools/slack-channel-resolver.js';
9
+ import { extractReadableSlackChannelMentions, extractReadableSlackUserMentions, extractReadableSlackUserIdMentions, extractSlackChannelMentionIds, extractSlackUserMentionIds, replaceReadableSlackChannelMentions, replaceReadableSlackUserIdMentions, replaceReadableSlackUserMentions, replaceSlackChannelMentions, replaceSlackUserMentions, } from '../slack/slack.helper.js';
10
+ test('Slack text helpers extract and replace mentions', () => {
11
+ assert.deepEqual(extractSlackUserMentionIds('hi <@U123> and <@U456|legacy> and <@U123>'), ['U123', 'U456']);
12
+ assert.equal(replaceSlackUserMentions('hi <@U123> and <@U456|legacy> and <@U789>', new Map([['U123', '@alice'], ['U456', '@bob']])), 'hi @alice and @bob and @U789');
13
+ assert.deepEqual(extractSlackChannelMentionIds('see <#C123|product> and <#G456> and <#C123|product>'), ['C123', 'G456']);
14
+ assert.equal(replaceSlackChannelMentions('see <#C123|product> and <#G456>', new Map([['C123', '#support']])), 'see #support and #G456');
15
+ });
16
+ test('Slack text helpers convert readable mentions to Slack markup', () => {
17
+ assert.deepEqual(extractReadableSlackUserMentions('cc @alice, @bob.smith and <@U123>'), ['alice', 'bob.smith']);
18
+ assert.equal(replaceReadableSlackUserMentions('cc @alice, @unknown and <@U123>', new Map([['alice', 'U123']])), 'cc <@U123>, @unknown and <@U123>');
19
+ assert.deepEqual(extractReadableSlackUserMentions('试一下:@nora,中文里也能@alice'), ['nora', 'alice']);
20
+ assert.deepEqual(extractReadableSlackUserMentions('literal `@nora` stays plain'), []);
21
+ assert.equal(replaceReadableSlackUserMentions('试一下:@nora,中文里也能@alice;email a@b.com and <@U123>', new Map([['alice', 'U123'], ['nora', 'U456']])), '试一下:<@U456>,中文里也能<@U123>;email a@b.com and <@U123>');
22
+ assert.deepEqual(extractReadableSlackChannelMentions('see #product, #team-updates and <#C123>'), ['product', 'team-updates']);
23
+ assert.deepEqual(extractReadableSlackChannelMentions('options: #1 then #2, but #course-reports is a channel'), ['course-reports']);
24
+ assert.deepEqual(extractReadableSlackChannelMentions('看:#dev,中文里也能#product'), ['dev', 'product']);
25
+ assert.deepEqual(extractReadableSlackChannelMentions('literal `#dev` stays plain'), []);
26
+ assert.equal(replaceReadableSlackChannelMentions('see #product, #unknown and <#C123>', new Map([['product', 'C123']])), 'see <#C123>, #unknown and <#C123>');
27
+ assert.equal(replaceReadableSlackChannelMentions('看:#dev,中文里也能#product;url https://example.com/#section', new Map([['dev', 'C-dev'], ['product', 'C-product']])), '看:<#C-dev>,中文里也能<#C-product>;url https://example.com/#section');
28
+ });
29
+ test('Slack text helpers convert raw user ids outside code spans', () => {
30
+ assert.deepEqual(extractReadableSlackUserIdMentions('cc @U123 and `@U456` then ```\n@U789\n```'), ['U123']);
31
+ assert.equal(replaceReadableSlackUserIdMentions('cc @U123 and <@U456> but `@U789` and ```\n@U999\n```'), 'cc <@U123> and <@U456> but `@U789` and ```\n@U999\n```');
32
+ assert.equal(replaceReadableSlackUserMentions('cc `please @alice` and @alice', new Map([['alice', 'U123']])), 'cc `please @alice` and <@U123>');
33
+ });
34
+ test('Slack workspace directory resolves username and display name handles', async () => {
35
+ const client = fakeSlackApi({
36
+ users: {
37
+ list: async () => ({
38
+ members: [
39
+ {
40
+ id: 'U123',
41
+ name: 'alice',
42
+ profile: { display_name: 'alice', real_name: 'Alice Example' },
43
+ },
44
+ ],
45
+ ok: true,
46
+ }),
47
+ },
48
+ });
49
+ const directory = new SlackWorkspaceDirectoryService({ client });
50
+ assert.equal((await directory.getUserByHandle('alice')).id, 'U123');
51
+ assert.equal((await directory.getUserByHandle('Alice Example')).id, 'U123');
52
+ });
53
+ test('Slack workspace directory caches user lists by team', async () => {
54
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-slack-directory-cache-'));
55
+ const previousHome = process.env.ANIMA_HOME;
56
+ process.env.ANIMA_HOME = stateDir;
57
+ try {
58
+ let calls = 0;
59
+ const client = fakeSlackApi({
60
+ users: {
61
+ list: async () => {
62
+ calls += 1;
63
+ return {
64
+ members: [{ id: 'U123', name: 'alice', profile: { display_name: 'Alice' } }],
65
+ ok: true,
66
+ };
67
+ },
68
+ },
69
+ });
70
+ const directory = new SlackWorkspaceDirectoryService({ client, teamId: 'T123' });
71
+ assert.equal((await directory.getUserByHandle('alice')).id, 'U123');
72
+ assert.equal((await directory.getUserByHandle('alice')).id, 'U123');
73
+ assert.equal(calls, 1);
74
+ }
75
+ finally {
76
+ if (previousHome === undefined)
77
+ delete process.env.ANIMA_HOME;
78
+ else
79
+ process.env.ANIMA_HOME = previousHome;
80
+ await rm(stateDir, { force: true, recursive: true });
81
+ }
82
+ });
83
+ test('Slack directory cache accepts user change events', async () => {
84
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-slack-directory-event-'));
85
+ const previousHome = process.env.ANIMA_HOME;
86
+ process.env.ANIMA_HOME = stateDir;
87
+ try {
88
+ const client = fakeSlackApi({
89
+ users: {
90
+ list: async () => {
91
+ throw new Error('users.list should not be called for a fresh event cache hit');
92
+ },
93
+ },
94
+ });
95
+ const directory = new SlackWorkspaceDirectoryService({ client, teamId: 'T123' });
96
+ await directory.applyEvent({
97
+ team: 'T123',
98
+ type: 'user_change',
99
+ user: { id: 'U123', name: 'alice', profile: { display_name: 'Alice' } },
100
+ });
101
+ assert.equal((await directory.getUserByHandle('alice')).id, 'U123');
102
+ }
103
+ finally {
104
+ if (previousHome === undefined)
105
+ delete process.env.ANIMA_HOME;
106
+ else
107
+ process.env.ANIMA_HOME = previousHome;
108
+ await rm(stateDir, { force: true, recursive: true });
109
+ }
110
+ });
111
+ test('Slack channel resolver treats raw channel ids as already resolved', async () => {
112
+ assert.deepEqual(await resolveSlackChannelArgument({ channel: 'C123ABC' }), { id: 'C123ABC' });
113
+ });
114
+ test('Slack channel resolver requires a WebClient for readable names', async () => {
115
+ await assert.rejects(() => resolveSlackChannelArgument({ channel: '#team' }), /Slack WebClient is required to resolve Slack channel name: #team\. Pass a channel ID or configure slack\.botToken\./);
116
+ });
117
+ test('Slack channel resolver resolves readable names through the workspace directory', async () => {
118
+ const client = fakeSlackApi({
119
+ conversations: {
120
+ list: async () => ({
121
+ channels: [{ id: 'C123', name: 'team', name_normalized: 'team' }],
122
+ ok: true,
123
+ }),
124
+ },
125
+ });
126
+ assert.deepEqual(await resolveSlackChannelArgument({ channel: '#team', client }), { id: 'C123', name: 'team' });
127
+ });
128
+ test('Slack channel resolver resolves DM handles through the workspace directory', async () => {
129
+ const client = fakeSlackApi({
130
+ users: {
131
+ list: async () => ({
132
+ members: [{ id: 'U123', name: 'alice', profile: { display_name: 'Alice' } }],
133
+ ok: true,
134
+ }),
135
+ },
136
+ });
137
+ assert.deepEqual(await resolveSlackChannelArgument({ channel: '@alice', client }), {
138
+ dmHandle: 'alice',
139
+ dmUserId: 'U123',
140
+ id: 'D123',
141
+ });
142
+ });
143
+ test('SlackProfileResolver caches Slack timezone metadata with the handle profile', async () => {
144
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-slack-profile-cache-'));
145
+ const previousHome = process.env.ANIMA_HOME;
146
+ process.env.ANIMA_HOME = stateDir;
147
+ let calls = 0;
148
+ try {
149
+ const client = fakeSlackApi({
150
+ users: {
151
+ info: async () => {
152
+ calls += 1;
153
+ return {
154
+ ok: true,
155
+ user: {
156
+ id: 'U123',
157
+ name: 'alice',
158
+ profile: { display_name: 'Alice', real_name: 'Alice Lee' },
159
+ tz: 'Asia/Shanghai',
160
+ tz_label: 'China Standard Time',
161
+ tz_offset: 28800,
162
+ },
163
+ };
164
+ },
165
+ },
166
+ });
167
+ const profiles = new SlackProfileResolver();
168
+ const profile = await profiles.user({ client, teamId: 'T123', userId: 'U123' });
169
+ const cached = await profiles.user({ client, teamId: 'T123', userId: 'U123' });
170
+ assert.equal(calls, 1);
171
+ assert.equal(cached, profile);
172
+ assert.deepEqual(profile?.timezone, {
173
+ label: 'China Standard Time',
174
+ name: 'Asia/Shanghai',
175
+ offsetSeconds: 28800,
176
+ });
177
+ }
178
+ finally {
179
+ if (previousHome === undefined)
180
+ delete process.env.ANIMA_HOME;
181
+ else
182
+ process.env.ANIMA_HOME = previousHome;
183
+ await rm(stateDir, { force: true, recursive: true });
184
+ }
185
+ });
186
+ test('Slack workspace directory errors surface to callers', async () => {
187
+ const client = fakeSlackApi({
188
+ users: {
189
+ list: async () => {
190
+ throw new Error('not_in_channel');
191
+ },
192
+ },
193
+ });
194
+ await assert.rejects(() => new SlackWorkspaceDirectoryService({ client }).getUserByHandle('alice'), /not_in_channel/);
195
+ });
196
+ function fakeSlackApi(input = {}) {
197
+ return {
198
+ conversations: {
199
+ info: async () => ({ ok: true }),
200
+ list: async () => ({ channels: [], ok: true }),
201
+ open: async () => ({ channel: { id: 'D123' }, ok: true }),
202
+ ...input.conversations,
203
+ },
204
+ team: {
205
+ info: async () => ({ ok: true, team: {} }),
206
+ },
207
+ users: {
208
+ info: async () => ({ ok: true }),
209
+ list: async () => ({ members: [], ok: true }),
210
+ ...input.users,
211
+ },
212
+ };
213
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=state-cache.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-cache.test.d.ts","sourceRoot":"","sources":["../../../server/tests/state-cache.test.ts"],"names":[],"mappings":""}