@desplega.ai/agent-swarm 1.20.0 → 1.51.2

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 (561) hide show
  1. package/README.md +271 -169
  2. package/openapi.json +5015 -0
  3. package/package.json +40 -7
  4. package/plugin/commands/close-issue.md +7 -3
  5. package/plugin/commands/create-pr.md +18 -12
  6. package/plugin/commands/implement-issue.md +7 -3
  7. package/plugin/commands/respond-github.md +8 -4
  8. package/plugin/commands/review-pr.md +44 -10
  9. package/plugin/commands/start-leader.md +1 -3
  10. package/plugin/commands/start-worker.md +1 -3
  11. package/plugin/commands/work-on-task.md +22 -3
  12. package/plugin/pi-skills/close-issue/SKILL.md +90 -0
  13. package/plugin/pi-skills/create-pr/SKILL.md +99 -0
  14. package/plugin/pi-skills/implement-issue/SKILL.md +135 -0
  15. package/plugin/pi-skills/investigate-sentry-issue/SKILL.md +138 -0
  16. package/plugin/pi-skills/respond-github/SKILL.md +98 -0
  17. package/plugin/pi-skills/review-offered-task/SKILL.md +45 -0
  18. package/plugin/pi-skills/review-pr/SKILL.md +261 -0
  19. package/plugin/pi-skills/start-leader/SKILL.md +121 -0
  20. package/plugin/pi-skills/start-worker/SKILL.md +60 -0
  21. package/plugin/pi-skills/swarm-chat/SKILL.md +82 -0
  22. package/plugin/pi-skills/todos/SKILL.md +66 -0
  23. package/plugin/pi-skills/work-on-task/SKILL.md +65 -0
  24. package/plugin/skills/artifacts/examples/approval-flow.ts +34 -0
  25. package/plugin/skills/artifacts/examples/hono-dashboard.ts +31 -0
  26. package/plugin/skills/artifacts/examples/multi-artifact.ts +20 -0
  27. package/plugin/skills/artifacts/examples/static-report.sh +17 -0
  28. package/plugin/skills/artifacts/skill.md +71 -0
  29. package/src/agentmail/app.ts +65 -0
  30. package/src/agentmail/handlers.ts +262 -0
  31. package/src/agentmail/index.ts +9 -0
  32. package/src/agentmail/templates.ts +111 -0
  33. package/src/agentmail/types.ts +51 -0
  34. package/src/artifact-sdk/browser-sdk.ts +30 -0
  35. package/src/artifact-sdk/index.ts +2 -0
  36. package/src/artifact-sdk/localtunnel.d.ts +20 -0
  37. package/src/artifact-sdk/port.ts +12 -0
  38. package/src/artifact-sdk/server.ts +156 -0
  39. package/src/artifact-sdk/tunnel.ts +19 -0
  40. package/src/be/chunking.ts +193 -0
  41. package/src/be/db-queries/oauth.ts +90 -0
  42. package/src/be/db-queries/tracker.ts +182 -0
  43. package/src/be/db.ts +3327 -784
  44. package/src/be/embedding.ts +80 -0
  45. package/src/be/migrations/001_initial.sql +409 -0
  46. package/src/be/migrations/002_one_time_schedules.sql +59 -0
  47. package/src/be/migrations/003_workflows.sql +51 -0
  48. package/src/be/migrations/004_workflow_source.sql +81 -0
  49. package/src/be/migrations/005_epic_next_steps.sql +2 -0
  50. package/src/be/migrations/006_vcs_provider.sql +94 -0
  51. package/src/be/migrations/007_task_dir.sql +2 -0
  52. package/src/be/migrations/008_workflow_redesign.sql +85 -0
  53. package/src/be/migrations/009_tracker_integration.sql +144 -0
  54. package/src/be/migrations/010_step_diagnostics.sql +1 -0
  55. package/src/be/migrations/011_step_next_port.sql +1 -0
  56. package/src/be/migrations/012_trigger_schema.sql +1 -0
  57. package/src/be/migrations/013_task_output_schema.sql +2 -0
  58. package/src/be/migrations/014_prompt_templates.sql +33 -0
  59. package/src/be/migrations/015_workflow_workspace.sql +3 -0
  60. package/src/be/migrations/016_active_session_runner_session.sql +4 -0
  61. package/src/be/migrations/017_channel_activity_cursors.sql +6 -0
  62. package/src/be/migrations/018_fix_seed_double_version.sql +30 -0
  63. package/src/be/migrations/runner.ts +188 -0
  64. package/src/be/seed.ts +62 -0
  65. package/src/cli.tsx +231 -299
  66. package/src/commands/artifact.ts +241 -0
  67. package/src/commands/onboard/compose-generator.ts +169 -0
  68. package/src/commands/onboard/env-generator.ts +79 -0
  69. package/src/commands/onboard/manifest.ts +37 -0
  70. package/src/commands/onboard/presets.ts +85 -0
  71. package/src/commands/onboard/service-names.ts +47 -0
  72. package/src/commands/onboard/steps/core-credentials.tsx +111 -0
  73. package/src/commands/onboard/steps/custom-templates.tsx +168 -0
  74. package/src/commands/onboard/steps/generate.tsx +154 -0
  75. package/src/commands/onboard/steps/harness-credentials.tsx +195 -0
  76. package/src/commands/onboard/steps/harness.tsx +21 -0
  77. package/src/commands/onboard/steps/health-check.tsx +171 -0
  78. package/src/commands/onboard/steps/integration-github.tsx +105 -0
  79. package/src/commands/onboard/steps/integration-gitlab.tsx +79 -0
  80. package/src/commands/onboard/steps/integration-menu.tsx +58 -0
  81. package/src/commands/onboard/steps/integration-sentry.tsx +79 -0
  82. package/src/commands/onboard/steps/integration-slack.tsx +165 -0
  83. package/src/commands/onboard/steps/post-connect.tsx +145 -0
  84. package/src/commands/onboard/steps/post-dashboard.tsx +34 -0
  85. package/src/commands/onboard/steps/post-task.tsx +103 -0
  86. package/src/commands/onboard/steps/prereq-check.tsx +178 -0
  87. package/src/commands/onboard/steps/review.tsx +82 -0
  88. package/src/commands/onboard/steps/start.tsx +97 -0
  89. package/src/commands/onboard/templates.ts +34 -0
  90. package/src/commands/onboard/types.ts +259 -0
  91. package/src/commands/onboard.tsx +425 -0
  92. package/src/commands/runner.ts +1540 -630
  93. package/src/commands/setup.tsx +23 -38
  94. package/src/commands/shared/client-config.ts +41 -0
  95. package/src/commands/templates.ts +172 -0
  96. package/src/github/app.ts +8 -0
  97. package/src/github/handlers.ts +384 -151
  98. package/src/github/index.ts +1 -0
  99. package/src/github/mentions-aliases.test.ts +73 -0
  100. package/src/github/mentions.test.ts +3 -3
  101. package/src/github/mentions.ts +32 -6
  102. package/src/github/templates.ts +398 -0
  103. package/src/github/types.ts +1 -0
  104. package/src/gitlab/auth.ts +63 -0
  105. package/src/gitlab/handlers.ts +368 -0
  106. package/src/gitlab/index.ts +19 -0
  107. package/src/gitlab/reactions.ts +104 -0
  108. package/src/gitlab/templates.ts +140 -0
  109. package/src/gitlab/types.ts +130 -0
  110. package/src/heartbeat/heartbeat.ts +434 -0
  111. package/src/heartbeat/index.ts +1 -0
  112. package/src/heartbeat/templates.ts +30 -0
  113. package/src/hooks/hook.ts +555 -4
  114. package/src/hooks/tool-loop-detection.test.ts +158 -0
  115. package/src/hooks/tool-loop-detection.ts +167 -0
  116. package/src/http/active-sessions.ts +199 -0
  117. package/src/http/agents.ts +328 -0
  118. package/src/http/config.ts +191 -0
  119. package/src/http/core.ts +309 -0
  120. package/src/http/db-query.ts +91 -0
  121. package/src/http/ecosystem.ts +63 -0
  122. package/src/http/epics.ts +460 -0
  123. package/src/http/index.ts +216 -0
  124. package/src/http/mcp.ts +77 -0
  125. package/src/http/memory.ts +168 -0
  126. package/src/http/openapi.ts +109 -0
  127. package/src/http/poll.ts +299 -0
  128. package/src/http/prompt-templates.ts +412 -0
  129. package/src/http/repos.ts +195 -0
  130. package/src/http/route-def.ts +123 -0
  131. package/src/http/schedules.ts +426 -0
  132. package/src/http/session-data.ts +241 -0
  133. package/src/http/stats.ts +174 -0
  134. package/src/http/tasks.ts +468 -0
  135. package/src/http/trackers/index.ts +10 -0
  136. package/src/http/trackers/linear.ts +187 -0
  137. package/src/http/types.ts +12 -0
  138. package/src/http/utils.ts +87 -0
  139. package/src/http/webhooks.ts +432 -0
  140. package/src/http/workflows.ts +530 -0
  141. package/src/http.ts +1 -1890
  142. package/src/linear/README.md +65 -0
  143. package/src/linear/app.ts +48 -0
  144. package/src/linear/client.ts +18 -0
  145. package/src/linear/index.ts +1 -0
  146. package/src/linear/oauth.ts +35 -0
  147. package/src/linear/outbound.ts +212 -0
  148. package/src/linear/sync.ts +567 -0
  149. package/src/linear/templates.ts +47 -0
  150. package/src/linear/types.ts +7 -0
  151. package/src/linear/webhook.ts +104 -0
  152. package/src/oauth/README.md +66 -0
  153. package/src/oauth/index.ts +6 -0
  154. package/src/oauth/wrapper.ts +204 -0
  155. package/src/prompts/base-prompt.ts +150 -265
  156. package/src/prompts/defaults.ts +196 -0
  157. package/src/prompts/registry.ts +57 -0
  158. package/src/prompts/resolver.ts +296 -0
  159. package/src/prompts/session-templates.ts +604 -0
  160. package/src/providers/claude-adapter.ts +442 -0
  161. package/src/providers/index.ts +24 -0
  162. package/src/providers/pi-mono-adapter.ts +442 -0
  163. package/src/providers/pi-mono-extension.ts +624 -0
  164. package/src/providers/pi-mono-mcp-client.ts +124 -0
  165. package/src/providers/types.ts +75 -0
  166. package/src/scheduler/scheduler.test.ts +2 -0
  167. package/src/scheduler/scheduler.ts +231 -40
  168. package/src/server.ts +97 -6
  169. package/src/slack/HEURISTICS.md +105 -0
  170. package/src/slack/actions.ts +133 -0
  171. package/src/slack/app.ts +7 -0
  172. package/src/slack/assistant.ts +118 -0
  173. package/src/slack/blocks.ts +233 -0
  174. package/src/slack/channel-activity.ts +177 -0
  175. package/src/slack/commands.ts +31 -17
  176. package/src/slack/files.ts +1 -1
  177. package/src/slack/handlers.test.ts +114 -1
  178. package/src/slack/handlers.ts +230 -55
  179. package/src/slack/responses.ts +120 -67
  180. package/src/slack/router.ts +17 -99
  181. package/src/slack/templates.ts +55 -0
  182. package/src/slack/thread-buffer.ts +213 -0
  183. package/src/slack/watcher.ts +119 -4
  184. package/src/tests/agent-activity.test.ts +247 -0
  185. package/src/tests/agentmail-filters.test.ts +97 -0
  186. package/src/tests/artifact-sdk.test.ts +800 -0
  187. package/src/tests/base-prompt.test.ts +264 -0
  188. package/src/tests/build-pi-skills.test.ts +127 -0
  189. package/src/tests/channel-activity.test.ts +363 -0
  190. package/src/tests/claude-adapter.test.ts +126 -0
  191. package/src/tests/context-versioning.test.ts +425 -0
  192. package/src/tests/db-queries-oauth.test.ts +197 -0
  193. package/src/tests/db-queries-tracker.test.ts +230 -0
  194. package/src/tests/epics.test.ts +3 -3
  195. package/src/tests/error-tracker.test.ts +368 -0
  196. package/src/tests/fetch-resolved-env.test.ts +167 -0
  197. package/src/tests/generate-default-claude-md.test.ts +9 -1
  198. package/src/tests/generate-identity-templates.test.ts +124 -0
  199. package/src/tests/gitlab-auth.test.ts +109 -0
  200. package/src/tests/gitlab-handlers.test.ts +691 -0
  201. package/src/tests/gitlab-vcs-db.test.ts +177 -0
  202. package/src/tests/heartbeat.test.ts +364 -0
  203. package/src/tests/http-api-integration.test.ts +1698 -0
  204. package/src/tests/linear-outbound-sync.test.ts +200 -0
  205. package/src/tests/linear-webhook.test.ts +406 -0
  206. package/src/tests/match-route.test.ts +187 -0
  207. package/src/tests/memory.test.ts +737 -0
  208. package/src/tests/migration-runner-regressions.test.ts +86 -0
  209. package/src/tests/model-control.test.ts +338 -0
  210. package/src/tests/oauth-wrapper.test.ts +147 -0
  211. package/src/tests/onboard-compose.test.ts +138 -0
  212. package/src/tests/onboard-env.test.ts +174 -0
  213. package/src/tests/onboard-manifest.test.ts +137 -0
  214. package/src/tests/pi-mono-adapter.test.ts +234 -0
  215. package/src/tests/pool-session-logs.test.ts +199 -0
  216. package/src/tests/progress-dedup.test.ts +98 -0
  217. package/src/tests/prompt-template-github.test.ts +682 -0
  218. package/src/tests/prompt-template-remaining.test.ts +504 -0
  219. package/src/tests/prompt-template-resolver.test.ts +621 -0
  220. package/src/tests/prompt-template-session.test.ts +363 -0
  221. package/src/tests/prompt-templates-db.test.ts +616 -0
  222. package/src/tests/provider-adapter.test.ts +122 -0
  223. package/src/tests/provider-command-format.test.ts +98 -0
  224. package/src/tests/reload-config.test.ts +170 -0
  225. package/src/tests/runner-polling-api.test.ts +25 -20
  226. package/src/tests/scheduled-tasks.test.ts +104 -0
  227. package/src/tests/scheduler-backoff.test.ts +166 -0
  228. package/src/tests/self-improvement.test.ts +541 -0
  229. package/src/tests/session-attach.test.ts +536 -0
  230. package/src/tests/session-costs.test.ts +267 -1
  231. package/src/tests/slack-actions.test.ts +133 -0
  232. package/src/tests/slack-assistant.test.ts +136 -0
  233. package/src/tests/slack-blocks.test.ts +246 -0
  234. package/src/tests/slack-metadata-inheritance.test.ts +243 -0
  235. package/src/tests/slack-queue-offline.test.ts +174 -0
  236. package/src/tests/slack-router.test.ts +181 -0
  237. package/src/tests/slack-thread-buffer.test.ts +305 -0
  238. package/src/tests/slack-thread-followups.test.ts +298 -0
  239. package/src/tests/slack-watcher.test.ts +101 -0
  240. package/src/tests/structured-output.test.ts +307 -0
  241. package/src/tests/swarm-repos.test.ts +198 -0
  242. package/src/tests/task-cancellation.test.ts +6 -4
  243. package/src/tests/task-working-dir.test.ts +176 -0
  244. package/src/tests/template-fetch.test.ts +490 -0
  245. package/src/tests/tool-annotations.test.ts +371 -0
  246. package/src/tests/tracker-tools.test.ts +184 -0
  247. package/src/tests/update-profile-agentid.test.ts +248 -0
  248. package/src/tests/update-profile-api.test.ts +143 -3
  249. package/src/tests/update-profile-auth.test.ts +195 -0
  250. package/src/tests/validation-adapters.test.ts +86 -0
  251. package/src/tests/vcs-provider.test.ts +27 -0
  252. package/src/tests/workflow-agent-task.test.ts +196 -0
  253. package/src/tests/workflow-async-v2.test.ts +508 -0
  254. package/src/tests/workflow-convergence.test.ts +541 -0
  255. package/src/tests/workflow-definition-validation.test.ts +366 -0
  256. package/src/tests/workflow-engine-v2.test.ts +691 -0
  257. package/src/tests/workflow-executors.test.ts +736 -0
  258. package/src/tests/workflow-http-v2.test.ts +599 -0
  259. package/src/tests/workflow-integration-io.test.ts +902 -0
  260. package/src/tests/workflow-io-schemas.test.ts +624 -0
  261. package/src/tests/workflow-registry.test.ts +592 -0
  262. package/src/tests/workflow-retry-v2.test.ts +401 -0
  263. package/src/tests/workflow-retry-validation.test.ts +282 -0
  264. package/src/tests/workflow-schedule-trigger.test.ts +104 -0
  265. package/src/tests/workflow-template.test.ts +288 -0
  266. package/src/tests/workflow-trigger-schema.test.ts +359 -0
  267. package/src/tests/workflow-triggers-v2.test.ts +264 -0
  268. package/src/tests/workflow-versions.test.ts +208 -0
  269. package/src/tests/workflow-workspace.test.ts +272 -0
  270. package/src/tests/x402-client.test.ts +117 -0
  271. package/src/tests/x402-config.test.ts +182 -0
  272. package/src/tests/x402-spending-tracker.test.ts +185 -0
  273. package/src/tools/cancel-task.ts +2 -0
  274. package/src/tools/context-diff.ts +171 -0
  275. package/src/tools/context-history.ts +138 -0
  276. package/src/tools/create-channel.ts +1 -0
  277. package/src/tools/db-query.ts +78 -0
  278. package/src/tools/delete-channel.ts +132 -0
  279. package/src/tools/epics/assign-task-to-epic.ts +1 -0
  280. package/src/tools/epics/create-epic.ts +3 -2
  281. package/src/tools/epics/delete-epic.ts +2 -0
  282. package/src/tools/epics/get-epic-details.ts +2 -0
  283. package/src/tools/epics/list-epics.ts +2 -0
  284. package/src/tools/epics/unassign-task-from-epic.ts +1 -0
  285. package/src/tools/epics/update-epic.ts +7 -4
  286. package/src/tools/get-swarm.ts +2 -0
  287. package/src/tools/get-task-details.ts +2 -0
  288. package/src/tools/get-tasks.ts +27 -1
  289. package/src/tools/inject-learning.ts +106 -0
  290. package/src/tools/join-swarm.ts +17 -7
  291. package/src/tools/list-channels.ts +2 -0
  292. package/src/tools/list-services.ts +2 -0
  293. package/src/tools/memory-get.ts +56 -0
  294. package/src/tools/memory-search.ts +131 -0
  295. package/src/tools/my-agent-info.ts +2 -0
  296. package/src/tools/poll-task.ts +2 -20
  297. package/src/tools/post-message.ts +1 -0
  298. package/src/tools/prompt-templates/delete.ts +86 -0
  299. package/src/tools/prompt-templates/get.ts +89 -0
  300. package/src/tools/prompt-templates/index.ts +5 -0
  301. package/src/tools/prompt-templates/list.ts +95 -0
  302. package/src/tools/prompt-templates/preview.ts +84 -0
  303. package/src/tools/prompt-templates/set.ts +117 -0
  304. package/src/tools/read-messages.ts +2 -0
  305. package/src/tools/register-agentmail-inbox.ts +166 -0
  306. package/src/tools/register-service.ts +2 -0
  307. package/src/tools/schedules/create-schedule.ts +134 -24
  308. package/src/tools/schedules/delete-schedule.ts +2 -0
  309. package/src/tools/schedules/list-schedules.ts +20 -4
  310. package/src/tools/schedules/run-schedule-now.ts +1 -0
  311. package/src/tools/schedules/update-schedule.ts +49 -17
  312. package/src/tools/send-task.ts +132 -10
  313. package/src/tools/slack-download-file.ts +4 -2
  314. package/src/tools/slack-list-channels.ts +2 -0
  315. package/src/tools/slack-post.ts +2 -0
  316. package/src/tools/slack-read.ts +2 -0
  317. package/src/tools/slack-reply.ts +2 -0
  318. package/src/tools/slack-upload-file.ts +2 -0
  319. package/src/tools/store-progress.ts +205 -4
  320. package/src/tools/swarm-config/delete-config.ts +87 -0
  321. package/src/tools/swarm-config/get-config.ts +108 -0
  322. package/src/tools/swarm-config/index.ts +4 -0
  323. package/src/tools/swarm-config/list-config.ts +99 -0
  324. package/src/tools/swarm-config/set-config.ts +118 -0
  325. package/src/tools/task-action.ts +50 -5
  326. package/src/tools/task-dedup.ts +97 -0
  327. package/src/tools/templates.ts +53 -0
  328. package/src/tools/tool-config.ts +124 -0
  329. package/src/tools/tracker/index.ts +6 -0
  330. package/src/tools/tracker/tracker-link-epic.ts +64 -0
  331. package/src/tools/tracker/tracker-link-task.ts +64 -0
  332. package/src/tools/tracker/tracker-map-agent.ts +57 -0
  333. package/src/tools/tracker/tracker-status.ts +56 -0
  334. package/src/tools/tracker/tracker-sync-status.ts +42 -0
  335. package/src/tools/tracker/tracker-unlink.ts +41 -0
  336. package/src/tools/unregister-service.ts +2 -0
  337. package/src/tools/update-profile.ts +172 -17
  338. package/src/tools/update-service-status.ts +2 -0
  339. package/src/tools/utils.ts +10 -1
  340. package/src/tools/workflows/create-workflow.ts +129 -0
  341. package/src/tools/workflows/delete-workflow.ts +42 -0
  342. package/src/tools/workflows/get-workflow-run.ts +59 -0
  343. package/src/tools/workflows/get-workflow.ts +53 -0
  344. package/src/tools/workflows/index.ts +9 -0
  345. package/src/tools/workflows/list-workflow-runs.ts +48 -0
  346. package/src/tools/workflows/list-workflows.ts +42 -0
  347. package/src/tools/workflows/retry-workflow-run.ts +40 -0
  348. package/src/tools/workflows/trigger-workflow.ts +96 -0
  349. package/src/tools/workflows/update-workflow.ts +133 -0
  350. package/src/tracker/types.ts +51 -0
  351. package/src/types.ts +530 -14
  352. package/src/utils/credentials.test.ts +156 -0
  353. package/src/utils/credentials.ts +50 -0
  354. package/src/utils/error-tracker.ts +190 -0
  355. package/src/vcs/index.ts +15 -0
  356. package/src/vcs/types.ts +5 -0
  357. package/src/workflows/checkpoint.ts +121 -0
  358. package/src/workflows/cooldown.ts +28 -0
  359. package/src/workflows/definition.ts +235 -0
  360. package/src/workflows/engine.ts +580 -0
  361. package/src/workflows/event-bus.ts +29 -0
  362. package/src/workflows/executors/agent-task.ts +103 -0
  363. package/src/workflows/executors/base.ts +86 -0
  364. package/src/workflows/executors/code-match.ts +88 -0
  365. package/src/workflows/executors/index.ts +16 -0
  366. package/src/workflows/executors/notify.ts +93 -0
  367. package/src/workflows/executors/property-match.ts +104 -0
  368. package/src/workflows/executors/raw-llm.ts +83 -0
  369. package/src/workflows/executors/registry.ts +76 -0
  370. package/src/workflows/executors/script.ts +103 -0
  371. package/src/workflows/executors/validate.ts +215 -0
  372. package/src/workflows/executors/vcs.ts +58 -0
  373. package/src/workflows/index.ts +61 -0
  374. package/src/workflows/input.ts +46 -0
  375. package/src/workflows/json-schema-validator.ts +118 -0
  376. package/src/workflows/recovery.ts +139 -0
  377. package/src/workflows/resume.ts +229 -0
  378. package/src/workflows/retry-poller.ts +216 -0
  379. package/src/workflows/template.ts +74 -0
  380. package/src/workflows/templates.ts +86 -0
  381. package/src/workflows/triggers.ts +124 -0
  382. package/src/workflows/validation.ts +104 -0
  383. package/src/workflows/version.ts +44 -0
  384. package/src/x402/cli.ts +140 -0
  385. package/src/x402/client.ts +192 -0
  386. package/src/x402/config.ts +131 -0
  387. package/src/x402/index.ts +37 -0
  388. package/src/x402/openfort-signer.ts +83 -0
  389. package/src/x402/spending-tracker.ts +109 -0
  390. package/templates/official/coder/CLAUDE.md +49 -0
  391. package/templates/official/coder/IDENTITY.md +28 -0
  392. package/templates/official/coder/SOUL.md +43 -0
  393. package/templates/official/coder/TOOLS.md +40 -0
  394. package/templates/official/coder/config.json +23 -0
  395. package/templates/official/coder/start-up.sh +23 -0
  396. package/templates/official/content-reviewer/CLAUDE.md +68 -0
  397. package/templates/official/content-reviewer/IDENTITY.md +28 -0
  398. package/templates/official/content-reviewer/SOUL.md +44 -0
  399. package/templates/official/content-reviewer/TOOLS.md +37 -0
  400. package/templates/official/content-reviewer/config.json +23 -0
  401. package/templates/official/content-reviewer/start-up.sh +23 -0
  402. package/templates/official/content-strategist/CLAUDE.md +63 -0
  403. package/templates/official/content-strategist/IDENTITY.md +33 -0
  404. package/templates/official/content-strategist/SOUL.md +48 -0
  405. package/templates/official/content-strategist/TOOLS.md +47 -0
  406. package/templates/official/content-strategist/config.json +23 -0
  407. package/templates/official/content-strategist/start-up.sh +23 -0
  408. package/templates/official/content-writer/CLAUDE.md +72 -0
  409. package/templates/official/content-writer/IDENTITY.md +30 -0
  410. package/templates/official/content-writer/SOUL.md +46 -0
  411. package/templates/official/content-writer/TOOLS.md +44 -0
  412. package/templates/official/content-writer/config.json +23 -0
  413. package/templates/official/content-writer/start-up.sh +23 -0
  414. package/templates/official/forward-deployed-engineer/CLAUDE.md +54 -0
  415. package/templates/official/forward-deployed-engineer/IDENTITY.md +37 -0
  416. package/templates/official/forward-deployed-engineer/SOUL.md +55 -0
  417. package/templates/official/forward-deployed-engineer/config.json +21 -0
  418. package/templates/official/lead/CLAUDE.md +33 -0
  419. package/templates/official/lead/IDENTITY.md +36 -0
  420. package/templates/official/lead/SOUL.md +51 -0
  421. package/templates/official/lead/config.json +22 -0
  422. package/templates/official/researcher/CLAUDE.md +46 -0
  423. package/templates/official/researcher/IDENTITY.md +28 -0
  424. package/templates/official/researcher/SOUL.md +43 -0
  425. package/templates/official/researcher/config.json +21 -0
  426. package/templates/official/reviewer/CLAUDE.md +63 -0
  427. package/templates/official/reviewer/IDENTITY.md +28 -0
  428. package/templates/official/reviewer/SOUL.md +45 -0
  429. package/templates/official/reviewer/config.json +21 -0
  430. package/templates/official/tester/CLAUDE.md +53 -0
  431. package/templates/official/tester/IDENTITY.md +28 -0
  432. package/templates/official/tester/SOUL.md +55 -0
  433. package/templates/official/tester/config.json +21 -0
  434. package/templates/schema.ts +35 -0
  435. package/.claude/settings.local.json +0 -115
  436. package/.dockerignore +0 -61
  437. package/.editorconfig +0 -15
  438. package/.env.docker.example +0 -39
  439. package/.env.example +0 -40
  440. package/.github/workflows/ci.yml +0 -76
  441. package/.github/workflows/docker-and-deploy.yml +0 -117
  442. package/.wts-config.json +0 -4
  443. package/.wts-setup.ts +0 -102
  444. package/CLAUDE.md +0 -104
  445. package/CONTRIBUTING.md +0 -270
  446. package/DEPLOYMENT.md +0 -605
  447. package/Dockerfile +0 -57
  448. package/Dockerfile.worker +0 -157
  449. package/FAQ.md +0 -19
  450. package/MCP.md +0 -406
  451. package/UI.md +0 -40
  452. package/assets/agent-swarm-logo-orange.png +0 -0
  453. package/assets/agent-swarm-logo.png +0 -0
  454. package/assets/agent-swarm.mp4 +0 -0
  455. package/assets/agent-swarm.png +0 -0
  456. package/biome.json +0 -39
  457. package/deploy/DEPLOY.md +0 -60
  458. package/deploy/agent-swarm.service +0 -17
  459. package/deploy/docker-push.ts +0 -30
  460. package/deploy/install.ts +0 -85
  461. package/deploy/prod-db.ts +0 -42
  462. package/deploy/uninstall.ts +0 -12
  463. package/deploy/update.ts +0 -21
  464. package/docker-compose.example.yml +0 -159
  465. package/docker-entrypoint.sh +0 -352
  466. package/ecosystem.config.cjs +0 -66
  467. package/plugin/README.md +0 -1
  468. package/plugin/hooks/hooks.json +0 -71
  469. package/pyproject.toml +0 -9
  470. package/scripts/generate-mcp-docs.ts +0 -415
  471. package/slack-manifest.json +0 -71
  472. package/src/tests/get-inbox-message.test.ts +0 -145
  473. package/src/tools/get-inbox-message.ts +0 -89
  474. package/src/tools/inbox-delegate.ts +0 -113
  475. package/thoughts/shared/plans/2025-12-18-slack-integration.md +0 -1195
  476. package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +0 -732
  477. package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +0 -361
  478. package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +0 -501
  479. package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +0 -560
  480. package/thoughts/shared/plans/2025-12-23-runner-level-polling.md +0 -934
  481. package/thoughts/shared/plans/2025-12-23-runner-session-logs.md +0 -1000
  482. package/thoughts/shared/plans/2025-12-23-worker-lead-spawn-triggers.md +0 -568
  483. package/thoughts/shared/plans/2026-01-09-inverse-teleport.md +0 -1516
  484. package/thoughts/shared/plans/2026-01-12-agent-rename-pm2-control.md +0 -1133
  485. package/thoughts/shared/plans/2026-01-12-github-app-integration.md +0 -380
  486. package/thoughts/shared/plans/2026-01-12-lead-inbox-model.md +0 -876
  487. package/thoughts/shared/plans/2026-01-12-ralph-wiggum-integration.md +0 -463
  488. package/thoughts/shared/plans/2026-01-13-agent-concurrency.md +0 -691
  489. package/thoughts/shared/plans/2026-01-13-github-assignment-handling.md +0 -690
  490. package/thoughts/shared/plans/2026-01-13-prevent-duplicate-trigger-processing.md +0 -1071
  491. package/thoughts/shared/plans/2026-01-14-fix-slack-thread-context.md +0 -507
  492. package/thoughts/shared/plans/2026-01-15-scheduled-tasks-implementation.md +0 -565
  493. package/thoughts/shared/plans/2026-01-15-usage-cost-tracking-ui.md +0 -1479
  494. package/thoughts/shared/plans/2026-01-16-epics-feature-implementation.md +0 -1230
  495. package/thoughts/shared/research/.gitkeep +0 -0
  496. package/thoughts/shared/research/2025-01-09-inverse-teleport-plan-review.md +0 -420
  497. package/thoughts/shared/research/2025-12-18-slack-integration.md +0 -442
  498. package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +0 -339
  499. package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +0 -390
  500. package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +0 -376
  501. package/thoughts/shared/research/2025-12-22-runner-loop-architecture.md +0 -582
  502. package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +0 -264
  503. package/thoughts/shared/research/2026-01-13-lead-duplicate-trigger-processing.md +0 -223
  504. package/thoughts/shared/research/2026-01-14-lead-slack-thread-context.md +0 -277
  505. package/thoughts/shared/research/2026-01-15-ai-tracker-agent-swarm-integration.md +0 -376
  506. package/thoughts/shared/research/2026-01-15-auto-starting-processes-in-worker-containers.md +0 -787
  507. package/thoughts/shared/research/2026-01-15-scheduled-tasks.md +0 -390
  508. package/thoughts/shared/research/2026-01-16-epics-feature-research.md +0 -437
  509. package/thoughts/taras/plans/2026-01-22-agent-swarm-schemas.md +0 -98
  510. package/thoughts/taras/plans/2026-01-28-per-worker-claude-md.md +0 -617
  511. package/thoughts/taras/plans/2026-01-28-sentry-cli-integration.md +0 -214
  512. package/thoughts/taras/research/2026-01-22-vercel-cli-integration.md +0 -287
  513. package/thoughts/taras/research/2026-01-27-excessive-polling-issue.md +0 -311
  514. package/thoughts/taras/research/2026-01-28-per-worker-claude-md.md +0 -383
  515. package/thoughts/taras/research/2026-01-28-sentry-cli-integration.md +0 -240
  516. package/tsconfig.json +0 -37
  517. package/ui/CLAUDE.md +0 -49
  518. package/ui/bun.lock +0 -771
  519. package/ui/index.html +0 -22
  520. package/ui/package-lock.json +0 -5290
  521. package/ui/package.json +0 -33
  522. package/ui/pnpm-lock.yaml +0 -3341
  523. package/ui/postcss.config.js +0 -6
  524. package/ui/public/logo.png +0 -0
  525. package/ui/src/App.tsx +0 -63
  526. package/ui/src/components/ActivityFeed.tsx +0 -440
  527. package/ui/src/components/AgentDetailPanel.tsx +0 -733
  528. package/ui/src/components/AgentsPanel.tsx +0 -815
  529. package/ui/src/components/ChatPanel.tsx +0 -1920
  530. package/ui/src/components/ConfigModal.tsx +0 -253
  531. package/ui/src/components/Dashboard.tsx +0 -832
  532. package/ui/src/components/EditAgentProfileModal.tsx +0 -433
  533. package/ui/src/components/EpicDetailPage.tsx +0 -741
  534. package/ui/src/components/EpicsPanel.tsx +0 -566
  535. package/ui/src/components/Header.tsx +0 -160
  536. package/ui/src/components/JsonViewer.tsx +0 -171
  537. package/ui/src/components/ScheduledTaskDetailPanel.tsx +0 -517
  538. package/ui/src/components/ScheduledTasksPanel.tsx +0 -639
  539. package/ui/src/components/ServicesPanel.tsx +0 -622
  540. package/ui/src/components/SessionLogPanel.tsx +0 -1219
  541. package/ui/src/components/StatsBar.tsx +0 -321
  542. package/ui/src/components/StatusBadge.tsx +0 -168
  543. package/ui/src/components/TaskDetailPanel.tsx +0 -903
  544. package/ui/src/components/TasksPanel.tsx +0 -614
  545. package/ui/src/components/UsageCharts.tsx +0 -216
  546. package/ui/src/components/UsageTab.tsx +0 -394
  547. package/ui/src/hooks/queries.ts +0 -353
  548. package/ui/src/hooks/useAutoScroll.ts +0 -83
  549. package/ui/src/index.css +0 -257
  550. package/ui/src/lib/api.ts +0 -268
  551. package/ui/src/lib/config.ts +0 -35
  552. package/ui/src/lib/contentPreview.ts +0 -208
  553. package/ui/src/lib/theme.ts +0 -214
  554. package/ui/src/lib/utils.ts +0 -88
  555. package/ui/src/main.tsx +0 -28
  556. package/ui/src/types/api.ts +0 -323
  557. package/ui/src/vite-env.d.ts +0 -1
  558. package/ui/tailwind.config.js +0 -37
  559. package/ui/tsconfig.json +0 -31
  560. package/ui/vite.config.ts +0 -35
  561. /package/{thoughts/shared/plans → templates/community}/.gitkeep +0 -0
@@ -0,0 +1,1698 @@
1
+ /**
2
+ * Exhaustive HTTP API Integration Tests
3
+ *
4
+ * Spawns the actual HTTP server with a temporary SQLite database
5
+ * and tests every API endpoint for correct behavior.
6
+ */
7
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8
+ import { randomUUID } from "node:crypto";
9
+ import { unlink } from "node:fs/promises";
10
+ import type { Subprocess } from "bun";
11
+ import { Webhook } from "svix";
12
+
13
+ const TEST_PORT = 19876;
14
+ const TEST_DB_PATH = `/tmp/test-http-integration-${Date.now()}.sqlite`;
15
+ const BASE = `http://localhost:${TEST_PORT}`;
16
+
17
+ let serverProc: Subprocess;
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Helpers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ async function api(
24
+ method: string,
25
+ path: string,
26
+ opts: { body?: unknown; agentId?: string; headers?: Record<string, string> } = {},
27
+ // biome-ignore lint/suspicious/noExplicitAny: test helper needs flexible body type
28
+ ): Promise<{ status: number; body: any; ok: boolean }> {
29
+ const headers: Record<string, string> = {
30
+ "Content-Type": "application/json",
31
+ ...opts.headers,
32
+ };
33
+ if (opts.agentId) headers["x-agent-id"] = opts.agentId;
34
+
35
+ const res = await fetch(`${BASE}${path}`, {
36
+ method,
37
+ headers,
38
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
39
+ });
40
+
41
+ const text = await res.text();
42
+ // biome-ignore lint/suspicious/noExplicitAny: body can be parsed JSON or raw text
43
+ let body: any;
44
+ try {
45
+ body = JSON.parse(text);
46
+ } catch {
47
+ body = text;
48
+ }
49
+ return { status: res.status, body, ok: res.ok };
50
+ }
51
+
52
+ const get = (p: string, o?: Parameters<typeof api>[2]) => api("GET", p, o);
53
+ const post = (p: string, o?: Parameters<typeof api>[2]) => api("POST", p, o);
54
+ const put = (p: string, o?: Parameters<typeof api>[2]) => api("PUT", p, o);
55
+ const del = (p: string, o?: Parameters<typeof api>[2]) => api("DELETE", p, o);
56
+
57
+ async function waitForServer(url: string, timeoutMs = 15000) {
58
+ const start = Date.now();
59
+ while (Date.now() - start < timeoutMs) {
60
+ try {
61
+ const r = await fetch(url);
62
+ if (r.ok) return;
63
+ } catch {
64
+ // not ready yet
65
+ }
66
+ await Bun.sleep(200);
67
+ }
68
+ throw new Error(`Server did not start within ${timeoutMs}ms`);
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Shared state — IDs created during tests for cross-test references
73
+ // ---------------------------------------------------------------------------
74
+ const ids = {
75
+ leadAgent: randomUUID(),
76
+ workerAgent: randomUUID(),
77
+ workerAgent2: randomUUID(),
78
+ task: "",
79
+ task2: "",
80
+ epic: "",
81
+ channel: "",
82
+ message: "",
83
+ config: "",
84
+ repo: "",
85
+ session: "",
86
+ };
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Server lifecycle
90
+ // ---------------------------------------------------------------------------
91
+
92
+ beforeAll(async () => {
93
+ // Clean up any leftover test DB
94
+ try {
95
+ await unlink(TEST_DB_PATH);
96
+ } catch {}
97
+ try {
98
+ await unlink(`${TEST_DB_PATH}-wal`);
99
+ } catch {}
100
+ try {
101
+ await unlink(`${TEST_DB_PATH}-shm`);
102
+ } catch {}
103
+
104
+ serverProc = Bun.spawn(["bun", "src/http.ts"], {
105
+ cwd: `${import.meta.dir}/../..`,
106
+ env: {
107
+ ...process.env,
108
+ PORT: String(TEST_PORT),
109
+ DATABASE_PATH: TEST_DB_PATH,
110
+ API_KEY: "", // no auth required
111
+ CAPABILITIES: "core,task-pool,messaging,profiles,services,scheduling,epics,memory",
112
+ // Disable optional integrations
113
+ SLACK_BOT_TOKEN: "",
114
+ GITHUB_WEBHOOK_SECRET: "",
115
+ AGENTMAIL_API_KEY: "",
116
+ },
117
+ stdout: "ignore",
118
+ stderr: "ignore",
119
+ });
120
+
121
+ await waitForServer(`${BASE}/health`);
122
+ }, 20000);
123
+
124
+ afterAll(async () => {
125
+ if (serverProc) {
126
+ serverProc.kill();
127
+ // Wait for process to exit
128
+ try {
129
+ await serverProc.exited;
130
+ } catch {}
131
+ }
132
+ await Bun.sleep(300);
133
+ try {
134
+ await unlink(TEST_DB_PATH);
135
+ } catch {}
136
+ try {
137
+ await unlink(`${TEST_DB_PATH}-wal`);
138
+ } catch {}
139
+ try {
140
+ await unlink(`${TEST_DB_PATH}-shm`);
141
+ } catch {}
142
+ });
143
+
144
+ // ===========================================================================
145
+ // 1. Health & Core
146
+ // ===========================================================================
147
+
148
+ describe("Health & Core", () => {
149
+ test("GET /health returns ok", async () => {
150
+ const { status, body } = await get("/health");
151
+ expect(status).toBe(200);
152
+ expect(body.status).toBe("ok");
153
+ expect(body.version).toBeDefined();
154
+ });
155
+
156
+ test("OPTIONS returns 204 (CORS preflight)", async () => {
157
+ const res = await fetch(`${BASE}/api/agents`, { method: "OPTIONS" });
158
+ expect(res.status).toBe(204);
159
+ expect(res.headers.get("access-control-allow-origin")).toBe("*");
160
+ });
161
+
162
+ test("GET /me without agent ID returns 400", async () => {
163
+ const { status, body } = await get("/me");
164
+ expect(status).toBe(400);
165
+ expect(body.error).toContain("Missing X-Agent-ID");
166
+ });
167
+
168
+ test("GET /me with non-existent agent returns 404", async () => {
169
+ const { status } = await get("/me", { agentId: randomUUID() });
170
+ expect(status).toBe(404);
171
+ });
172
+
173
+ test("POST /ping without agent ID returns 400", async () => {
174
+ const { status } = await post("/ping");
175
+ expect(status).toBe(400);
176
+ });
177
+
178
+ test("POST /close without agent ID returns 400", async () => {
179
+ const { status } = await post("/close");
180
+ expect(status).toBe(400);
181
+ });
182
+
183
+ test("unknown route returns 404", async () => {
184
+ const { status } = await get("/api/nonexistent");
185
+ expect(status).toBe(404);
186
+ });
187
+ });
188
+
189
+ // ===========================================================================
190
+ // 2. Agents
191
+ // ===========================================================================
192
+
193
+ describe("Agents", () => {
194
+ test("POST /api/agents — missing name returns 400", async () => {
195
+ const { status, body } = await post("/api/agents", { body: {} });
196
+ expect(status).toBe(400);
197
+ expect(body.error).toContain("name");
198
+ });
199
+
200
+ test("POST /api/agents — create lead agent", async () => {
201
+ const { status, body } = await post("/api/agents", {
202
+ agentId: ids.leadAgent,
203
+ body: {
204
+ name: "TestLead",
205
+ isLead: true,
206
+ description: "Lead agent for tests",
207
+ role: "lead",
208
+ capabilities: ["core", "messaging"],
209
+ maxTasks: 3,
210
+ },
211
+ });
212
+ expect(status).toBe(201);
213
+ expect(body.id).toBe(ids.leadAgent);
214
+ expect(body.name).toBe("TestLead");
215
+ expect(body.isLead).toBeTruthy();
216
+ });
217
+
218
+ test("POST /api/agents — create worker agent", async () => {
219
+ const { status, body } = await post("/api/agents", {
220
+ agentId: ids.workerAgent,
221
+ body: { name: "TestWorker", role: "worker", maxTasks: 2 },
222
+ });
223
+ expect(status).toBe(201);
224
+ expect(body.id).toBe(ids.workerAgent);
225
+ expect(body.name).toBe("TestWorker");
226
+ });
227
+
228
+ test("POST /api/agents — create second worker", async () => {
229
+ const { status, body } = await post("/api/agents", {
230
+ agentId: ids.workerAgent2,
231
+ body: { name: "TestWorker2" },
232
+ });
233
+ expect(status).toBe(201);
234
+ expect(body.id).toBe(ids.workerAgent2);
235
+ });
236
+
237
+ test("POST /api/agents — re-register existing agent returns 200", async () => {
238
+ const { status, body } = await post("/api/agents", {
239
+ agentId: ids.workerAgent,
240
+ body: { name: "TestWorker" },
241
+ });
242
+ expect(status).toBe(200);
243
+ expect(body.id).toBe(ids.workerAgent);
244
+ });
245
+
246
+ test("GET /api/agents — list all agents", async () => {
247
+ const { status, body } = await get("/api/agents");
248
+ expect(status).toBe(200);
249
+ expect(body.agents).toBeDefined();
250
+ expect(Array.isArray(body.agents)).toBe(true);
251
+ expect(body.agents.length).toBeGreaterThanOrEqual(3);
252
+ });
253
+
254
+ test("GET /api/agents/:id — get specific agent", async () => {
255
+ const { status, body } = await get(`/api/agents/${ids.workerAgent}`);
256
+ expect(status).toBe(200);
257
+ expect(body.id).toBe(ids.workerAgent);
258
+ expect(body.name).toBe("TestWorker");
259
+ // Should include capacity info
260
+ expect(body.capacity).toBeDefined();
261
+ });
262
+
263
+ test("GET /api/agents/:id — non-existent returns 404", async () => {
264
+ const { status } = await get(`/api/agents/${randomUUID()}`);
265
+ expect(status).toBe(404);
266
+ });
267
+
268
+ test("PUT /api/agents/:id/name — rename agent", async () => {
269
+ const { status, body } = await put(`/api/agents/${ids.workerAgent}/name`, {
270
+ body: { name: "RenamedWorker" },
271
+ });
272
+ expect(status).toBe(200);
273
+ expect(body.name).toBe("RenamedWorker");
274
+ });
275
+
276
+ test("PUT /api/agents/:id/name — missing name returns 400", async () => {
277
+ const { status } = await put(`/api/agents/${ids.workerAgent}/name`, { body: {} });
278
+ expect(status).toBe(400);
279
+ });
280
+
281
+ test("PUT /api/agents/:id/profile — update profile", async () => {
282
+ const { status, body } = await put(`/api/agents/${ids.workerAgent}/profile`, {
283
+ body: {
284
+ description: "Updated description",
285
+ role: "senior-worker",
286
+ capabilities: ["typescript", "testing"],
287
+ },
288
+ });
289
+ expect(status).toBe(200);
290
+ expect(body.description).toBe("Updated description");
291
+ expect(body.role).toBe("senior-worker");
292
+ });
293
+
294
+ test("PUT /api/agents/:id/profile — non-existent returns 404", async () => {
295
+ const { status } = await put(`/api/agents/${randomUUID()}/profile`, {
296
+ body: { role: "ghost" },
297
+ });
298
+ expect(status).toBe(404);
299
+ });
300
+
301
+ test("PUT /api/agents/:id/activity — update activity timestamp", async () => {
302
+ const { status } = await put(`/api/agents/${ids.workerAgent}/activity`);
303
+ expect(status).toBe(204);
304
+ });
305
+
306
+ test("PUT /api/agents/:id/activity — non-existent still returns 204 (fire-and-forget)", async () => {
307
+ // Activity endpoint doesn't validate agent existence; it just runs an UPDATE
308
+ const { status } = await put(`/api/agents/${randomUUID()}/activity`);
309
+ expect(status).toBe(204);
310
+ });
311
+
312
+ test("GET /api/agents/:id/setup-script — returns setup script", async () => {
313
+ const { status, body } = await get(`/api/agents/${ids.workerAgent}/setup-script`);
314
+ expect(status).toBe(200);
315
+ expect(body).toBeDefined();
316
+ });
317
+
318
+ test("GET /api/agents/:id/setup-script — non-existent returns 404", async () => {
319
+ const { status } = await get(`/api/agents/${randomUUID()}/setup-script`);
320
+ expect(status).toBe(404);
321
+ });
322
+
323
+ test("GET /me — returns agent info", async () => {
324
+ const { status, body } = await get("/me", { agentId: ids.workerAgent });
325
+ expect(status).toBe(200);
326
+ expect(body.id).toBe(ids.workerAgent);
327
+ });
328
+
329
+ test("POST /ping — heartbeat updates agent", async () => {
330
+ const { status } = await post("/ping", { agentId: ids.workerAgent });
331
+ expect(status).toBe(204);
332
+ });
333
+
334
+ test("POST /ping — non-existent agent returns 404", async () => {
335
+ const { status } = await post("/ping", { agentId: randomUUID() });
336
+ expect(status).toBe(404);
337
+ });
338
+ });
339
+
340
+ // ===========================================================================
341
+ // 3. Tasks
342
+ // ===========================================================================
343
+
344
+ describe("Tasks", () => {
345
+ test("POST /api/tasks — missing task field returns 400", async () => {
346
+ const { status, body } = await post("/api/tasks", { body: {} });
347
+ expect(status).toBe(400);
348
+ expect(body.error).toContain("task");
349
+ });
350
+
351
+ test("POST /api/tasks — create unassigned task", async () => {
352
+ const { status, body } = await post("/api/tasks", {
353
+ agentId: ids.leadAgent,
354
+ body: {
355
+ task: "Integration test task 1",
356
+ taskType: "test",
357
+ tags: ["automated"],
358
+ priority: 75,
359
+ },
360
+ });
361
+ expect(status).toBe(201);
362
+ expect(body.id).toBeDefined();
363
+ expect(body.task).toBe("Integration test task 1");
364
+ ids.task = body.id;
365
+ });
366
+
367
+ test("POST /api/tasks — create assigned task", async () => {
368
+ const { status, body } = await post("/api/tasks", {
369
+ agentId: ids.leadAgent,
370
+ body: {
371
+ task: "Integration test task 2",
372
+ agentId: ids.workerAgent,
373
+ },
374
+ });
375
+ expect(status).toBe(201);
376
+ expect(body.agentId).toBe(ids.workerAgent);
377
+ ids.task2 = body.id;
378
+ });
379
+
380
+ test("GET /api/tasks — list all tasks", async () => {
381
+ const { status, body } = await get("/api/tasks");
382
+ expect(status).toBe(200);
383
+ expect(body.tasks).toBeDefined();
384
+ expect(Array.isArray(body.tasks)).toBe(true);
385
+ expect(body.total).toBeGreaterThanOrEqual(2);
386
+ });
387
+
388
+ test("GET /api/tasks?status=pending — filter by status", async () => {
389
+ const { status, body } = await get("/api/tasks?status=pending");
390
+ expect(status).toBe(200);
391
+ for (const t of body.tasks) {
392
+ expect(t.status).toBe("pending");
393
+ }
394
+ });
395
+
396
+ test("GET /api/tasks?agentId=... — filter by agent", async () => {
397
+ const { status, body } = await get(`/api/tasks?agentId=${ids.workerAgent}`);
398
+ expect(status).toBe(200);
399
+ for (const t of body.tasks) {
400
+ expect(t.agentId).toBe(ids.workerAgent);
401
+ }
402
+ });
403
+
404
+ test("GET /api/tasks?search=... — search tasks", async () => {
405
+ const { status, body } = await get("/api/tasks?search=Integration+test");
406
+ expect(status).toBe(200);
407
+ expect(body.tasks.length).toBeGreaterThanOrEqual(1);
408
+ });
409
+
410
+ test("GET /api/tasks?limit=1 — pagination", async () => {
411
+ const { status, body } = await get("/api/tasks?limit=1");
412
+ expect(status).toBe(200);
413
+ expect(body.tasks.length).toBe(1);
414
+ expect(body.total).toBeGreaterThanOrEqual(2);
415
+ });
416
+
417
+ test("GET /api/tasks/:id — get specific task", async () => {
418
+ const { status, body } = await get(`/api/tasks/${ids.task}`);
419
+ expect(status).toBe(200);
420
+ expect(body.id).toBe(ids.task);
421
+ expect(body.task).toBe("Integration test task 1");
422
+ });
423
+
424
+ test("GET /api/tasks/:id — non-existent returns 404", async () => {
425
+ const { status } = await get(`/api/tasks/${randomUUID()}`);
426
+ expect(status).toBe(404);
427
+ });
428
+
429
+ test("PUT /api/tasks/:id/claude-session — update session ID", async () => {
430
+ const sessionId = randomUUID();
431
+ const { status, body } = await put(`/api/tasks/${ids.task2}/claude-session`, {
432
+ agentId: ids.workerAgent,
433
+ body: { claudeSessionId: sessionId },
434
+ });
435
+ expect(status).toBe(200);
436
+ expect(body.claudeSessionId).toBe(sessionId);
437
+ });
438
+
439
+ test("PUT /api/tasks/:id/claude-session — missing fields returns 400", async () => {
440
+ const { status } = await put(`/api/tasks/${ids.task2}/claude-session`, {
441
+ body: {},
442
+ });
443
+ expect(status).toBe(400);
444
+ });
445
+
446
+ test("POST /api/tasks/:id/finish — missing agent ID returns 400", async () => {
447
+ const { status } = await post(`/api/tasks/${ids.task2}/finish`, {
448
+ body: { status: "completed" },
449
+ });
450
+ expect(status).toBe(400);
451
+ });
452
+
453
+ test("POST /api/tasks/:id/finish — invalid status returns 400", async () => {
454
+ const { status } = await post(`/api/tasks/${ids.task2}/finish`, {
455
+ agentId: ids.workerAgent,
456
+ body: { status: "invalid" },
457
+ });
458
+ expect(status).toBe(400);
459
+ });
460
+
461
+ test("POST /api/tasks/:id/finish — pending task returns alreadyFinished", async () => {
462
+ // Task is in 'pending' status (not in_progress), so finish returns success with alreadyFinished flag
463
+ const { status, body } = await post(`/api/tasks/${ids.task2}/finish`, {
464
+ agentId: ids.workerAgent,
465
+ body: { status: "completed", output: "Test output" },
466
+ });
467
+ expect(status).toBe(200);
468
+ expect(body.success).toBe(true);
469
+ expect(body.alreadyFinished).toBe(true);
470
+ });
471
+
472
+ test("POST /api/tasks/:id/finish — wrong agent returns 403", async () => {
473
+ // Create a task for worker, try to finish as worker2
474
+ const createRes = await post("/api/tasks", {
475
+ agentId: ids.leadAgent,
476
+ body: { task: "Forbidden finish test", agentId: ids.workerAgent },
477
+ });
478
+ const taskId = createRes.body.id;
479
+ const { status } = await post(`/api/tasks/${taskId}/finish`, {
480
+ agentId: ids.workerAgent2,
481
+ body: { status: "completed" },
482
+ });
483
+ expect(status).toBe(403);
484
+ });
485
+
486
+ test("POST /api/tasks/:id/finish — non-existent task returns 404", async () => {
487
+ const { status } = await post(`/api/tasks/${randomUUID()}/finish`, {
488
+ agentId: ids.workerAgent,
489
+ body: { status: "failed", failureReason: "Not found" },
490
+ });
491
+ expect(status).toBe(404);
492
+ });
493
+ });
494
+
495
+ // ===========================================================================
496
+ // 4. Task Pause / Resume
497
+ // ===========================================================================
498
+
499
+ describe("Task Pause & Resume", () => {
500
+ let pauseTaskId: string;
501
+
502
+ test("create a task to pause", async () => {
503
+ const { body } = await post("/api/tasks", {
504
+ agentId: ids.leadAgent,
505
+ body: { task: "Pause test task", agentId: ids.workerAgent2 },
506
+ });
507
+ pauseTaskId = body.id;
508
+ });
509
+
510
+ test("POST /api/tasks/:id/pause — missing agent ID returns 400", async () => {
511
+ const { status } = await post(`/api/tasks/${pauseTaskId}/pause`);
512
+ expect(status).toBe(400);
513
+ });
514
+
515
+ test("POST /api/tasks/:id/pause — pause the task", async () => {
516
+ const { status } = await post(`/api/tasks/${pauseTaskId}/pause`, {
517
+ agentId: ids.workerAgent2,
518
+ });
519
+ // Pausing a pending task may succeed or fail depending on implementation
520
+ expect([200, 400, 403]).toContain(status);
521
+ });
522
+
523
+ test("GET /api/paused-tasks — requires agent ID", async () => {
524
+ const { status } = await get("/api/paused-tasks");
525
+ expect(status).toBe(400);
526
+ });
527
+
528
+ test("GET /api/paused-tasks — returns paused tasks for agent", async () => {
529
+ const { status } = await get("/api/paused-tasks", { agentId: ids.workerAgent2 });
530
+ expect(status).toBe(200);
531
+ });
532
+
533
+ test("POST /api/tasks/:id/resume — non-existent returns 404", async () => {
534
+ const { status } = await post(`/api/tasks/${randomUUID()}/resume`, {
535
+ agentId: ids.workerAgent,
536
+ });
537
+ expect(status).toBe(404);
538
+ });
539
+ });
540
+
541
+ // ===========================================================================
542
+ // 4b. Task Cancel
543
+ // ===========================================================================
544
+
545
+ describe("Task Cancel", () => {
546
+ let cancelTaskId: string;
547
+
548
+ test("POST /api/tasks/:id/cancel — cancel pending task with reason", async () => {
549
+ const { body: t } = await post("/api/tasks", {
550
+ agentId: ids.leadAgent,
551
+ body: { task: "Cancel test - pending task" },
552
+ });
553
+ cancelTaskId = t.id;
554
+
555
+ const { status, body } = await post(`/api/tasks/${cancelTaskId}/cancel`, {
556
+ body: { reason: "test cancellation" },
557
+ });
558
+ expect(status).toBe(200);
559
+ expect(body.success).toBe(true);
560
+ expect(body.task).toBeDefined();
561
+ expect(body.task.status).toBe("cancelled");
562
+ expect(body.task.failureReason).toContain("test cancellation");
563
+ });
564
+
565
+ test("POST /api/tasks/:id/cancel — already-cancelled task returns 400", async () => {
566
+ // cancelTaskId was cancelled in the previous test
567
+ const { status } = await post(`/api/tasks/${cancelTaskId}/cancel`);
568
+ expect(status).toBe(400);
569
+ });
570
+
571
+ test("POST /api/tasks/:id/cancel — non-existent task returns 404", async () => {
572
+ const { status } = await post(`/api/tasks/${randomUUID()}/cancel`);
573
+ expect(status).toBe(404);
574
+ });
575
+
576
+ test("POST /api/tasks/:id/cancel — cancel without reason", async () => {
577
+ const { body: t } = await post("/api/tasks", {
578
+ agentId: ids.leadAgent,
579
+ body: { task: "Cancel test - no reason" },
580
+ });
581
+ const { status, body } = await post(`/api/tasks/${t.id}/cancel`);
582
+ expect(status).toBe(200);
583
+ expect(body.success).toBe(true);
584
+ expect(body.task.status).toBe("cancelled");
585
+ });
586
+ });
587
+
588
+ // ===========================================================================
589
+ // 5. Cancelled Tasks
590
+ // ===========================================================================
591
+
592
+ describe("Cancelled Tasks", () => {
593
+ test("GET /cancelled-tasks — missing agent ID returns 400", async () => {
594
+ const { status } = await get("/cancelled-tasks");
595
+ expect(status).toBe(400);
596
+ });
597
+
598
+ test("GET /cancelled-tasks — returns cancelled list for agent", async () => {
599
+ const { status, body } = await get("/cancelled-tasks", { agentId: ids.workerAgent });
600
+ expect(status).toBe(200);
601
+ expect(body.cancelled).toBeDefined();
602
+ expect(Array.isArray(body.cancelled)).toBe(true);
603
+ });
604
+
605
+ test("GET /cancelled-tasks?taskId=... — check specific task", async () => {
606
+ const { status, body } = await get(`/cancelled-tasks?taskId=${ids.task}`, {
607
+ agentId: ids.workerAgent,
608
+ });
609
+ expect(status).toBe(200);
610
+ expect(body.cancelled).toBeDefined();
611
+ });
612
+ });
613
+
614
+ // ===========================================================================
615
+ // 6. Session Logs
616
+ // ===========================================================================
617
+
618
+ describe("Session Logs", () => {
619
+ const sessionId = randomUUID();
620
+
621
+ test("POST /api/session-logs — missing sessionId returns 400", async () => {
622
+ const { status, body } = await post("/api/session-logs", {
623
+ body: { iteration: 1, lines: ["test"] },
624
+ });
625
+ expect(status).toBe(400);
626
+ expect(body.error).toContain("sessionId");
627
+ });
628
+
629
+ test("POST /api/session-logs — missing iteration returns 400", async () => {
630
+ const { status } = await post("/api/session-logs", {
631
+ body: { sessionId, lines: ["test"] },
632
+ });
633
+ expect(status).toBe(400);
634
+ });
635
+
636
+ test("POST /api/session-logs — missing lines returns 400", async () => {
637
+ const { status } = await post("/api/session-logs", {
638
+ body: { sessionId, iteration: 1 },
639
+ });
640
+ expect(status).toBe(400);
641
+ });
642
+
643
+ test("POST /api/session-logs — store logs successfully", async () => {
644
+ const { status, body } = await post("/api/session-logs", {
645
+ body: {
646
+ sessionId,
647
+ taskId: ids.task,
648
+ iteration: 1,
649
+ lines: ["line 1", "line 2"],
650
+ },
651
+ });
652
+ expect(status).toBe(201);
653
+ expect(body.success).toBe(true);
654
+ expect(body.count).toBe(2);
655
+ });
656
+
657
+ test("GET /api/tasks/:id/session-logs — get logs for task", async () => {
658
+ const { status, body } = await get(`/api/tasks/${ids.task}/session-logs`);
659
+ expect(status).toBe(200);
660
+ expect(body.logs).toBeDefined();
661
+ });
662
+
663
+ test("GET /api/tasks/:id/session-logs — non-existent task returns 404", async () => {
664
+ const { status } = await get(`/api/tasks/${randomUUID()}/session-logs`);
665
+ expect(status).toBe(404);
666
+ });
667
+ });
668
+
669
+ // ===========================================================================
670
+ // 7. Session Costs
671
+ // ===========================================================================
672
+
673
+ describe("Session Costs", () => {
674
+ const sessionId = randomUUID();
675
+
676
+ test("POST /api/session-costs — missing sessionId returns 400", async () => {
677
+ const { status } = await post("/api/session-costs", {
678
+ body: { agentId: ids.workerAgent, totalCostUsd: 0.5 },
679
+ });
680
+ expect(status).toBe(400);
681
+ });
682
+
683
+ test("POST /api/session-costs — missing agentId returns 400", async () => {
684
+ const { status } = await post("/api/session-costs", {
685
+ body: { sessionId, totalCostUsd: 0.5 },
686
+ });
687
+ expect(status).toBe(400);
688
+ });
689
+
690
+ test("POST /api/session-costs — missing totalCostUsd returns 400", async () => {
691
+ const { status } = await post("/api/session-costs", {
692
+ body: { sessionId, agentId: ids.workerAgent },
693
+ });
694
+ expect(status).toBe(400);
695
+ });
696
+
697
+ test("POST /api/session-costs — store cost successfully", async () => {
698
+ const { status, body } = await post("/api/session-costs", {
699
+ body: {
700
+ sessionId,
701
+ agentId: ids.workerAgent,
702
+ taskId: ids.task,
703
+ totalCostUsd: 1.23,
704
+ inputTokens: 5000,
705
+ outputTokens: 2000,
706
+ model: "opus",
707
+ numTurns: 5,
708
+ durationMs: 30000,
709
+ },
710
+ });
711
+ expect(status).toBe(201);
712
+ expect(body.success).toBe(true);
713
+ expect(body.cost).toBeDefined();
714
+ });
715
+
716
+ test("GET /api/session-costs — list costs", async () => {
717
+ const { status, body } = await get("/api/session-costs");
718
+ expect(status).toBe(200);
719
+ expect(body.costs).toBeDefined();
720
+ expect(Array.isArray(body.costs)).toBe(true);
721
+ });
722
+
723
+ test("GET /api/session-costs/summary — get cost summary", async () => {
724
+ const { status } = await get("/api/session-costs/summary");
725
+ expect(status).toBe(200);
726
+ });
727
+
728
+ test("GET /api/session-costs/dashboard — get dashboard data", async () => {
729
+ const { status } = await get("/api/session-costs/dashboard");
730
+ expect(status).toBe(200);
731
+ });
732
+ });
733
+
734
+ // ===========================================================================
735
+ // 8. Active Sessions
736
+ // ===========================================================================
737
+
738
+ describe("Active Sessions", () => {
739
+ test("POST /api/active-sessions — missing fields returns 400", async () => {
740
+ const { status } = await post("/api/active-sessions", {
741
+ body: { agentId: ids.workerAgent },
742
+ });
743
+ expect(status).toBe(400);
744
+ });
745
+
746
+ test("POST /api/active-sessions — create session", async () => {
747
+ const { status, body } = await post("/api/active-sessions", {
748
+ body: {
749
+ agentId: ids.workerAgent,
750
+ taskId: ids.task,
751
+ triggerType: "task",
752
+ taskDescription: "Test session",
753
+ },
754
+ });
755
+ expect(status).toBe(201);
756
+ expect(body.session).toBeDefined();
757
+ ids.session = body.session.id;
758
+ });
759
+
760
+ test("GET /api/active-sessions — list sessions", async () => {
761
+ const { status, body } = await get("/api/active-sessions");
762
+ expect(status).toBe(200);
763
+ expect(body.sessions).toBeDefined();
764
+ expect(Array.isArray(body.sessions)).toBe(true);
765
+ });
766
+
767
+ test("GET /api/active-sessions?agentId=... — filter by agent", async () => {
768
+ const { status, body } = await get(`/api/active-sessions?agentId=${ids.workerAgent}`);
769
+ expect(status).toBe(200);
770
+ expect(body.sessions).toBeDefined();
771
+ });
772
+
773
+ test("PUT /api/active-sessions/heartbeat/:taskId — update heartbeat", async () => {
774
+ const { status } = await put(`/api/active-sessions/heartbeat/${ids.task}`);
775
+ expect(status).toBe(200);
776
+ });
777
+
778
+ test("DELETE /api/active-sessions/:id — delete by session ID", async () => {
779
+ const { status } = await del(`/api/active-sessions/${ids.session}`);
780
+ expect(status).toBe(200);
781
+ });
782
+
783
+ test("DELETE /api/active-sessions/by-task/:taskId — delete by task", async () => {
784
+ // Create a session then delete by task
785
+ await post("/api/active-sessions", {
786
+ body: { agentId: ids.workerAgent2, taskId: ids.task, triggerType: "task" },
787
+ });
788
+ const { status } = await del(`/api/active-sessions/by-task/${ids.task}`);
789
+ expect(status).toBe(200);
790
+ });
791
+
792
+ test("POST /api/active-sessions/cleanup — cleanup stale sessions", async () => {
793
+ const { status, body } = await post("/api/active-sessions/cleanup", {
794
+ body: { maxAgeMinutes: 1 },
795
+ });
796
+ expect(status).toBe(200);
797
+ expect(typeof body.cleaned).toBe("number");
798
+ });
799
+
800
+ test("POST /api/active-sessions/cleanup — cleanup by agent", async () => {
801
+ const { status, body } = await post("/api/active-sessions/cleanup", {
802
+ body: { agentId: ids.workerAgent },
803
+ });
804
+ expect(status).toBe(200);
805
+ expect(typeof body.cleaned).toBe("number");
806
+ });
807
+ });
808
+
809
+ // ===========================================================================
810
+ // 9. Stats, Logs, Services, Scheduled Tasks
811
+ // ===========================================================================
812
+
813
+ describe("Stats & Metadata", () => {
814
+ test("GET /api/stats — returns stats", async () => {
815
+ const { status, body } = await get("/api/stats");
816
+ expect(status).toBe(200);
817
+ expect(body.agents).toBeDefined();
818
+ expect(body.tasks).toBeDefined();
819
+ expect(body.agents.total).toBeGreaterThanOrEqual(3);
820
+ });
821
+
822
+ test("GET /api/logs — returns logs", async () => {
823
+ const { status, body } = await get("/api/logs");
824
+ expect(status).toBe(200);
825
+ expect(body.logs).toBeDefined();
826
+ expect(Array.isArray(body.logs)).toBe(true);
827
+ });
828
+
829
+ test("GET /api/services — returns services list", async () => {
830
+ const { status, body } = await get("/api/services");
831
+ expect(status).toBe(200);
832
+ expect(body.services).toBeDefined();
833
+ expect(Array.isArray(body.services)).toBe(true);
834
+ });
835
+
836
+ test("GET /api/scheduled-tasks — returns scheduled tasks", async () => {
837
+ const { status, body } = await get("/api/scheduled-tasks");
838
+ expect(status).toBe(200);
839
+ expect(body.scheduledTasks).toBeDefined();
840
+ expect(Array.isArray(body.scheduledTasks)).toBe(true);
841
+ });
842
+
843
+ test("GET /api/concurrent-context — returns concurrency info", async () => {
844
+ const { status } = await get("/api/concurrent-context");
845
+ expect(status).toBe(200);
846
+ });
847
+ });
848
+
849
+ // ===========================================================================
850
+ // 10. Polling
851
+ // ===========================================================================
852
+
853
+ describe("Polling", () => {
854
+ test("GET /api/poll — requires agent ID", async () => {
855
+ const { status } = await get("/api/poll");
856
+ expect(status).toBe(400);
857
+ });
858
+
859
+ test("GET /api/poll — returns poll response for agent", async () => {
860
+ const { status, body } = await get("/api/poll", { agentId: ids.workerAgent });
861
+ expect(status).toBe(200);
862
+ expect(body).toBeDefined();
863
+ });
864
+ });
865
+
866
+ // ===========================================================================
867
+ // 11. Epics
868
+ // ===========================================================================
869
+
870
+ describe("Epics", () => {
871
+ test("POST /api/epics — missing fields returns 400", async () => {
872
+ const { status, body } = await post("/api/epics", {
873
+ body: { name: "No Goal" },
874
+ });
875
+ expect(status).toBe(400);
876
+ expect(body.error).toContain("goal");
877
+ });
878
+
879
+ test("POST /api/epics — create epic", async () => {
880
+ const { status, body } = await post("/api/epics", {
881
+ agentId: ids.leadAgent,
882
+ body: {
883
+ name: "Test Epic",
884
+ goal: "Test all API endpoints",
885
+ description: "Comprehensive testing",
886
+ priority: 80,
887
+ tags: ["testing"],
888
+ },
889
+ });
890
+ expect(status).toBe(201);
891
+ expect(body.id).toBeDefined();
892
+ expect(body.name).toBe("Test Epic");
893
+ ids.epic = body.id;
894
+ });
895
+
896
+ test("GET /api/epics — list epics", async () => {
897
+ const { status, body } = await get("/api/epics");
898
+ expect(status).toBe(200);
899
+ expect(body.epics).toBeDefined();
900
+ expect(Array.isArray(body.epics)).toBe(true);
901
+ expect(body.total).toBeGreaterThanOrEqual(1);
902
+ });
903
+
904
+ test("GET /api/epics/:id — get specific epic", async () => {
905
+ const { status, body } = await get(`/api/epics/${ids.epic}`);
906
+ expect(status).toBe(200);
907
+ expect(body.id).toBe(ids.epic);
908
+ expect(body.name).toBe("Test Epic");
909
+ });
910
+
911
+ test("GET /api/epics/:id — non-existent returns 404", async () => {
912
+ const { status } = await get(`/api/epics/${randomUUID()}`);
913
+ expect(status).toBe(404);
914
+ });
915
+
916
+ test("PUT /api/epics/:id — update epic", async () => {
917
+ const { status } = await put(`/api/epics/${ids.epic}`, {
918
+ body: { goal: "Updated goal", status: "active" },
919
+ });
920
+ expect(status).toBe(200);
921
+ });
922
+
923
+ test("PUT /api/epics/:id — non-existent returns 404", async () => {
924
+ const { status } = await put(`/api/epics/${randomUUID()}`, {
925
+ body: { goal: "Ghost" },
926
+ });
927
+ expect(status).toBe(404);
928
+ });
929
+
930
+ test("POST /api/epics/:id/tasks — assign task to epic", async () => {
931
+ const { status } = await post(`/api/epics/${ids.epic}/tasks`, {
932
+ body: { taskId: ids.task },
933
+ });
934
+ expect(status).toBe(200);
935
+ });
936
+
937
+ test("POST /api/epics/:id/tasks — missing taskId returns 400", async () => {
938
+ const { status } = await post(`/api/epics/${ids.epic}/tasks`, {
939
+ body: {},
940
+ });
941
+ expect(status).toBe(400);
942
+ });
943
+
944
+ test("POST /api/epics/:id/tasks — non-existent epic returns 404", async () => {
945
+ const { status } = await post(`/api/epics/${randomUUID()}/tasks`, {
946
+ body: { taskId: ids.task },
947
+ });
948
+ expect(status).toBe(404);
949
+ });
950
+
951
+ test("DELETE /api/epics/:id — delete the test epic", async () => {
952
+ const { status } = await del(`/api/epics/${ids.epic}`);
953
+ expect(status).toBe(200);
954
+ });
955
+
956
+ test("DELETE /api/epics/:id — non-existent returns 404", async () => {
957
+ const { status } = await del(`/api/epics/${randomUUID()}`);
958
+ expect(status).toBe(404);
959
+ });
960
+ });
961
+
962
+ // ===========================================================================
963
+ // 11b. Schedule CRUD
964
+ // ===========================================================================
965
+
966
+ describe("Schedule CRUD", () => {
967
+ let scheduleId: string;
968
+
969
+ test("POST /api/schedules — create schedule with cron expression", async () => {
970
+ const { status, body } = await post("/api/schedules", {
971
+ body: {
972
+ name: "test-schedule",
973
+ taskTemplate: "Run integration test suite",
974
+ cronExpression: "0 * * * *",
975
+ },
976
+ });
977
+ expect(status).toBe(201);
978
+ expect(body.id).toBeDefined();
979
+ expect(body.name).toBe("test-schedule");
980
+ expect(body.nextRunAt).toBeDefined();
981
+ expect(body.enabled).toBe(true);
982
+ scheduleId = body.id;
983
+ });
984
+
985
+ test("POST /api/schedules — missing name returns 400", async () => {
986
+ const { status } = await post("/api/schedules", {
987
+ body: { taskTemplate: "do something" },
988
+ });
989
+ expect(status).toBe(400);
990
+ });
991
+
992
+ test("POST /api/schedules — missing taskTemplate returns 400", async () => {
993
+ const { status } = await post("/api/schedules", {
994
+ body: { name: "no-template" },
995
+ });
996
+ expect(status).toBe(400);
997
+ });
998
+
999
+ test("POST /api/schedules — invalid cron expression returns 400", async () => {
1000
+ const { status } = await post("/api/schedules", {
1001
+ body: {
1002
+ name: "bad-cron",
1003
+ taskTemplate: "do something",
1004
+ cronExpression: "not-a-cron",
1005
+ },
1006
+ });
1007
+ expect(status).toBe(400);
1008
+ });
1009
+
1010
+ test("POST /api/schedules — duplicate name returns 409", async () => {
1011
+ const { status } = await post("/api/schedules", {
1012
+ body: {
1013
+ name: "test-schedule",
1014
+ taskTemplate: "duplicate",
1015
+ cronExpression: "0 * * * *",
1016
+ },
1017
+ });
1018
+ expect([400, 409]).toContain(status);
1019
+ });
1020
+
1021
+ test("GET /api/schedules/:id — fetch single schedule", async () => {
1022
+ const { status, body } = await get(`/api/schedules/${scheduleId}`);
1023
+ expect(status).toBe(200);
1024
+ expect(body.id).toBe(scheduleId);
1025
+ expect(body.name).toBe("test-schedule");
1026
+ expect(body.taskTemplate).toBe("Run integration test suite");
1027
+ });
1028
+
1029
+ test("GET /api/schedules/:id — non-existent returns 404", async () => {
1030
+ const { status } = await get(`/api/schedules/${randomUUID()}`);
1031
+ expect(status).toBe(404);
1032
+ });
1033
+
1034
+ test("PUT /api/schedules/:id — update name", async () => {
1035
+ const { status, body } = await put(`/api/schedules/${scheduleId}`, {
1036
+ body: { name: "updated-schedule" },
1037
+ });
1038
+ expect(status).toBe(200);
1039
+ expect(body.name).toBe("updated-schedule");
1040
+ });
1041
+
1042
+ test("PUT /api/schedules/:id — disable schedule clears nextRunAt", async () => {
1043
+ const { status, body } = await put(`/api/schedules/${scheduleId}`, {
1044
+ body: { enabled: false },
1045
+ });
1046
+ expect(status).toBe(200);
1047
+ expect(body.enabled).toBe(false);
1048
+ expect(body.nextRunAt).toBeFalsy();
1049
+ });
1050
+
1051
+ test("PUT /api/schedules/:id — re-enable schedule recalculates nextRunAt", async () => {
1052
+ const { status, body } = await put(`/api/schedules/${scheduleId}`, {
1053
+ body: { enabled: true },
1054
+ });
1055
+ expect(status).toBe(200);
1056
+ expect(body.enabled).toBe(true);
1057
+ expect(body.nextRunAt).toBeDefined();
1058
+ expect(body.nextRunAt).not.toBeNull();
1059
+ });
1060
+
1061
+ test("PUT /api/schedules/:id — non-existent returns 404", async () => {
1062
+ const { status } = await put(`/api/schedules/${randomUUID()}`, {
1063
+ body: { name: "ghost" },
1064
+ });
1065
+ expect(status).toBe(404);
1066
+ });
1067
+
1068
+ test("POST /api/schedules/:id/run — run now creates a task", async () => {
1069
+ const { status, body } = await post(`/api/schedules/${scheduleId}/run`);
1070
+ expect(status).toBe(200);
1071
+ expect(body.schedule).toBeDefined();
1072
+ expect(body.task).toBeDefined();
1073
+ expect(body.task.id).toBeDefined();
1074
+ });
1075
+
1076
+ test("POST /api/schedules/:id/run — disabled schedule returns 400", async () => {
1077
+ // Disable the schedule first
1078
+ await put(`/api/schedules/${scheduleId}`, {
1079
+ body: { enabled: false },
1080
+ });
1081
+ const { status } = await post(`/api/schedules/${scheduleId}/run`);
1082
+ expect(status).toBe(400);
1083
+ // Re-enable for subsequent tests
1084
+ await put(`/api/schedules/${scheduleId}`, {
1085
+ body: { enabled: true },
1086
+ });
1087
+ });
1088
+
1089
+ test("POST /api/schedules/:id/run — non-existent returns 404", async () => {
1090
+ const { status } = await post(`/api/schedules/${randomUUID()}/run`);
1091
+ expect(status).toBe(404);
1092
+ });
1093
+
1094
+ test("DELETE /api/schedules/:id — delete schedule", async () => {
1095
+ const { status, body } = await del(`/api/schedules/${scheduleId}`);
1096
+ expect(status).toBe(200);
1097
+ expect(body.success).toBe(true);
1098
+ });
1099
+
1100
+ test("DELETE /api/schedules/:id — non-existent returns 404", async () => {
1101
+ const { status } = await del(`/api/schedules/${randomUUID()}`);
1102
+ expect(status).toBe(404);
1103
+ });
1104
+
1105
+ test("GET /api/schedules/:id — deleted schedule returns 404", async () => {
1106
+ const { status } = await get(`/api/schedules/${scheduleId}`);
1107
+ expect(status).toBe(404);
1108
+ });
1109
+ });
1110
+
1111
+ // ===========================================================================
1112
+ // 12. Channels & Messages
1113
+ // ===========================================================================
1114
+
1115
+ describe("Channels & Messages", () => {
1116
+ test("GET /api/channels — list channels (includes default 'general')", async () => {
1117
+ const { status, body } = await get("/api/channels");
1118
+ expect(status).toBe(200);
1119
+ expect(body.channels).toBeDefined();
1120
+ expect(Array.isArray(body.channels)).toBe(true);
1121
+ // biome-ignore lint/suspicious/noExplicitAny: channel shape varies
1122
+ const general = body.channels.find((c: any) => c.name === "general");
1123
+ expect(general).toBeDefined();
1124
+ ids.channel = general.id;
1125
+ });
1126
+
1127
+ test("POST /api/channels/:id/messages — post a message", async () => {
1128
+ const { status, body } = await post(`/api/channels/${ids.channel}/messages`, {
1129
+ body: { content: "Hello from integration test!", agentId: ids.workerAgent },
1130
+ });
1131
+ expect(status).toBe(201);
1132
+ expect(body.id).toBeDefined();
1133
+ ids.message = body.id;
1134
+ });
1135
+
1136
+ test("POST /api/channels/:id/messages — missing content returns 400", async () => {
1137
+ const { status } = await post(`/api/channels/${ids.channel}/messages`, {
1138
+ body: {},
1139
+ });
1140
+ expect(status).toBe(400);
1141
+ });
1142
+
1143
+ test("POST /api/channels/:id/messages — non-existent channel returns 404", async () => {
1144
+ const { status } = await post(`/api/channels/${randomUUID()}/messages`, {
1145
+ body: { content: "Ghost channel" },
1146
+ });
1147
+ expect(status).toBe(404);
1148
+ });
1149
+
1150
+ test("GET /api/channels/:id/messages — get messages", async () => {
1151
+ const { status, body } = await get(`/api/channels/${ids.channel}/messages`);
1152
+ expect(status).toBe(200);
1153
+ const messages = body.messages || body;
1154
+ expect(Array.isArray(messages)).toBe(true);
1155
+ expect(messages.length).toBeGreaterThanOrEqual(1);
1156
+ });
1157
+
1158
+ test("GET /api/channels/:id/messages — non-existent channel returns 404", async () => {
1159
+ const { status } = await get(`/api/channels/${randomUUID()}/messages`);
1160
+ expect(status).toBe(404);
1161
+ });
1162
+
1163
+ test("GET /api/channels/:id/messages/:messageId/thread — get thread", async () => {
1164
+ const { status } = await get(`/api/channels/${ids.channel}/messages/${ids.message}/thread`);
1165
+ expect(status).toBe(200);
1166
+ });
1167
+
1168
+ // --- Channel Create / Delete ---
1169
+
1170
+ let createdChannelId: string;
1171
+
1172
+ test("POST /api/channels — create channel", async () => {
1173
+ const { status, body } = await post("/api/channels", {
1174
+ body: { name: "test-channel-crud" },
1175
+ });
1176
+ expect(status).toBe(201);
1177
+ expect(body.id).toBeDefined();
1178
+ expect(body.name).toBe("test-channel-crud");
1179
+ expect(body.type).toBe("public");
1180
+ createdChannelId = body.id;
1181
+ });
1182
+
1183
+ test("POST /api/channels — create with description and type", async () => {
1184
+ const { status, body } = await post("/api/channels", {
1185
+ body: { name: "test-dm-channel", description: "A DM channel", type: "dm" },
1186
+ });
1187
+ expect(status).toBe(201);
1188
+ expect(body.type).toBe("dm");
1189
+ expect(body.description).toBe("A DM channel");
1190
+ // Clean up - delete this channel
1191
+ await del(`/api/channels/${body.id}`);
1192
+ });
1193
+
1194
+ test("POST /api/channels — duplicate name returns 409", async () => {
1195
+ const { status } = await post("/api/channels", {
1196
+ body: { name: "test-channel-crud" },
1197
+ });
1198
+ expect([400, 409]).toContain(status);
1199
+ });
1200
+
1201
+ test("POST /api/channels — missing name returns 400", async () => {
1202
+ const { status } = await post("/api/channels", {
1203
+ body: {},
1204
+ });
1205
+ expect(status).toBe(400);
1206
+ });
1207
+
1208
+ test("DELETE /api/channels/:id — delete created channel", async () => {
1209
+ const { status, body } = await del(`/api/channels/${createdChannelId}`);
1210
+ expect(status).toBe(200);
1211
+ expect(body.success).toBe(true);
1212
+ });
1213
+
1214
+ test("DELETE /api/channels/:id — cannot delete general channel", async () => {
1215
+ // The general channel has the well-known ID
1216
+ const generalId = "00000000-0000-4000-8000-000000000001";
1217
+ const { status } = await del(`/api/channels/${generalId}`);
1218
+ expect([400, 403]).toContain(status);
1219
+ });
1220
+
1221
+ test("DELETE /api/channels/:id — non-existent returns 404", async () => {
1222
+ const { status } = await del(`/api/channels/${randomUUID()}`);
1223
+ expect(status).toBe(404);
1224
+ });
1225
+
1226
+ test("GET /api/channels — deleted channel not in list", async () => {
1227
+ const { status, body } = await get("/api/channels");
1228
+ expect(status).toBe(200);
1229
+ // biome-ignore lint/suspicious/noExplicitAny: channel shape varies
1230
+ const found = body.channels.find((c: any) => c.id === createdChannelId);
1231
+ expect(found).toBeUndefined();
1232
+ });
1233
+ });
1234
+
1235
+ // ===========================================================================
1236
+ // 13. Config
1237
+ // ===========================================================================
1238
+
1239
+ describe("Config", () => {
1240
+ test("PUT /api/config — missing required fields returns 400", async () => {
1241
+ const { status } = await put("/api/config", {
1242
+ body: { scope: "global" },
1243
+ });
1244
+ expect(status).toBe(400);
1245
+ });
1246
+
1247
+ test("PUT /api/config — create global config", async () => {
1248
+ const { status, body } = await put("/api/config", {
1249
+ body: {
1250
+ scope: "global",
1251
+ key: "TEST_CONFIG_KEY",
1252
+ value: "test_value",
1253
+ description: "Test config",
1254
+ },
1255
+ });
1256
+ expect(status).toBe(200);
1257
+ expect(body.key).toBe("TEST_CONFIG_KEY");
1258
+ ids.config = body.id;
1259
+ });
1260
+
1261
+ test("PUT /api/config — agent scope requires scopeId", async () => {
1262
+ const { status, body } = await put("/api/config", {
1263
+ body: { scope: "agent", key: "AGENT_KEY", value: "v" },
1264
+ });
1265
+ expect(status).toBe(400);
1266
+ expect(body.error).toContain("scopeId");
1267
+ });
1268
+
1269
+ test("PUT /api/config — create agent-scoped config", async () => {
1270
+ const { status, body } = await put("/api/config", {
1271
+ body: {
1272
+ scope: "agent",
1273
+ scopeId: ids.workerAgent,
1274
+ key: "AGENT_SPECIFIC",
1275
+ value: "agent_val",
1276
+ },
1277
+ });
1278
+ expect(status).toBe(200);
1279
+ expect(body.scope).toBe("agent");
1280
+ });
1281
+
1282
+ test("GET /api/config — list all config entries", async () => {
1283
+ const { status, body } = await get("/api/config");
1284
+ expect(status).toBe(200);
1285
+ expect(body.configs).toBeDefined();
1286
+ expect(Array.isArray(body.configs)).toBe(true);
1287
+ });
1288
+
1289
+ test("GET /api/config/:id — get specific config", async () => {
1290
+ const { status, body } = await get(`/api/config/${ids.config}`);
1291
+ expect(status).toBe(200);
1292
+ expect(body.key).toBe("TEST_CONFIG_KEY");
1293
+ });
1294
+
1295
+ test("GET /api/config/:id — non-existent returns 404", async () => {
1296
+ const { status } = await get(`/api/config/${randomUUID()}`);
1297
+ expect(status).toBe(404);
1298
+ });
1299
+
1300
+ test("GET /api/config/resolved — get merged config", async () => {
1301
+ const { status } = await get("/api/config/resolved");
1302
+ expect(status).toBe(200);
1303
+ });
1304
+
1305
+ test("GET /api/config/resolved?agentId=... — with agent scope", async () => {
1306
+ const { status } = await get(`/api/config/resolved?agentId=${ids.workerAgent}`);
1307
+ expect(status).toBe(200);
1308
+ });
1309
+
1310
+ test("DELETE /api/config/:id — delete config", async () => {
1311
+ // Create a config to delete
1312
+ const createRes = await put("/api/config", {
1313
+ body: { scope: "global", key: "DELETE_ME", value: "bye" },
1314
+ });
1315
+ const configId = createRes.body.id;
1316
+ const { status } = await del(`/api/config/${configId}`);
1317
+ expect(status).toBe(200);
1318
+ });
1319
+
1320
+ test("DELETE /api/config/:id — non-existent returns 404", async () => {
1321
+ const { status } = await del(`/api/config/${randomUUID()}`);
1322
+ expect(status).toBe(404);
1323
+ });
1324
+ });
1325
+
1326
+ // ===========================================================================
1327
+ // 14. Repos
1328
+ // ===========================================================================
1329
+
1330
+ describe("Repos", () => {
1331
+ test("POST /api/repos — missing fields returns 400", async () => {
1332
+ const { status } = await post("/api/repos", {
1333
+ body: { name: "test-repo" },
1334
+ });
1335
+ expect(status).toBe(400);
1336
+ });
1337
+
1338
+ test("POST /api/repos — create repo", async () => {
1339
+ const { status, body } = await post("/api/repos", {
1340
+ body: {
1341
+ name: "test-repo",
1342
+ url: "https://github.com/test-org/test-repo",
1343
+ defaultBranch: "main",
1344
+ description: "Test repository",
1345
+ },
1346
+ });
1347
+ expect(status).toBe(201);
1348
+ expect(body.id).toBeDefined();
1349
+ expect(body.name).toBe("test-repo");
1350
+ ids.repo = body.id;
1351
+ });
1352
+
1353
+ test("POST /api/repos — duplicate URL returns 409", async () => {
1354
+ const { status } = await post("/api/repos", {
1355
+ body: {
1356
+ name: "test-repo-dup",
1357
+ url: "https://github.com/test-org/test-repo",
1358
+ },
1359
+ });
1360
+ expect(status).toBe(409);
1361
+ });
1362
+
1363
+ test("GET /api/repos — list repos", async () => {
1364
+ const { status, body } = await get("/api/repos");
1365
+ expect(status).toBe(200);
1366
+ expect(body.repos).toBeDefined();
1367
+ expect(Array.isArray(body.repos)).toBe(true);
1368
+ });
1369
+
1370
+ test("GET /api/repos/:id — get specific repo", async () => {
1371
+ const { status, body } = await get(`/api/repos/${ids.repo}`);
1372
+ expect(status).toBe(200);
1373
+ expect(body.name).toBe("test-repo");
1374
+ });
1375
+
1376
+ test("GET /api/repos/:id — non-existent returns 404", async () => {
1377
+ const { status } = await get(`/api/repos/${randomUUID()}`);
1378
+ expect(status).toBe(404);
1379
+ });
1380
+
1381
+ test("PUT /api/repos/:id — update repo", async () => {
1382
+ const { status, body } = await put(`/api/repos/${ids.repo}`, {
1383
+ body: { defaultBranch: "develop" },
1384
+ });
1385
+ expect(status).toBe(200);
1386
+ expect(body.defaultBranch).toBe("develop");
1387
+ });
1388
+
1389
+ test("PUT /api/repos/:id — non-existent returns 404", async () => {
1390
+ const { status } = await put(`/api/repos/${randomUUID()}`, {
1391
+ body: { description: "Ghost" },
1392
+ });
1393
+ expect(status).toBe(404);
1394
+ });
1395
+
1396
+ test("DELETE /api/repos/:id — delete repo", async () => {
1397
+ // Create a repo to delete
1398
+ const createRes = await post("/api/repos", {
1399
+ body: { name: "delete-me", url: "https://github.com/test-org/delete-me" },
1400
+ });
1401
+ const repoId = createRes.body.id;
1402
+ const { status } = await del(`/api/repos/${repoId}`);
1403
+ expect(status).toBe(200);
1404
+ });
1405
+
1406
+ test("DELETE /api/repos/:id — non-existent returns 404", async () => {
1407
+ const { status } = await del(`/api/repos/${randomUUID()}`);
1408
+ expect(status).toBe(404);
1409
+ });
1410
+ });
1411
+
1412
+ // ===========================================================================
1413
+ // 15. Memory
1414
+ // ===========================================================================
1415
+
1416
+ describe("Memory", () => {
1417
+ test("POST /api/memory/index — missing required fields returns 400", async () => {
1418
+ const { status } = await post("/api/memory/index", {
1419
+ body: {},
1420
+ });
1421
+ expect(status).toBe(400);
1422
+ });
1423
+
1424
+ test("POST /api/memory/index — index content (returns 202)", async () => {
1425
+ const { status, body } = await post("/api/memory/index", {
1426
+ agentId: ids.workerAgent,
1427
+ body: {
1428
+ content: "This is a test memory about API integration testing patterns.",
1429
+ name: "test-memory",
1430
+ scope: "agent",
1431
+ source: "manual",
1432
+ },
1433
+ });
1434
+ expect(status).toBe(202);
1435
+ expect(body.queued).toBe(true);
1436
+ expect(body.memoryIds).toBeDefined();
1437
+ });
1438
+
1439
+ test("POST /api/memory/search — missing query returns 400", async () => {
1440
+ const { status } = await post("/api/memory/search", {
1441
+ body: {},
1442
+ });
1443
+ expect(status).toBe(400);
1444
+ });
1445
+
1446
+ test("POST /api/memory/search — search with valid query", async () => {
1447
+ const { status } = await post("/api/memory/search", {
1448
+ agentId: ids.workerAgent,
1449
+ body: { query: "integration testing", limit: 5 },
1450
+ });
1451
+ // May return 200 (success) or 500 (no OPENAI_API_KEY for embeddings)
1452
+ expect([200, 500]).toContain(status);
1453
+ });
1454
+ });
1455
+
1456
+ // ===========================================================================
1457
+ // 16. Ecosystem
1458
+ // ===========================================================================
1459
+
1460
+ describe("Ecosystem", () => {
1461
+ test("GET /ecosystem — missing agent ID header returns 400", async () => {
1462
+ const { status } = await get("/ecosystem");
1463
+ expect(status).toBe(400);
1464
+ });
1465
+
1466
+ test("GET /ecosystem — returns ecosystem config for agent", async () => {
1467
+ const { status, body } = await get("/ecosystem", { agentId: ids.workerAgent });
1468
+ expect(status).toBe(200);
1469
+ expect(body.apps).toBeDefined();
1470
+ expect(Array.isArray(body.apps)).toBe(true);
1471
+ });
1472
+ });
1473
+
1474
+ // ===========================================================================
1475
+ // 17. Close Agent (run near end as it changes agent state)
1476
+ // ===========================================================================
1477
+
1478
+ describe("Close Agent", () => {
1479
+ test("POST /close — close agent session", async () => {
1480
+ // Create a disposable agent to close
1481
+ const closeAgentId = randomUUID();
1482
+ await post("/api/agents", {
1483
+ agentId: closeAgentId,
1484
+ body: { name: "DisposableAgent" },
1485
+ });
1486
+
1487
+ const { status } = await post("/close", { agentId: closeAgentId });
1488
+ expect(status).toBe(204);
1489
+
1490
+ // Verify agent is now offline
1491
+ const { body } = await get(`/api/agents/${closeAgentId}`);
1492
+ expect(body.status).toBe("offline");
1493
+ });
1494
+
1495
+ test("POST /close — non-existent agent returns 404", async () => {
1496
+ const { status } = await post("/close", { agentId: randomUUID() });
1497
+ expect(status).toBe(404);
1498
+ });
1499
+ });
1500
+
1501
+ // ===========================================================================
1502
+ // AgentMail Webhooks
1503
+ // ===========================================================================
1504
+
1505
+ describe("AgentMail Webhooks (disabled)", () => {
1506
+ test("POST /api/agentmail/webhook returns 503 when disabled", async () => {
1507
+ const { status, body } = await post("/api/agentmail/webhook", {
1508
+ body: { type: "event", event_type: "message.received", event_id: "test-1" },
1509
+ });
1510
+ expect(status).toBe(503);
1511
+ expect(body.error).toContain("not configured");
1512
+ });
1513
+ });
1514
+
1515
+ describe("AgentMail Webhooks (with filters)", () => {
1516
+ const AGENTMAIL_PORT = 19877;
1517
+ const AGENTMAIL_DB = `/tmp/test-agentmail-${Date.now()}.sqlite`;
1518
+ const AGENTMAIL_BASE = `http://localhost:${AGENTMAIL_PORT}`;
1519
+ const WEBHOOK_SECRET = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"; // test-only secret
1520
+ let agentmailProc: Subprocess;
1521
+
1522
+ function signPayload(payload: unknown): { body: string; headers: Record<string, string> } {
1523
+ const wh = new Webhook(WEBHOOK_SECRET);
1524
+ const msgId = `msg_${randomUUID()}`;
1525
+ const timestamp = new Date();
1526
+ const body = JSON.stringify(payload);
1527
+ const signature = wh.sign(msgId, timestamp, body);
1528
+ return {
1529
+ body,
1530
+ headers: {
1531
+ "svix-id": msgId,
1532
+ "svix-timestamp": Math.floor(timestamp.getTime() / 1000).toString(),
1533
+ "svix-signature": signature,
1534
+ },
1535
+ };
1536
+ }
1537
+
1538
+ async function postWebhook(payload: unknown): Promise<{ status: number; body: unknown }> {
1539
+ const signed = signPayload(payload);
1540
+ const res = await fetch(`${AGENTMAIL_BASE}/api/agentmail/webhook`, {
1541
+ method: "POST",
1542
+ headers: { "Content-Type": "application/json", ...signed.headers },
1543
+ body: signed.body,
1544
+ });
1545
+ const text = await res.text();
1546
+ let parsed: unknown;
1547
+ try {
1548
+ parsed = JSON.parse(text);
1549
+ } catch {
1550
+ parsed = text;
1551
+ }
1552
+ return { status: res.status, body: parsed };
1553
+ }
1554
+
1555
+ function makePayload(
1556
+ overrides: { inboxId?: string; from?: string | string[]; eventId?: string } = {},
1557
+ ) {
1558
+ return {
1559
+ type: "event",
1560
+ event_type: "message.received",
1561
+ event_id: overrides.eventId ?? randomUUID(),
1562
+ message: {
1563
+ message_id: randomUUID(),
1564
+ thread_id: randomUUID(),
1565
+ inbox_id: overrides.inboxId ?? "bot@x.dev",
1566
+ organization_id: "org-1",
1567
+ from_: overrides.from ?? "alice@a.com",
1568
+ to: [overrides.inboxId ?? "bot@x.dev"],
1569
+ cc: [],
1570
+ bcc: [],
1571
+ reply_to: [],
1572
+ subject: "Test",
1573
+ preview: "Test email",
1574
+ text: "Hello",
1575
+ html: null,
1576
+ labels: [],
1577
+ attachments: [],
1578
+ in_reply_to: null,
1579
+ references: [],
1580
+ timestamp: new Date().toISOString(),
1581
+ created_at: new Date().toISOString(),
1582
+ updated_at: new Date().toISOString(),
1583
+ },
1584
+ };
1585
+ }
1586
+
1587
+ beforeAll(async () => {
1588
+ try {
1589
+ await unlink(AGENTMAIL_DB);
1590
+ } catch {}
1591
+ try {
1592
+ await unlink(`${AGENTMAIL_DB}-wal`);
1593
+ } catch {}
1594
+ try {
1595
+ await unlink(`${AGENTMAIL_DB}-shm`);
1596
+ } catch {}
1597
+
1598
+ agentmailProc = Bun.spawn(["bun", "src/http.ts"], {
1599
+ cwd: `${import.meta.dir}/../..`,
1600
+ env: {
1601
+ ...process.env,
1602
+ PORT: String(AGENTMAIL_PORT),
1603
+ DATABASE_PATH: AGENTMAIL_DB,
1604
+ API_KEY: "",
1605
+ CAPABILITIES: "core,task-pool,messaging,profiles",
1606
+ SLACK_BOT_TOKEN: "",
1607
+ GITHUB_WEBHOOK_SECRET: "",
1608
+ AGENTMAIL_WEBHOOK_SECRET: WEBHOOK_SECRET,
1609
+ AGENTMAIL_INBOX_DOMAIN_FILTER: "x.dev,y.xyz",
1610
+ AGENTMAIL_SENDER_DOMAIN_FILTER: "a.com,b.com",
1611
+ },
1612
+ stdout: "ignore",
1613
+ stderr: "ignore",
1614
+ });
1615
+
1616
+ await waitForServer(`${AGENTMAIL_BASE}/health`);
1617
+
1618
+ // Register a lead agent so messages can be routed
1619
+ await fetch(`${AGENTMAIL_BASE}/api/agents`, {
1620
+ method: "POST",
1621
+ headers: { "Content-Type": "application/json", "x-agent-id": randomUUID() },
1622
+ body: JSON.stringify({ name: "TestLead", isLead: true }),
1623
+ });
1624
+ }, 20000);
1625
+
1626
+ afterAll(async () => {
1627
+ if (agentmailProc) {
1628
+ agentmailProc.kill();
1629
+ try {
1630
+ await agentmailProc.exited;
1631
+ } catch {}
1632
+ }
1633
+ await Bun.sleep(300);
1634
+ try {
1635
+ await unlink(AGENTMAIL_DB);
1636
+ } catch {}
1637
+ try {
1638
+ await unlink(`${AGENTMAIL_DB}-wal`);
1639
+ } catch {}
1640
+ try {
1641
+ await unlink(`${AGENTMAIL_DB}-shm`);
1642
+ } catch {}
1643
+ });
1644
+
1645
+ test("rejects unsigned webhook with 401", async () => {
1646
+ const res = await fetch(`${AGENTMAIL_BASE}/api/agentmail/webhook`, {
1647
+ method: "POST",
1648
+ headers: { "Content-Type": "application/json" },
1649
+ body: JSON.stringify(makePayload()),
1650
+ });
1651
+ expect(res.status).toBe(401);
1652
+ });
1653
+
1654
+ test("accepts signed webhook with allowed inbox + sender", async () => {
1655
+ const { status, body } = await postWebhook(
1656
+ makePayload({ inboxId: "bot@x.dev", from: "alice@a.com" }),
1657
+ );
1658
+ expect(status).toBe(200);
1659
+ expect(body).toEqual({ received: true });
1660
+ });
1661
+
1662
+ test("accepts second allowed inbox domain", async () => {
1663
+ const { status } = await postWebhook(
1664
+ makePayload({ inboxId: "support@y.xyz", from: "bob@b.com" }),
1665
+ );
1666
+ expect(status).toBe(200);
1667
+ });
1668
+
1669
+ test("filters out disallowed inbox domain (returns 200 but no processing)", async () => {
1670
+ // The server returns 200 before filtering (Svix best practice),
1671
+ // so we verify by checking that no task was created for a disallowed inbox.
1672
+ const { status } = await postWebhook(
1673
+ makePayload({ inboxId: "bot@evil.com", from: "alice@a.com" }),
1674
+ );
1675
+ expect(status).toBe(200);
1676
+ });
1677
+
1678
+ test("filters out disallowed sender domain (returns 200 but no processing)", async () => {
1679
+ const { status } = await postWebhook(
1680
+ makePayload({ inboxId: "bot@x.dev", from: "hacker@evil.org" }),
1681
+ );
1682
+ expect(status).toBe(200);
1683
+ });
1684
+
1685
+ test("filters out when sender is array of disallowed domains", async () => {
1686
+ const { status } = await postWebhook(
1687
+ makePayload({ inboxId: "bot@x.dev", from: ["hacker@evil.org", "spam@bad.net"] }),
1688
+ );
1689
+ expect(status).toBe(200);
1690
+ });
1691
+
1692
+ test("allows when at least one sender in array matches", async () => {
1693
+ const { status } = await postWebhook(
1694
+ makePayload({ inboxId: "bot@x.dev", from: ["hacker@evil.org", "alice@a.com"] }),
1695
+ );
1696
+ expect(status).toBe(200);
1697
+ });
1698
+ });