@cursor/july 0.1.26 → 0.1.28

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 (324) hide show
  1. package/dist/ab.d.ts +6 -0
  2. package/dist/ab.d.ts.map +1 -1
  3. package/dist/ab.js +1 -0
  4. package/dist/artifacts.d.ts +34 -0
  5. package/dist/artifacts.d.ts.map +1 -0
  6. package/dist/artifacts.js +57 -0
  7. package/dist/bin/agent-serve.js +43 -12
  8. package/dist/channels/github/github-channel.d.ts +1 -1
  9. package/dist/channels/github/github-channel.d.ts.map +1 -1
  10. package/dist/channels/github/github-channel.js +14 -1
  11. package/dist/channels/github/types.d.ts +6 -1
  12. package/dist/channels/github/types.d.ts.map +1 -1
  13. package/dist/channels/slack/api.d.ts +2 -1
  14. package/dist/channels/slack/api.d.ts.map +1 -1
  15. package/dist/channels/slack/api.js +1 -0
  16. package/dist/channels/slack/auth.d.ts +13 -13
  17. package/dist/channels/slack/auth.d.ts.map +1 -1
  18. package/dist/channels/slack/auth.js +19 -10
  19. package/dist/channels/slack/cursor-account.d.ts +2 -1
  20. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  21. package/dist/channels/slack/cursor-account.js +3 -1
  22. package/dist/channels/slack/defaults.d.ts +7 -4
  23. package/dist/channels/slack/defaults.d.ts.map +1 -1
  24. package/dist/channels/slack/defaults.js +14 -4
  25. package/dist/channels/slack/dispatch.d.ts +10 -1
  26. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  27. package/dist/channels/slack/dispatch.js +59 -21
  28. package/dist/channels/slack/external-policy.d.ts +27 -0
  29. package/dist/channels/slack/external-policy.d.ts.map +1 -0
  30. package/dist/channels/slack/external-policy.js +53 -0
  31. package/dist/channels/slack/index.d.ts +1 -0
  32. package/dist/channels/slack/index.d.ts.map +1 -1
  33. package/dist/channels/slack/index.js +1 -0
  34. package/dist/channels/slack/interactive.d.ts +8 -1
  35. package/dist/channels/slack/interactive.d.ts.map +1 -1
  36. package/dist/channels/slack/interactive.js +43 -15
  37. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  38. package/dist/channels/slack/slack-channel.js +22 -8
  39. package/dist/channels/slack/types.d.ts +16 -1
  40. package/dist/channels/slack/types.d.ts.map +1 -1
  41. package/dist/docs/404.html +2 -2
  42. package/dist/docs/ab.html +3 -3
  43. package/dist/docs/assets/{app.DYcC9FY-.js → app.CqjrYaT2.js} +1 -1
  44. package/dist/docs/assets/chunks/@localSearchIndexroot.VWQoPFJ1.js +1 -0
  45. package/dist/docs/assets/chunks/{VPLocalSearchBox.o4N_knTV.js → VPLocalSearchBox.ag-MwUel.js} +1 -1
  46. package/dist/docs/assets/chunks/{theme.DQ-njyo0.js → theme.BS-DJWat.js} +2 -2
  47. package/dist/docs/assets/{guides_slack.md.D_xFKlnF.js → guides_slack.md.CjmJSvZS.js} +8 -4
  48. package/dist/docs/assets/guides_slack.md.CjmJSvZS.lean.js +1 -0
  49. package/dist/docs/assets/guides_webhooks.md.wSOYas3X.js +183 -0
  50. package/dist/docs/assets/{guides_webhooks.md.B1EswtUu.lean.js → guides_webhooks.md.wSOYas3X.lean.js} +1 -1
  51. package/dist/docs/assets/{index.md.Dfv5ic9t.js → index.md.CH_s5uZe.js} +1 -1
  52. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.js → reference_channels.md.CDhTRfUz.js} +2 -2
  53. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.lean.js → reference_channels.md.CDhTRfUz.lean.js} +1 -1
  54. package/dist/docs/building-with-agents.html +3 -3
  55. package/dist/docs/concepts.html +3 -3
  56. package/dist/docs/deployment.html +3 -3
  57. package/dist/docs/evals.html +3 -3
  58. package/dist/docs/example-agents/approval-buddy.html +3 -3
  59. package/dist/docs/example-agents/benny.html +3 -3
  60. package/dist/docs/example-agents/bugbot.html +3 -3
  61. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  62. package/dist/docs/example-agents/codeowners-review.html +3 -3
  63. package/dist/docs/example-agents/concierge.html +3 -3
  64. package/dist/docs/example-agents/fsd.html +3 -3
  65. package/dist/docs/example-agents/index.html +3 -3
  66. package/dist/docs/example-agents/knowledge-base.html +3 -3
  67. package/dist/docs/example-agents/oncall.html +3 -3
  68. package/dist/docs/example-agents/security-reviewer.html +3 -3
  69. package/dist/docs/example-agents/slack-agent.html +3 -3
  70. package/dist/docs/example-agents/weather-agent.html +3 -3
  71. package/dist/docs/guides/agent-to-agent.html +3 -3
  72. package/dist/docs/guides/cloud-runtime.html +3 -3
  73. package/dist/docs/guides/github.html +3 -3
  74. package/dist/docs/guides/human-in-the-loop.html +3 -3
  75. package/dist/docs/guides/mcp-oauth.html +3 -3
  76. package/dist/docs/guides/slack.html +11 -7
  77. package/dist/docs/guides/webhooks.html +138 -5
  78. package/dist/docs/hashmap.json +1 -1
  79. package/dist/docs/hillclimbing.html +3 -3
  80. package/dist/docs/index.html +5 -5
  81. package/dist/docs/quickstart.html +3 -3
  82. package/dist/docs/reference/agent-config.html +3 -3
  83. package/dist/docs/reference/channels.html +5 -5
  84. package/dist/docs/reference/cli.html +3 -3
  85. package/dist/docs/reference/connections.html +3 -3
  86. package/dist/docs/reference/hooks.html +3 -3
  87. package/dist/docs/reference/http-api.html +3 -3
  88. package/dist/docs/reference/instructions.html +3 -3
  89. package/dist/docs/reference/playground.html +3 -3
  90. package/dist/docs/reference/project-layout.html +3 -3
  91. package/dist/docs/reference/prompt.html +3 -3
  92. package/dist/docs/reference/schedules.html +3 -3
  93. package/dist/docs/reference/sessions.html +3 -3
  94. package/dist/docs/reference/skills.html +3 -3
  95. package/dist/docs/reference/subagents.html +3 -3
  96. package/dist/docs/reference/tools.html +3 -3
  97. package/dist/docs/scaffolding-agents.html +3 -3
  98. package/dist/docs/storage.html +3 -3
  99. package/dist/docs/troubleshooting.html +3 -3
  100. package/dist/evals/assertions.d.ts +10 -1
  101. package/dist/evals/assertions.d.ts.map +1 -1
  102. package/dist/evals/assertions.js +12 -0
  103. package/dist/evals.d.ts +1 -1
  104. package/dist/evals.d.ts.map +1 -1
  105. package/dist/index.d.ts +1 -0
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +1 -0
  108. package/dist/internal/ab-fold.d.ts.map +1 -1
  109. package/dist/internal/ab-fold.js +4 -0
  110. package/dist/internal/ab-snapshot.d.ts.map +1 -1
  111. package/dist/internal/ab-snapshot.js +1 -0
  112. package/dist/internal/artifacts-store.d.ts +45 -0
  113. package/dist/internal/artifacts-store.d.ts.map +1 -0
  114. package/dist/internal/artifacts-store.js +250 -0
  115. package/dist/internal/builtin-tools/artifacts.d.ts +10 -0
  116. package/dist/internal/builtin-tools/artifacts.d.ts.map +1 -0
  117. package/dist/internal/builtin-tools/artifacts.js +73 -0
  118. package/dist/internal/builtin-tools/index.d.ts +8 -1
  119. package/dist/internal/builtin-tools/index.d.ts.map +1 -1
  120. package/dist/internal/builtin-tools/index.js +15 -0
  121. package/dist/internal/cli-ax.d.ts +19 -0
  122. package/dist/internal/cli-ax.d.ts.map +1 -1
  123. package/dist/internal/cli-ax.js +196 -57
  124. package/dist/internal/cli-slack.d.ts.map +1 -1
  125. package/dist/internal/cli-slack.js +14 -6
  126. package/dist/internal/cursor-slack-relay.d.ts +2 -0
  127. package/dist/internal/cursor-slack-relay.d.ts.map +1 -1
  128. package/dist/internal/cursor-slack-relay.js +1 -0
  129. package/dist/internal/deploy-client.d.ts +2 -0
  130. package/dist/internal/deploy-client.d.ts.map +1 -1
  131. package/dist/internal/deploy-client.js +1 -0
  132. package/dist/internal/discovery.d.ts.map +1 -1
  133. package/dist/internal/discovery.js +91 -3
  134. package/dist/internal/distribution.d.ts.map +1 -1
  135. package/dist/internal/distribution.js +1 -0
  136. package/dist/internal/model-pricing.d.ts +49 -0
  137. package/dist/internal/model-pricing.d.ts.map +1 -0
  138. package/dist/internal/model-pricing.js +377 -0
  139. package/dist/internal/reminder-runner.d.ts.map +1 -1
  140. package/dist/internal/reminder-runner.js +1 -0
  141. package/dist/internal/schedule-runner.d.ts.map +1 -1
  142. package/dist/internal/schedule-runner.js +1 -0
  143. package/dist/internal/sdk-runner.d.ts.map +1 -1
  144. package/dist/internal/sdk-runner.js +9 -1
  145. package/dist/internal/server.d.ts.map +1 -1
  146. package/dist/internal/server.js +49 -5
  147. package/dist/internal/session-cost.d.ts +49 -0
  148. package/dist/internal/session-cost.d.ts.map +1 -0
  149. package/dist/internal/session-cost.js +175 -0
  150. package/dist/internal/session-engine.d.ts +35 -1
  151. package/dist/internal/session-engine.d.ts.map +1 -1
  152. package/dist/internal/session-engine.js +128 -7
  153. package/dist/internal/storage-coordinator.d.ts +15 -0
  154. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  155. package/dist/internal/storage-coordinator.js +52 -0
  156. package/dist/internal/trajectory.d.ts +9 -1
  157. package/dist/internal/trajectory.d.ts.map +1 -1
  158. package/dist/internal/trajectory.js +32 -5
  159. package/dist/playground/assets/index-Bul1WfrB.css +1 -0
  160. package/dist/playground/assets/index-CkN74U_L.js +87 -0
  161. package/dist/playground/index.html +2 -2
  162. package/dist/storage.d.ts +4 -0
  163. package/dist/storage.d.ts.map +1 -1
  164. package/dist/storage.js +4 -0
  165. package/dist/types.d.ts +164 -1
  166. package/dist/types.d.ts.map +1 -1
  167. package/docs/README.md +1 -0
  168. package/docs/guides/slack.md +32 -7
  169. package/docs/guides/webhooks.md +183 -0
  170. package/docs/reference/channels.md +2 -1
  171. package/package.json +26 -26
  172. package/skills/setup-slack/SKILL.md +3 -0
  173. package/src/ab.ts +7 -0
  174. package/src/bin/agent-serve.ts +42 -5
  175. package/src/channels/slack/auth.ts +29 -22
  176. package/src/channels/slack/cursor-account.ts +2 -0
  177. package/src/channels/slack/defaults.ts +24 -4
  178. package/src/channels/slack/dispatch.ts +42 -1
  179. package/src/channels/slack/external-policy.ts +62 -0
  180. package/src/channels/slack/index.ts +1 -0
  181. package/src/channels/slack/interactive.ts +42 -10
  182. package/src/channels/slack/slack-channel.ts +20 -3
  183. package/src/channels/slack/types.ts +12 -0
  184. package/src/internal/ab-fold.ts +4 -0
  185. package/src/internal/ab-snapshot.ts +1 -0
  186. package/src/internal/cli-ax.ts +205 -34
  187. package/src/internal/cursor-slack-relay.ts +3 -0
  188. package/src/internal/deploy-client.ts +3 -0
  189. package/src/internal/discovery.ts +8 -1
  190. package/src/internal/model-pricing.ts +426 -0
  191. package/src/internal/sdk-runner.ts +10 -2
  192. package/src/internal/server.ts +9 -5
  193. package/src/internal/session-cost.ts +231 -0
  194. package/src/internal/session-engine.ts +28 -0
  195. package/src/internal/trajectory.ts +45 -3
  196. package/src/types.ts +24 -1
  197. package/dist/channels/github/instrument.d.ts +0 -20
  198. package/dist/channels/github/instrument.d.ts.map +0 -1
  199. package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +0 -1
  200. package/dist/docs/assets/guides_slack.md.D_xFKlnF.lean.js +0 -1
  201. package/dist/docs/assets/guides_webhooks.md.B1EswtUu.js +0 -50
  202. package/dist/internal/json-dir-store.d.ts +0 -32
  203. package/dist/internal/json-dir-store.d.ts.map +0 -1
  204. package/dist/internal/json-dir-store.js +0 -100
  205. package/dist/playground/assets/index-CjOQ4hN9.css +0 -1
  206. package/dist/playground/assets/index-DqXdAFGa.js +0 -85
  207. package/src/bin/agent-serve.version.test.ts +0 -64
  208. package/src/channels/github/api.test.ts +0 -64
  209. package/src/channels/github/auth.test.ts +0 -105
  210. package/src/channels/github/cursor-account.test.ts +0 -204
  211. package/src/channels/github/forward.test.ts +0 -457
  212. package/src/channels/github/github.test.ts +0 -985
  213. package/src/channels/github/replay.test.ts +0 -179
  214. package/src/channels/slack/api.post-message.test.ts +0 -148
  215. package/src/channels/slack/approvals.test.ts +0 -328
  216. package/src/channels/slack/block-actions.test.ts +0 -469
  217. package/src/channels/slack/bot-mentions.test.ts +0 -267
  218. package/src/channels/slack/channel-watch.test.ts +0 -363
  219. package/src/channels/slack/cursor-account.test.ts +0 -301
  220. package/src/channels/slack/defaults.final-post.test.ts +0 -182
  221. package/src/channels/slack/dispatch.test.ts +0 -875
  222. package/src/channels/slack/eval-directive.test.ts +0 -273
  223. package/src/channels/slack/message-body.test.ts +0 -54
  224. package/src/channels/slack/nudge-store.test.ts +0 -143
  225. package/src/channels/slack/slack.test.ts +0 -391
  226. package/src/channels/slack/stop.test.ts +0 -23
  227. package/src/channels/slack/thread-context.test.ts +0 -202
  228. package/src/evals/assertions.test.ts +0 -617
  229. package/src/evals/expect.test.ts +0 -144
  230. package/src/evals/judge.test.ts +0 -181
  231. package/src/evals/loaders.test.ts +0 -132
  232. package/src/evals/matchers.test.ts +0 -95
  233. package/src/evals/reporters.test.ts +0 -303
  234. package/src/evals/run-facts.test.ts +0 -259
  235. package/src/internal/ab-snapshot.test.ts +0 -325
  236. package/src/internal/approval-gate.test.ts +0 -49
  237. package/src/internal/approvals.integration.test.ts +0 -383
  238. package/src/internal/artifacts-store.test.ts +0 -283
  239. package/src/internal/authored-loaders.test.ts +0 -31
  240. package/src/internal/builtin-tools/artifacts.test.ts +0 -247
  241. package/src/internal/builtin-tools/reminders.test.ts +0 -201
  242. package/src/internal/channel-route-schema.test.ts +0 -294
  243. package/src/internal/chat-attach.test.ts +0 -262
  244. package/src/internal/cli-deploy.test.ts +0 -1991
  245. package/src/internal/cli-docs.test.ts +0 -161
  246. package/src/internal/cli-mcp.test.ts +0 -789
  247. package/src/internal/cli-skills.test.ts +0 -133
  248. package/src/internal/cli-slack.test.ts +0 -1654
  249. package/src/internal/cloud-merge.test.ts +0 -74
  250. package/src/internal/cron.test.ts +0 -22
  251. package/src/internal/cursor/account-mcp.test.ts +0 -807
  252. package/src/internal/cursor/backend-client.test.ts +0 -591
  253. package/src/internal/cursor/credentials.test.ts +0 -351
  254. package/src/internal/cursor/github-credentials.test.ts +0 -136
  255. package/src/internal/cursor-account-mcp-auth.test.ts +0 -310
  256. package/src/internal/cursor-account.integration.test.ts +0 -441
  257. package/src/internal/cursor-event-relay.test.ts +0 -746
  258. package/src/internal/cursor-github-credentials.integration.test.ts +0 -271
  259. package/src/internal/cursor-slack-relay.test.ts +0 -525
  260. package/src/internal/deploy-source.test.ts +0 -111
  261. package/src/internal/discovery.artifact-tool.test.ts +0 -136
  262. package/src/internal/discovery.artifacts.test.ts +0 -119
  263. package/src/internal/discovery.builtin-tools.test.ts +0 -94
  264. package/src/internal/discovery.concurrency.test.ts +0 -60
  265. package/src/internal/discovery.cursor-account.test.ts +0 -133
  266. package/src/internal/discovery.cwd.test.ts +0 -83
  267. package/src/internal/discovery.hosting.test.ts +0 -80
  268. package/src/internal/discovery.identity.test.ts +0 -44
  269. package/src/internal/docs-site.test.ts +0 -66
  270. package/src/internal/duration.test.ts +0 -29
  271. package/src/internal/eval-judge-model.test.ts +0 -187
  272. package/src/internal/eval-run-store.cancel.test.ts +0 -142
  273. package/src/internal/eval-run-store.storage.test.ts +0 -211
  274. package/src/internal/eval-runner.http.test.ts +0 -403
  275. package/src/internal/eval-runner.run.test.ts +0 -928
  276. package/src/internal/evals-client.test.ts +0 -307
  277. package/src/internal/event-mapper.test.ts +0 -243
  278. package/src/internal/github-fanout.test.ts +0 -213
  279. package/src/internal/handleAgentServeTrigger.test.ts +0 -185
  280. package/src/internal/host-kv.test.ts +0 -82
  281. package/src/internal/host-platforms.test.ts +0 -126
  282. package/src/internal/http-channel.test.ts +0 -402
  283. package/src/internal/init-project.test.ts +0 -270
  284. package/src/internal/install-cursor-skills.test.ts +0 -262
  285. package/src/internal/local-env.test.ts +0 -120
  286. package/src/internal/log-ring.test.ts +0 -31
  287. package/src/internal/logs-client.test.ts +0 -350
  288. package/src/internal/mcp-endpoint.test.ts +0 -436
  289. package/src/internal/mcp-host.test.ts +0 -298
  290. package/src/internal/mcp-oauth.test.ts +0 -148
  291. package/src/internal/net.test.ts +0 -17
  292. package/src/internal/peer-connections.test.ts +0 -128
  293. package/src/internal/peer-mcp.integration.test.ts +0 -289
  294. package/src/internal/playground/toolchain.test.ts +0 -53
  295. package/src/internal/playground-cli.test.ts +0 -187
  296. package/src/internal/playground-proxy.test.ts +0 -376
  297. package/src/internal/prompt-context.integration.test.ts +0 -232
  298. package/src/internal/prompt-context.test.ts +0 -127
  299. package/src/internal/reminder-runner.test.ts +0 -432
  300. package/src/internal/reminder-store.test.ts +0 -53
  301. package/src/internal/request-headers.test.ts +0 -27
  302. package/src/internal/resolve-prod-target.test.ts +0 -787
  303. package/src/internal/resolved-connections.test.ts +0 -295
  304. package/src/internal/router.test.ts +0 -57
  305. package/src/internal/sdk-runner.test.ts +0 -290
  306. package/src/internal/server.artifacts.test.ts +0 -334
  307. package/src/internal/session-engine.artifacts.test.ts +0 -293
  308. package/src/internal/session-engine.coalesce.test.ts +0 -169
  309. package/src/internal/session-engine.concurrency.test.ts +0 -250
  310. package/src/internal/session-engine.host-oauth-mcp.test.ts +0 -110
  311. package/src/internal/session-engine.interrupt.test.ts +0 -577
  312. package/src/internal/session-engine.storage.test.ts +0 -547
  313. package/src/internal/session-urls.test.ts +0 -28
  314. package/src/internal/sessions-client.test.ts +0 -518
  315. package/src/internal/storage-coordinator.test.ts +0 -517
  316. package/src/internal/tool-call.test.ts +0 -458
  317. package/src/internal/tool-result.test.ts +0 -52
  318. package/src/internal/trajectory.approvals.test.ts +0 -83
  319. package/src/internal/trajectory.subagents.test.ts +0 -198
  320. package/src/internal/turn-governor.test.ts +0 -137
  321. package/src/internal/update-check.test.ts +0 -485
  322. package/src/internal/workspace.test.ts +0 -207
  323. package/src/storage-backends/cursor-hosted.test.ts +0 -121
  324. /package/dist/docs/assets/{index.md.Dfv5ic9t.lean.js → index.md.CH_s5uZe.lean.js} +0 -0
@@ -1,985 +0,0 @@
1
- import { createHmac } from "node:crypto";
2
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
- import { scmMetadataToGitHubPayload } from "../../internal/cursor-event-relay.js";
4
- import type {
5
- ArtifactsApi,
6
- ChannelHandlerArgs,
7
- ChannelRouteHandler,
8
- ChannelSession,
9
- SendMessageFn,
10
- } from "../../types.js";
11
- import { formatGitHubOutboundComment } from "./binding.js";
12
- import {
13
- clearGitHubAuthCaches,
14
- defaultGitHubAuth,
15
- defaultGitHubPullRequestMessage,
16
- defaultOnComment,
17
- type GitHubEventContext,
18
- type GitHubRawEvent,
19
- githubChannel,
20
- githubPullRequestContinuationKey,
21
- isGitHubAppBotAuthor,
22
- parseGitHubPullRequestEvent,
23
- shouldDispatchGitHubComment,
24
- verifyGitHubWebhookSignature,
25
- } from "./index.js";
26
-
27
- describe("parseGitHubPullRequestEvent", () => {
28
- it("normalizes a GitHub pull_request payload", () => {
29
- const parsed = parseGitHubPullRequestEvent({
30
- deliveryId: "delivery-1",
31
- payload: pullRequestPayload(),
32
- });
33
-
34
- expect(parsed).toEqual({
35
- ctx: {
36
- deliveryId: "delivery-1",
37
- eventName: "pull_request",
38
- repository: {
39
- fullName: "acme/widgets",
40
- url: "https://github.com/acme/widgets",
41
- owner: "acme",
42
- name: "widgets",
43
- },
44
- conversation: {
45
- kind: "pull_request",
46
- number: 42,
47
- url: "https://github.com/acme/widgets/pull/42",
48
- },
49
- senderLogin: "sender",
50
- sender: { login: "sender" },
51
- },
52
- pullRequest: {
53
- action: "opened",
54
- number: 42,
55
- url: "https://github.com/acme/widgets/pull/42",
56
- title: "Add widgets",
57
- body: "PR body",
58
- headRef: "feature/widgets",
59
- headSha: "abc123",
60
- baseRef: "main",
61
- draft: false,
62
- merged: false,
63
- authorLogin: "octocat",
64
- },
65
- });
66
- });
67
-
68
- it("rejects malformed payloads", () => {
69
- expect(
70
- parseGitHubPullRequestEvent({
71
- payload: { action: "opened" },
72
- })
73
- ).toBeNull();
74
- });
75
- });
76
-
77
- describe("GitHub pull request prompt helpers", () => {
78
- it("builds stable continuation keys and context-rich messages", () => {
79
- const parsed = parseGitHubPullRequestEvent({
80
- payload: pullRequestPayload(),
81
- });
82
- expect(parsed).not.toBeNull();
83
- if (parsed === null) {
84
- return;
85
- }
86
-
87
- expect(githubPullRequestContinuationKey(parsed.ctx)).toBe(
88
- "github-pr:acme/widgets#42"
89
- );
90
- const message = defaultGitHubPullRequestMessage(
91
- parsed.ctx,
92
- parsed.pullRequest
93
- );
94
- expect(message).toContain("GitHub pull request trigger: acme/widgets#42");
95
- expect(message).toContain("Head: feature/widgets (abc123)");
96
- });
97
-
98
- it("derives the turn auth from the delivery actor", () => {
99
- const parsed = parseGitHubPullRequestEvent({
100
- payload: pullRequestPayload(),
101
- });
102
- expect(parsed).not.toBeNull();
103
- if (parsed === null) {
104
- return;
105
- }
106
-
107
- expect(defaultGitHubAuth(parsed.ctx)).toEqual({
108
- authenticator: "github-webhook",
109
- principalId: "sender",
110
- principalType: "user",
111
- attributes: expect.objectContaining({ repository: "acme/widgets" }),
112
- });
113
- });
114
- });
115
-
116
- describe("defaultOnComment / mention gate", () => {
117
- it("dispatches only when the bot is @mentioned", () => {
118
- expect(
119
- shouldDispatchGitHubComment({
120
- body: "hey @my-agent please look",
121
- botName: "my-agent",
122
- })
123
- ).toBe(true);
124
- expect(
125
- shouldDispatchGitHubComment({
126
- body: "no mention here",
127
- botName: "my-agent",
128
- })
129
- ).toBe(false);
130
- expect(
131
- shouldDispatchGitHubComment({
132
- body: "@my-agent hi",
133
- botName: undefined,
134
- })
135
- ).toBe(false);
136
- });
137
-
138
- it("ignores comments from the GitHub App bot actor", () => {
139
- expect(
140
- shouldDispatchGitHubComment({
141
- body: "hey @my-agent please look",
142
- author: { login: "my-agent[bot]", type: "Bot" },
143
- botName: "my-agent",
144
- })
145
- ).toBe(false);
146
- expect(
147
- isGitHubAppBotAuthor({ login: "my-agent[bot]", type: "Bot" }, "my-agent")
148
- ).toBe(true);
149
- });
150
-
151
- it("returns auth when mentioned", () => {
152
- const ctx = {
153
- eventName: "issue_comment",
154
- repository: {
155
- fullName: "acme/widgets",
156
- url: "https://github.com/acme/widgets",
157
- owner: "acme",
158
- name: "widgets",
159
- },
160
- conversation: {
161
- kind: "pull_request" as const,
162
- number: 42,
163
- url: "https://github.com/acme/widgets/pull/42",
164
- },
165
- senderLogin: "sender",
166
- };
167
- expect(
168
- defaultOnComment(
169
- ctx,
170
- {
171
- id: 1,
172
- body: "@my-agent triage this",
173
- url: "https://github.com/acme/widgets/issues/42#issuecomment-1",
174
- htmlUrl: "https://github.com/acme/widgets/issues/42#issuecomment-1",
175
- },
176
- { botName: "my-agent" }
177
- )
178
- ).toEqual({ auth: defaultGitHubAuth(ctx) });
179
- });
180
- });
181
-
182
- describe("verifyGitHubWebhookSignature", () => {
183
- it("accepts a valid sha256 signature", async () => {
184
- const body = JSON.stringify(pullRequestPayload());
185
- const signature = `sha256=${createHmac("sha256", "secret")
186
- .update(body)
187
- .digest("hex")}`;
188
-
189
- await expect(
190
- verifyGitHubWebhookSignature({
191
- body,
192
- signatureHeader: signature,
193
- secret: "secret",
194
- })
195
- ).resolves.toBe("verified");
196
- });
197
-
198
- it("rejects mismatched signatures", async () => {
199
- await expect(
200
- verifyGitHubWebhookSignature({
201
- body: "{}",
202
- signatureHeader: "sha256=bad",
203
- secret: "secret",
204
- })
205
- ).resolves.toBe("mismatch");
206
- });
207
- });
208
-
209
- describe("githubChannel", () => {
210
- let priorWebhookSecret: string | undefined;
211
-
212
- beforeEach(() => {
213
- priorWebhookSecret = process.env.GITHUB_WEBHOOK_SECRET;
214
- delete process.env.GITHUB_WEBHOOK_SECRET;
215
- });
216
-
217
- afterEach(() => {
218
- if (priorWebhookSecret === undefined) {
219
- delete process.env.GITHUB_WEBHOOK_SECRET;
220
- } else {
221
- process.env.GITHUB_WEBHOOK_SECRET = priorWebhookSecret;
222
- }
223
- });
224
-
225
- it("dispatches default review triggers as the delivery actor", async () => {
226
- const body = JSON.stringify(pullRequestPayload());
227
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
228
- const response = await routeHandler(
229
- githubChannel({
230
- auth: [],
231
- cloud: true,
232
- })
233
- )(
234
- new Request("http://127.0.0.1:3000/v1/channels/github", {
235
- method: "POST",
236
- headers: {
237
- "x-github-event": "pull_request",
238
- "x-github-delivery": "delivery-1",
239
- "content-type": "application/json",
240
- },
241
- body,
242
- }),
243
- handlerArgs(send)
244
- );
245
-
246
- await expect(response.json()).resolves.toEqual({
247
- ok: true,
248
- sessionId: "session-1",
249
- isNew: true,
250
- continuationToken: "github-pr:acme/widgets#42",
251
- });
252
- expect(send).toHaveBeenCalledExactlyOnceWith(
253
- expect.stringContaining("GitHub pull request trigger: acme/widgets#42"),
254
- expect.objectContaining({
255
- auth: expect.objectContaining({
256
- authenticator: "github-webhook",
257
- principalId: "sender",
258
- principalType: "user",
259
- }),
260
- continuationToken: "github-pr:acme/widgets#42",
261
- title: "GitHub acme/widgets#42",
262
- cloud: {
263
- repos: [
264
- {
265
- url: "https://github.com/acme/widgets",
266
- prUrl: "https://github.com/acme/widgets/pull/42",
267
- startingRef: "abc123",
268
- },
269
- ],
270
- },
271
- })
272
- );
273
- });
274
-
275
- it("ignores actions outside the default gate without starting a session", async () => {
276
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
277
- const response = await routeHandler(githubChannel({ auth: [] }))(
278
- new Request("http://127.0.0.1:3000/v1/channels/github", {
279
- method: "POST",
280
- headers: { "x-github-event": "pull_request" },
281
- body: JSON.stringify(pullRequestPayload({ action: "closed" })),
282
- }),
283
- handlerArgs(send)
284
- );
285
-
286
- await expect(response.json()).resolves.toEqual({
287
- ok: true,
288
- skipped: true,
289
- reason: "not_dispatched",
290
- action: "closed",
291
- });
292
- expect(send).not.toHaveBeenCalled();
293
- });
294
-
295
- it("replaces the default gate with the onPullRequest hook", async () => {
296
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
297
- const handler = routeHandler(
298
- githubChannel({
299
- auth: [],
300
- onPullRequest: (ctx, pr) =>
301
- pr.action === "synchronize"
302
- ? { auth: defaultGitHubAuth(ctx), context: ["Focus on the diff."] }
303
- : null,
304
- })
305
- );
306
-
307
- const ignored = await handler(
308
- new Request("http://127.0.0.1:3000/v1/channels/github", {
309
- method: "POST",
310
- headers: { "x-github-event": "pull_request" },
311
- body: JSON.stringify(pullRequestPayload({ action: "opened" })),
312
- }),
313
- handlerArgs(send)
314
- );
315
- await expect(ignored.json()).resolves.toEqual({
316
- ok: true,
317
- skipped: true,
318
- reason: "not_dispatched",
319
- action: "opened",
320
- });
321
- expect(send).not.toHaveBeenCalled();
322
-
323
- const dispatched = await handler(
324
- new Request("http://127.0.0.1:3000/v1/channels/github", {
325
- method: "POST",
326
- headers: { "x-github-event": "pull_request" },
327
- body: JSON.stringify(pullRequestPayload({ action: "synchronize" })),
328
- }),
329
- handlerArgs(send)
330
- );
331
- await expect(dispatched.json()).resolves.toMatchObject({ ok: true });
332
- expect(send).toHaveBeenCalledExactlyOnceWith(
333
- expect.stringContaining("Focus on the diff."),
334
- expect.anything()
335
- );
336
- });
337
-
338
- it("hands the artifacts facade to inbound hooks (ctx.artifacts)", async () => {
339
- const tags: Array<Parameters<ArtifactsApi["tag"]>[0]> = [];
340
- const artifacts: ArtifactsApi = {
341
- tag: async (input) => {
342
- tags.push(input);
343
- return {
344
- id: `art_${tags.length}`,
345
- kind: input.kind ?? "artifact",
346
- key: input.key,
347
- data: input.data,
348
- source: "host",
349
- createdAt: new Date().toISOString(),
350
- updatedAt: new Date().toISOString(),
351
- };
352
- },
353
- list: async () => [],
354
- };
355
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
356
- const response = await routeHandler(
357
- githubChannel({
358
- auth: [],
359
- onPullRequest: async (ctx, pr) => {
360
- await ctx.artifacts.tag({
361
- key: `pr:${pr.number}`,
362
- data: { headSha: pr.headSha },
363
- });
364
- return null;
365
- },
366
- })
367
- )(
368
- new Request("http://127.0.0.1:3000/v1/channels/github", {
369
- method: "POST",
370
- headers: { "x-github-event": "pull_request" },
371
- body: JSON.stringify(pullRequestPayload()),
372
- }),
373
- { ...handlerArgs(send), artifacts }
374
- );
375
- await expect(response.json()).resolves.toMatchObject({ ok: true });
376
- expect(tags).toEqual([{ key: "pr:42", data: { headSha: "abc123" } }]);
377
- });
378
-
379
- it("dispatches @mention issue comments via the default onComment gate", async () => {
380
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
381
- const response = await routeHandler(
382
- githubChannel({
383
- auth: [],
384
- botName: "my-agent",
385
- })
386
- )(
387
- new Request("http://127.0.0.1:3000/v1/channels/github", {
388
- method: "POST",
389
- headers: {
390
- "x-github-event": "issue_comment",
391
- "x-github-delivery": "delivery-comment",
392
- },
393
- body: JSON.stringify(issueCommentPayload("@my-agent please triage")),
394
- }),
395
- handlerArgs(send)
396
- );
397
-
398
- await expect(response.json()).resolves.toMatchObject({ ok: true });
399
- expect(send).toHaveBeenCalledExactlyOnceWith(
400
- "please triage",
401
- expect.objectContaining({
402
- continuationToken: "github-pr:acme/widgets#42",
403
- auth: expect.objectContaining({ principalId: "sender" }),
404
- })
405
- );
406
- });
407
-
408
- it("dispatches relay-stubbed issue_comment payloads (scm-events metadata)", async () => {
409
- // Regression guard for the /v0/scm-events comment gap: a metadata-only
410
- // envelope with a comment snapshot must clear the issue_comment Zod
411
- // schema and dispatch, without a payload round-trip to GitHub.
412
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
413
- const response = await routeHandler(
414
- githubChannel({ auth: [], botName: "my-agent" })
415
- )(
416
- new Request("http://127.0.0.1:3000/v1/channels/github", {
417
- method: "POST",
418
- headers: {
419
- "x-github-event": "issue_comment",
420
- "x-github-delivery": "delivery-relay-comment",
421
- },
422
- body: JSON.stringify(
423
- scmMetadataToGitHubPayload({
424
- repo: "acme/widgets",
425
- action: "created",
426
- prNumber: 42,
427
- prUrl: "https://github.com/acme/widgets/pull/42",
428
- comment: {
429
- id: 7,
430
- body: "@my-agent please triage",
431
- htmlUrl: "https://github.com/acme/widgets/pull/42#issuecomment-7",
432
- authorLogin: "sender",
433
- },
434
- })
435
- ),
436
- }),
437
- handlerArgs(send)
438
- );
439
-
440
- await expect(response.json()).resolves.toMatchObject({ ok: true });
441
- expect(send).toHaveBeenCalledExactlyOnceWith(
442
- "please triage",
443
- expect.objectContaining({
444
- continuationToken: "github-pr:acme/widgets#42",
445
- auth: expect.objectContaining({ principalId: "sender" }),
446
- })
447
- );
448
- });
449
-
450
- it("skips relay-stubbed comments from the agent's own bot (self/bot-loop gate)", async () => {
451
- // Regression guard: the relay stub must carry user.type through so
452
- // isGitHubAppBotAuthor keeps failing CLOSED for the agent's own
453
- // comments on the scm-events path. Without it, a bot comment that
454
- // @mentions the agent would wake it again (self-wake loop).
455
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
456
- const response = await routeHandler(
457
- githubChannel({ auth: [], botName: "my-agent" })
458
- )(
459
- new Request("http://127.0.0.1:3000/v1/channels/github", {
460
- method: "POST",
461
- headers: {
462
- "x-github-event": "issue_comment",
463
- "x-github-delivery": "delivery-relay-bot-comment",
464
- },
465
- body: JSON.stringify(
466
- scmMetadataToGitHubPayload({
467
- repo: "acme/widgets",
468
- action: "created",
469
- prNumber: 42,
470
- prUrl: "https://github.com/acme/widgets/pull/42",
471
- comment: {
472
- id: 8,
473
- body: "@my-agent looks done to me",
474
- htmlUrl: "https://github.com/acme/widgets/pull/42#issuecomment-8",
475
- authorLogin: "my-agent[bot]",
476
- authorType: "Bot",
477
- },
478
- })
479
- ),
480
- }),
481
- handlerArgs(send)
482
- );
483
-
484
- await expect(response.json()).resolves.toEqual({
485
- ok: true,
486
- skipped: true,
487
- reason: "ignored_comment",
488
- });
489
- expect(send).not.toHaveBeenCalled();
490
- });
491
-
492
- it("skips issue comments without an @mention", async () => {
493
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
494
- const response = await routeHandler(
495
- githubChannel({ auth: [], botName: "my-agent" })
496
- )(
497
- new Request("http://127.0.0.1:3000/v1/channels/github", {
498
- method: "POST",
499
- headers: { "x-github-event": "issue_comment" },
500
- body: JSON.stringify(issueCommentPayload("no bot here")),
501
- }),
502
- handlerArgs(send)
503
- );
504
- await expect(response.json()).resolves.toEqual({
505
- ok: true,
506
- skipped: true,
507
- reason: "not_dispatched",
508
- });
509
- expect(send).not.toHaveBeenCalled();
510
- });
511
-
512
- it("dispatches failed check_suite via onCheckSuite when a PR is attached", async () => {
513
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
514
- const response = await routeHandler(
515
- githubChannel({
516
- auth: [],
517
- onCheckSuite: (ctx, suite) =>
518
- suite.action === "completed" &&
519
- suite.conclusion === "failure" &&
520
- suite.app.slug === "github-actions" &&
521
- suite.pullRequests.length > 0
522
- ? {
523
- auth: defaultGitHubAuth(ctx),
524
- context: [
525
- `Triage failed check suite ${suite.checkSuiteId} at ${suite.headSha}.`,
526
- ],
527
- }
528
- : null,
529
- })
530
- )(
531
- new Request("http://127.0.0.1:3000/v1/channels/github", {
532
- method: "POST",
533
- headers: { "x-github-event": "check_suite" },
534
- body: JSON.stringify(checkSuitePayload()),
535
- }),
536
- handlerArgs(send)
537
- );
538
-
539
- await expect(response.json()).resolves.toMatchObject({ ok: true });
540
- expect(send).toHaveBeenCalledExactlyOnceWith(
541
- expect.stringContaining("Triage failed check suite 99 at deadbeef"),
542
- expect.objectContaining({
543
- continuationToken: "github-pr:acme/widgets#42",
544
- })
545
- );
546
- });
547
-
548
- it("requires a valid signature when a webhook secret is configured", async () => {
549
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
550
- const response = await routeHandler(
551
- githubChannel({
552
- credentials: { webhookSecret: "secret" },
553
- })
554
- )(
555
- new Request("http://127.0.0.1:3000/v1/channels/github", {
556
- method: "POST",
557
- headers: { "x-github-event": "pull_request" },
558
- body: JSON.stringify(pullRequestPayload()),
559
- }),
560
- handlerArgs(send)
561
- );
562
-
563
- expect(response.status).toBe(401);
564
- await expect(response.json()).resolves.toEqual({
565
- ok: false,
566
- error: "missing_signature",
567
- });
568
- expect(send).not.toHaveBeenCalled();
569
- });
570
-
571
- it("returns 202 and schedules waitUntil for task dispatch", async () => {
572
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
573
- const waitUntil = vi.fn();
574
- const task = vi.fn(async () => {});
575
- const response = await routeHandler(
576
- githubChannel({
577
- auth: [],
578
- onPullRequest: async () => ({ task }),
579
- })
580
- )(
581
- new Request("http://127.0.0.1:3000/v1/channels/github", {
582
- method: "POST",
583
- headers: { "x-github-event": "pull_request" },
584
- body: JSON.stringify(pullRequestPayload()),
585
- }),
586
- { ...handlerArgs(send), waitUntil }
587
- );
588
-
589
- expect(response.status).toBe(202);
590
- await expect(response.json()).resolves.toEqual({
591
- ok: true,
592
- dispatched: true,
593
- mode: "task",
594
- action: "opened",
595
- prUrl: "https://github.com/acme/widgets/pull/42",
596
- headSha: "abc123",
597
- });
598
- expect(waitUntil).toHaveBeenCalledTimes(1);
599
- expect(task).not.toHaveBeenCalled();
600
- expect(send).not.toHaveBeenCalled();
601
-
602
- const scheduled = waitUntil.mock.calls[0]?.[0] as Promise<unknown>;
603
- await scheduled;
604
- expect(task).toHaveBeenCalledTimes(1);
605
- });
606
-
607
- it("exposes ctx.github and ctx.thread to inbound hooks", async () => {
608
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
609
- let captured: GitHubEventContext | undefined;
610
- await routeHandler(
611
- githubChannel({
612
- auth: [],
613
- onPullRequest: (ctx, pr) => {
614
- captured = ctx;
615
- return pr.action === "opened"
616
- ? { auth: defaultGitHubAuth(ctx) }
617
- : null;
618
- },
619
- })
620
- )(
621
- new Request("http://127.0.0.1:3000/v1/channels/github", {
622
- method: "POST",
623
- headers: { "x-github-event": "pull_request" },
624
- body: JSON.stringify(pullRequestPayload()),
625
- }),
626
- handlerArgs(send)
627
- );
628
-
629
- expect(captured).toBeDefined();
630
- expect(typeof captured?.github.request).toBe("function");
631
- expect(typeof captured?.github.createStatus).toBe("function");
632
- expect(typeof captured?.github.getOctokit).toBe("function");
633
- expect(typeof captured?.thread.post).toBe("function");
634
- expect(captured?.thread.kind).toBe("pull_request");
635
- expect(captured?.github.repository.fullName).toBe("acme/widgets");
636
- });
637
-
638
- it("runs onStatus task dispatch with ctx.github.createStatus", async () => {
639
- clearGitHubAuthCaches();
640
- const fetchMock = vi.fn(
641
- async () => new Response(JSON.stringify({}), { status: 201 })
642
- );
643
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
644
- const waitUntil = vi.fn();
645
- const response = await routeHandler(
646
- githubChannel({
647
- auth: [],
648
- credentials: { token: "test-token" },
649
- api: { fetch: fetchMock as unknown as typeof fetch },
650
- onStatus: (ctx, status) =>
651
- status.state === "failure"
652
- ? {
653
- task: async () => {
654
- await ctx.github.createStatus({
655
- sha: status.sha,
656
- state: "pending",
657
- context: "agent/triage",
658
- });
659
- },
660
- }
661
- : null,
662
- })
663
- )(
664
- new Request("http://127.0.0.1:3000/v1/channels/github", {
665
- method: "POST",
666
- headers: { "x-github-event": "status" },
667
- body: JSON.stringify(statusPayload()),
668
- }),
669
- { ...handlerArgs(send), waitUntil }
670
- );
671
-
672
- expect(response.status).toBe(202);
673
- await expect(response.json()).resolves.toMatchObject({
674
- ok: true,
675
- dispatched: true,
676
- mode: "task",
677
- event: "status",
678
- state: "failure",
679
- sha: "deadbeef",
680
- });
681
- expect(send).not.toHaveBeenCalled();
682
-
683
- const scheduled = waitUntil.mock.calls[0]?.[0] as Promise<unknown>;
684
- await scheduled;
685
- expect(fetchMock).toHaveBeenCalledOnce();
686
- const call = fetchMock.mock.calls[0];
687
- expect(call).toBeDefined();
688
- const [url, init] = call as unknown as [unknown, RequestInit | undefined];
689
- expect(String(url)).toContain("/repos/acme/widgets/statuses/deadbeef");
690
- expect(init?.method).toBe("POST");
691
- });
692
-
693
- it("dispatches unknown events through onEvent", async () => {
694
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
695
- let capturedEvent: GitHubRawEvent | undefined;
696
- const response = await routeHandler(
697
- githubChannel({
698
- auth: [],
699
- onEvent: (ctx, event) => {
700
- capturedEvent = event;
701
- return event.eventName === "label"
702
- ? { auth: defaultGitHubAuth(ctx) }
703
- : null;
704
- },
705
- })
706
- )(
707
- new Request("http://127.0.0.1:3000/v1/channels/github", {
708
- method: "POST",
709
- headers: {
710
- "x-github-event": "label",
711
- "x-github-delivery": "delivery-label",
712
- },
713
- body: JSON.stringify(labelPayload()),
714
- }),
715
- handlerArgs(send)
716
- );
717
-
718
- await expect(response.json()).resolves.toMatchObject({ ok: true });
719
- expect(capturedEvent?.eventName).toBe("label");
720
- expect(capturedEvent?.action).toBe("created");
721
- expect(send).toHaveBeenCalledExactlyOnceWith(
722
- expect.stringContaining("GitHub label event"),
723
- expect.objectContaining({
724
- continuationToken: "github-issue:acme/widgets#42",
725
- })
726
- );
727
- });
728
-
729
- it("schedules task dispatch for unanchored check_suite events", async () => {
730
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
731
- const waitUntil = vi.fn();
732
- const task = vi.fn(async () => {});
733
- const response = await routeHandler(
734
- githubChannel({
735
- auth: [],
736
- onCheckSuite: async () => ({ task }),
737
- })
738
- )(
739
- new Request("http://127.0.0.1:3000/v1/channels/github", {
740
- method: "POST",
741
- headers: { "x-github-event": "check_suite" },
742
- body: JSON.stringify({
743
- ...checkSuitePayload(),
744
- check_suite: {
745
- ...(checkSuitePayload().check_suite as Record<string, unknown>),
746
- pull_requests: [],
747
- },
748
- }),
749
- }),
750
- { ...handlerArgs(send), waitUntil }
751
- );
752
-
753
- expect(response.status).toBe(202);
754
- await expect(response.json()).resolves.toMatchObject({
755
- ok: true,
756
- dispatched: true,
757
- mode: "task",
758
- event: "check_suite",
759
- });
760
- expect(send).not.toHaveBeenCalled();
761
- const scheduled = waitUntil.mock.calls[0]?.[0] as Promise<unknown>;
762
- await scheduled;
763
- expect(task).toHaveBeenCalledTimes(1);
764
- });
765
-
766
- it("ignores outbound comments stamped with the agent marker", async () => {
767
- const send = vi.fn<SendMessageFn>(async () => fakeSession());
768
- const response = await routeHandler(
769
- githubChannel({
770
- auth: [],
771
- botName: "my-agent",
772
- })
773
- )(
774
- new Request("http://127.0.0.1:3000/v1/channels/github", {
775
- method: "POST",
776
- headers: { "x-github-event": "issue_comment" },
777
- body: JSON.stringify(
778
- issueCommentPayload(
779
- formatGitHubOutboundComment("Thanks @my-agent for the help!")
780
- )
781
- ),
782
- }),
783
- handlerArgs(send)
784
- );
785
-
786
- await expect(response.json()).resolves.toEqual({
787
- ok: true,
788
- skipped: true,
789
- reason: "ignored_comment",
790
- });
791
- expect(send).not.toHaveBeenCalled();
792
- });
793
- });
794
-
795
- function routeHandler(
796
- channel: ReturnType<typeof githubChannel>
797
- ): ChannelRouteHandler {
798
- const route = channel.routes?.[0];
799
- if (route === undefined) {
800
- throw new Error("expected github channel route");
801
- }
802
- return route.handler;
803
- }
804
-
805
- function handlerArgs(send: SendMessageFn): ChannelHandlerArgs {
806
- return {
807
- params: {},
808
- requestIp: "127.0.0.1",
809
- auth: null,
810
- host: {
811
- mcp: {
812
- names: () => [],
813
- listTools: async () => [],
814
- callTool: async () => ({ content: [] }),
815
- },
816
- github: {
817
- getOctokit: async () => {
818
- throw new Error("github client not configured in test");
819
- },
820
- },
821
- slack: {
822
- getClient: async () => {
823
- throw new Error("slack client not configured in test");
824
- },
825
- },
826
- kv: {
827
- get: async () => undefined,
828
- put: async () => {},
829
- delete: async () => {},
830
- },
831
- },
832
- artifacts: {
833
- tag: async () => {
834
- throw new Error("artifacts not configured in test");
835
- },
836
- list: async () => [],
837
- },
838
- send,
839
- getSession: async () => null,
840
- receive: async () => fakeSession(),
841
- callTool: async (toolName) => ({
842
- toolName,
843
- callId: "call-1",
844
- isError: false,
845
- result: null,
846
- durationMs: 0,
847
- }),
848
- waitUntil: () => {},
849
- sessionUrls: (_request, sessionId) => ({
850
- playgroundUrl: `http://127.0.0.1:3000/playground?sessionId=${sessionId}`,
851
- traceUrl: `http://127.0.0.1:3000/v1/session/${sessionId}/events`,
852
- }),
853
- };
854
- }
855
-
856
- function fakeSession(): ChannelSession {
857
- return {
858
- id: "session-1",
859
- isNew: true,
860
- continuationToken: "github-pr:acme/widgets#42",
861
- info: {
862
- id: "session-1",
863
- channelId: "github",
864
- mode: "chat",
865
- auth: null,
866
- sdkAgentId: undefined,
867
- title: "GitHub acme/widgets#42",
868
- },
869
- getEventStream: async () => new ReadableStream<Uint8Array>(),
870
- waitForCompletion: async () => ({ status: "finished", result: "done" }),
871
- setContinuationToken: async () => {},
872
- };
873
- }
874
-
875
- function pullRequestPayload(
876
- overrides: { action?: string } = {}
877
- ): Record<string, unknown> {
878
- return {
879
- action: overrides.action ?? "opened",
880
- repository: {
881
- full_name: "acme/widgets",
882
- html_url: "https://github.com/acme/widgets",
883
- },
884
- pull_request: {
885
- number: 42,
886
- html_url: "https://github.com/acme/widgets/pull/42",
887
- title: "Add widgets",
888
- body: "PR body",
889
- draft: false,
890
- user: { login: "octocat" },
891
- head: {
892
- ref: "feature/widgets",
893
- sha: "abc123",
894
- },
895
- base: {
896
- ref: "main",
897
- },
898
- },
899
- sender: { login: "sender" },
900
- };
901
- }
902
-
903
- function issueCommentPayload(body: string): Record<string, unknown> {
904
- return {
905
- action: "created",
906
- repository: {
907
- full_name: "acme/widgets",
908
- html_url: "https://github.com/acme/widgets",
909
- },
910
- issue: {
911
- number: 42,
912
- html_url: "https://github.com/acme/widgets/pull/42",
913
- pull_request: {},
914
- },
915
- comment: {
916
- id: 7,
917
- body,
918
- html_url: "https://github.com/acme/widgets/pull/42#issuecomment-7",
919
- url: "https://api.github.com/repos/acme/widgets/issues/comments/7",
920
- user: { login: "sender", type: "User" },
921
- },
922
- sender: { login: "sender", type: "User" },
923
- };
924
- }
925
-
926
- function statusPayload(): Record<string, unknown> {
927
- return {
928
- state: "failure",
929
- sha: "deadbeef",
930
- context: "ci/build",
931
- description: "Build failed",
932
- target_url: "https://ci.example.com/build/1",
933
- name: "acme/widgets",
934
- branches: [{ name: "main", commit: { sha: "deadbeef" } }],
935
- commit: {
936
- sha: "deadbeef",
937
- html_url: "https://github.com/acme/widgets/commit/deadbeef",
938
- },
939
- repository: {
940
- full_name: "acme/widgets",
941
- html_url: "https://github.com/acme/widgets",
942
- },
943
- sender: { login: "ci-bot", type: "Bot" },
944
- };
945
- }
946
-
947
- function labelPayload(): Record<string, unknown> {
948
- return {
949
- action: "created",
950
- label: { name: "bug", color: "ff0000" },
951
- repository: {
952
- full_name: "acme/widgets",
953
- html_url: "https://github.com/acme/widgets",
954
- },
955
- issue: {
956
- number: 42,
957
- html_url: "https://github.com/acme/widgets/issues/42",
958
- },
959
- sender: { login: "sender", type: "User" },
960
- };
961
- }
962
-
963
- function checkSuitePayload(): Record<string, unknown> {
964
- return {
965
- action: "completed",
966
- repository: {
967
- full_name: "acme/widgets",
968
- html_url: "https://github.com/acme/widgets",
969
- },
970
- check_suite: {
971
- id: 99,
972
- head_sha: "deadbeef",
973
- status: "completed",
974
- conclusion: "failure",
975
- app: { slug: "github-actions", name: "GitHub Actions" },
976
- pull_requests: [
977
- {
978
- number: 42,
979
- html_url: "https://github.com/acme/widgets/pull/42",
980
- },
981
- ],
982
- },
983
- sender: { login: "github-actions[bot]", type: "Bot" },
984
- };
985
- }