@cursor/july 0.1.26 → 0.1.27

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 (277) 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 +31 -9
  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/cursor-account.d.ts +2 -1
  17. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  18. package/dist/channels/slack/cursor-account.js +1 -1
  19. package/dist/channels/slack/dispatch.d.ts +4 -1
  20. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  21. package/dist/channels/slack/dispatch.js +3 -3
  22. package/dist/channels/slack/interactive.d.ts +2 -1
  23. package/dist/channels/slack/interactive.d.ts.map +1 -1
  24. package/dist/channels/slack/interactive.js +1 -0
  25. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  26. package/dist/channels/slack/slack-channel.js +4 -3
  27. package/dist/channels/slack/types.d.ts +6 -1
  28. package/dist/channels/slack/types.d.ts.map +1 -1
  29. package/dist/docs/404.html +1 -1
  30. package/dist/docs/ab.html +2 -2
  31. package/dist/docs/assets/{app.DYcC9FY-.js → app.BICLoJYF.js} +1 -1
  32. package/dist/docs/assets/chunks/@localSearchIndexroot.CQJytDwU.js +1 -0
  33. package/dist/docs/assets/chunks/{VPLocalSearchBox.o4N_knTV.js → VPLocalSearchBox.wN4Ko9dT.js} +1 -1
  34. package/dist/docs/assets/chunks/{theme.DQ-njyo0.js → theme.dnnTE-Co.js} +2 -2
  35. package/dist/docs/building-with-agents.html +2 -2
  36. package/dist/docs/concepts.html +2 -2
  37. package/dist/docs/deployment.html +2 -2
  38. package/dist/docs/evals.html +2 -2
  39. package/dist/docs/example-agents/approval-buddy.html +2 -2
  40. package/dist/docs/example-agents/benny.html +2 -2
  41. package/dist/docs/example-agents/bugbot.html +2 -2
  42. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  43. package/dist/docs/example-agents/codeowners-review.html +2 -2
  44. package/dist/docs/example-agents/concierge.html +2 -2
  45. package/dist/docs/example-agents/fsd.html +2 -2
  46. package/dist/docs/example-agents/index.html +2 -2
  47. package/dist/docs/example-agents/knowledge-base.html +2 -2
  48. package/dist/docs/example-agents/oncall.html +2 -2
  49. package/dist/docs/example-agents/security-reviewer.html +2 -2
  50. package/dist/docs/example-agents/slack-agent.html +2 -2
  51. package/dist/docs/example-agents/weather-agent.html +2 -2
  52. package/dist/docs/guides/agent-to-agent.html +2 -2
  53. package/dist/docs/guides/cloud-runtime.html +2 -2
  54. package/dist/docs/guides/github.html +2 -2
  55. package/dist/docs/guides/human-in-the-loop.html +2 -2
  56. package/dist/docs/guides/mcp-oauth.html +2 -2
  57. package/dist/docs/guides/slack.html +2 -2
  58. package/dist/docs/guides/webhooks.html +2 -2
  59. package/dist/docs/hillclimbing.html +2 -2
  60. package/dist/docs/index.html +2 -2
  61. package/dist/docs/quickstart.html +2 -2
  62. package/dist/docs/reference/agent-config.html +2 -2
  63. package/dist/docs/reference/channels.html +2 -2
  64. package/dist/docs/reference/cli.html +2 -2
  65. package/dist/docs/reference/connections.html +2 -2
  66. package/dist/docs/reference/hooks.html +2 -2
  67. package/dist/docs/reference/http-api.html +2 -2
  68. package/dist/docs/reference/instructions.html +2 -2
  69. package/dist/docs/reference/playground.html +2 -2
  70. package/dist/docs/reference/project-layout.html +2 -2
  71. package/dist/docs/reference/prompt.html +2 -2
  72. package/dist/docs/reference/schedules.html +2 -2
  73. package/dist/docs/reference/sessions.html +2 -2
  74. package/dist/docs/reference/skills.html +2 -2
  75. package/dist/docs/reference/subagents.html +2 -2
  76. package/dist/docs/reference/tools.html +2 -2
  77. package/dist/docs/scaffolding-agents.html +2 -2
  78. package/dist/docs/storage.html +2 -2
  79. package/dist/docs/troubleshooting.html +2 -2
  80. package/dist/evals/assertions.d.ts +10 -1
  81. package/dist/evals/assertions.d.ts.map +1 -1
  82. package/dist/evals/assertions.js +12 -0
  83. package/dist/evals.d.ts +1 -1
  84. package/dist/evals.d.ts.map +1 -1
  85. package/dist/index.d.ts +1 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +1 -0
  88. package/dist/internal/ab-fold.d.ts.map +1 -1
  89. package/dist/internal/ab-fold.js +4 -0
  90. package/dist/internal/ab-snapshot.d.ts.map +1 -1
  91. package/dist/internal/ab-snapshot.js +1 -0
  92. package/dist/internal/artifacts-store.d.ts +45 -0
  93. package/dist/internal/artifacts-store.d.ts.map +1 -0
  94. package/dist/internal/artifacts-store.js +250 -0
  95. package/dist/internal/builtin-tools/artifacts.d.ts +10 -0
  96. package/dist/internal/builtin-tools/artifacts.d.ts.map +1 -0
  97. package/dist/internal/builtin-tools/artifacts.js +73 -0
  98. package/dist/internal/builtin-tools/index.d.ts +8 -1
  99. package/dist/internal/builtin-tools/index.d.ts.map +1 -1
  100. package/dist/internal/builtin-tools/index.js +15 -0
  101. package/dist/internal/cli-ax.d.ts +11 -0
  102. package/dist/internal/cli-ax.d.ts.map +1 -1
  103. package/dist/internal/cli-ax.js +80 -0
  104. package/dist/internal/cli-slack.d.ts.map +1 -1
  105. package/dist/internal/cli-slack.js +14 -6
  106. package/dist/internal/discovery.d.ts.map +1 -1
  107. package/dist/internal/discovery.js +83 -2
  108. package/dist/internal/distribution.d.ts.map +1 -1
  109. package/dist/internal/distribution.js +1 -0
  110. package/dist/internal/model-pricing.d.ts +49 -0
  111. package/dist/internal/model-pricing.d.ts.map +1 -0
  112. package/dist/internal/model-pricing.js +377 -0
  113. package/dist/internal/reminder-runner.d.ts.map +1 -1
  114. package/dist/internal/reminder-runner.js +1 -0
  115. package/dist/internal/schedule-runner.d.ts.map +1 -1
  116. package/dist/internal/schedule-runner.js +1 -0
  117. package/dist/internal/sdk-runner.d.ts.map +1 -1
  118. package/dist/internal/sdk-runner.js +9 -1
  119. package/dist/internal/server.d.ts.map +1 -1
  120. package/dist/internal/server.js +39 -0
  121. package/dist/internal/session-cost.d.ts +49 -0
  122. package/dist/internal/session-cost.d.ts.map +1 -0
  123. package/dist/internal/session-cost.js +175 -0
  124. package/dist/internal/session-engine.d.ts +35 -1
  125. package/dist/internal/session-engine.d.ts.map +1 -1
  126. package/dist/internal/session-engine.js +128 -7
  127. package/dist/internal/storage-coordinator.d.ts +15 -0
  128. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  129. package/dist/internal/storage-coordinator.js +52 -0
  130. package/dist/internal/trajectory.d.ts +9 -1
  131. package/dist/internal/trajectory.d.ts.map +1 -1
  132. package/dist/internal/trajectory.js +32 -5
  133. package/dist/playground/assets/index-BDCeW4uc.js +86 -0
  134. package/dist/playground/assets/index-Bul1WfrB.css +1 -0
  135. package/dist/playground/index.html +2 -2
  136. package/dist/storage.d.ts +4 -0
  137. package/dist/storage.d.ts.map +1 -1
  138. package/dist/storage.js +4 -0
  139. package/dist/types.d.ts +164 -1
  140. package/dist/types.d.ts.map +1 -1
  141. package/package.json +26 -26
  142. package/src/ab.ts +7 -0
  143. package/src/bin/agent-serve.ts +30 -2
  144. package/src/internal/ab-fold.ts +4 -0
  145. package/src/internal/ab-snapshot.ts +1 -0
  146. package/src/internal/cli-ax.ts +95 -0
  147. package/src/internal/model-pricing.ts +426 -0
  148. package/src/internal/sdk-runner.ts +10 -2
  149. package/src/internal/session-cost.ts +231 -0
  150. package/src/internal/session-engine.ts +28 -0
  151. package/src/internal/trajectory.ts +45 -3
  152. package/src/types.ts +24 -1
  153. package/dist/channels/github/instrument.d.ts +0 -20
  154. package/dist/channels/github/instrument.d.ts.map +0 -1
  155. package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +0 -1
  156. package/dist/internal/json-dir-store.d.ts +0 -32
  157. package/dist/internal/json-dir-store.d.ts.map +0 -1
  158. package/dist/internal/json-dir-store.js +0 -100
  159. package/dist/playground/assets/index-CjOQ4hN9.css +0 -1
  160. package/dist/playground/assets/index-DqXdAFGa.js +0 -85
  161. package/src/bin/agent-serve.version.test.ts +0 -64
  162. package/src/channels/github/api.test.ts +0 -64
  163. package/src/channels/github/auth.test.ts +0 -105
  164. package/src/channels/github/cursor-account.test.ts +0 -204
  165. package/src/channels/github/forward.test.ts +0 -457
  166. package/src/channels/github/github.test.ts +0 -985
  167. package/src/channels/github/replay.test.ts +0 -179
  168. package/src/channels/slack/api.post-message.test.ts +0 -148
  169. package/src/channels/slack/approvals.test.ts +0 -328
  170. package/src/channels/slack/block-actions.test.ts +0 -469
  171. package/src/channels/slack/bot-mentions.test.ts +0 -267
  172. package/src/channels/slack/channel-watch.test.ts +0 -363
  173. package/src/channels/slack/cursor-account.test.ts +0 -301
  174. package/src/channels/slack/defaults.final-post.test.ts +0 -182
  175. package/src/channels/slack/dispatch.test.ts +0 -875
  176. package/src/channels/slack/eval-directive.test.ts +0 -273
  177. package/src/channels/slack/message-body.test.ts +0 -54
  178. package/src/channels/slack/nudge-store.test.ts +0 -143
  179. package/src/channels/slack/slack.test.ts +0 -391
  180. package/src/channels/slack/stop.test.ts +0 -23
  181. package/src/channels/slack/thread-context.test.ts +0 -202
  182. package/src/evals/assertions.test.ts +0 -617
  183. package/src/evals/expect.test.ts +0 -144
  184. package/src/evals/judge.test.ts +0 -181
  185. package/src/evals/loaders.test.ts +0 -132
  186. package/src/evals/matchers.test.ts +0 -95
  187. package/src/evals/reporters.test.ts +0 -303
  188. package/src/evals/run-facts.test.ts +0 -259
  189. package/src/internal/ab-snapshot.test.ts +0 -325
  190. package/src/internal/approval-gate.test.ts +0 -49
  191. package/src/internal/approvals.integration.test.ts +0 -383
  192. package/src/internal/artifacts-store.test.ts +0 -283
  193. package/src/internal/authored-loaders.test.ts +0 -31
  194. package/src/internal/builtin-tools/artifacts.test.ts +0 -247
  195. package/src/internal/builtin-tools/reminders.test.ts +0 -201
  196. package/src/internal/channel-route-schema.test.ts +0 -294
  197. package/src/internal/chat-attach.test.ts +0 -262
  198. package/src/internal/cli-deploy.test.ts +0 -1991
  199. package/src/internal/cli-docs.test.ts +0 -161
  200. package/src/internal/cli-mcp.test.ts +0 -789
  201. package/src/internal/cli-skills.test.ts +0 -133
  202. package/src/internal/cli-slack.test.ts +0 -1654
  203. package/src/internal/cloud-merge.test.ts +0 -74
  204. package/src/internal/cron.test.ts +0 -22
  205. package/src/internal/cursor/account-mcp.test.ts +0 -807
  206. package/src/internal/cursor/backend-client.test.ts +0 -591
  207. package/src/internal/cursor/credentials.test.ts +0 -351
  208. package/src/internal/cursor/github-credentials.test.ts +0 -136
  209. package/src/internal/cursor-account-mcp-auth.test.ts +0 -310
  210. package/src/internal/cursor-account.integration.test.ts +0 -441
  211. package/src/internal/cursor-event-relay.test.ts +0 -746
  212. package/src/internal/cursor-github-credentials.integration.test.ts +0 -271
  213. package/src/internal/cursor-slack-relay.test.ts +0 -525
  214. package/src/internal/deploy-source.test.ts +0 -111
  215. package/src/internal/discovery.artifact-tool.test.ts +0 -136
  216. package/src/internal/discovery.artifacts.test.ts +0 -119
  217. package/src/internal/discovery.builtin-tools.test.ts +0 -94
  218. package/src/internal/discovery.concurrency.test.ts +0 -60
  219. package/src/internal/discovery.cursor-account.test.ts +0 -133
  220. package/src/internal/discovery.cwd.test.ts +0 -83
  221. package/src/internal/discovery.hosting.test.ts +0 -80
  222. package/src/internal/discovery.identity.test.ts +0 -44
  223. package/src/internal/docs-site.test.ts +0 -66
  224. package/src/internal/duration.test.ts +0 -29
  225. package/src/internal/eval-judge-model.test.ts +0 -187
  226. package/src/internal/eval-run-store.cancel.test.ts +0 -142
  227. package/src/internal/eval-run-store.storage.test.ts +0 -211
  228. package/src/internal/eval-runner.http.test.ts +0 -403
  229. package/src/internal/eval-runner.run.test.ts +0 -928
  230. package/src/internal/evals-client.test.ts +0 -307
  231. package/src/internal/event-mapper.test.ts +0 -243
  232. package/src/internal/github-fanout.test.ts +0 -213
  233. package/src/internal/handleAgentServeTrigger.test.ts +0 -185
  234. package/src/internal/host-kv.test.ts +0 -82
  235. package/src/internal/host-platforms.test.ts +0 -126
  236. package/src/internal/http-channel.test.ts +0 -402
  237. package/src/internal/init-project.test.ts +0 -270
  238. package/src/internal/install-cursor-skills.test.ts +0 -262
  239. package/src/internal/local-env.test.ts +0 -120
  240. package/src/internal/log-ring.test.ts +0 -31
  241. package/src/internal/logs-client.test.ts +0 -350
  242. package/src/internal/mcp-endpoint.test.ts +0 -436
  243. package/src/internal/mcp-host.test.ts +0 -298
  244. package/src/internal/mcp-oauth.test.ts +0 -148
  245. package/src/internal/net.test.ts +0 -17
  246. package/src/internal/peer-connections.test.ts +0 -128
  247. package/src/internal/peer-mcp.integration.test.ts +0 -289
  248. package/src/internal/playground/toolchain.test.ts +0 -53
  249. package/src/internal/playground-cli.test.ts +0 -187
  250. package/src/internal/playground-proxy.test.ts +0 -376
  251. package/src/internal/prompt-context.integration.test.ts +0 -232
  252. package/src/internal/prompt-context.test.ts +0 -127
  253. package/src/internal/reminder-runner.test.ts +0 -432
  254. package/src/internal/reminder-store.test.ts +0 -53
  255. package/src/internal/request-headers.test.ts +0 -27
  256. package/src/internal/resolve-prod-target.test.ts +0 -787
  257. package/src/internal/resolved-connections.test.ts +0 -295
  258. package/src/internal/router.test.ts +0 -57
  259. package/src/internal/sdk-runner.test.ts +0 -290
  260. package/src/internal/server.artifacts.test.ts +0 -334
  261. package/src/internal/session-engine.artifacts.test.ts +0 -293
  262. package/src/internal/session-engine.coalesce.test.ts +0 -169
  263. package/src/internal/session-engine.concurrency.test.ts +0 -250
  264. package/src/internal/session-engine.host-oauth-mcp.test.ts +0 -110
  265. package/src/internal/session-engine.interrupt.test.ts +0 -577
  266. package/src/internal/session-engine.storage.test.ts +0 -547
  267. package/src/internal/session-urls.test.ts +0 -28
  268. package/src/internal/sessions-client.test.ts +0 -518
  269. package/src/internal/storage-coordinator.test.ts +0 -517
  270. package/src/internal/tool-call.test.ts +0 -458
  271. package/src/internal/tool-result.test.ts +0 -52
  272. package/src/internal/trajectory.approvals.test.ts +0 -83
  273. package/src/internal/trajectory.subagents.test.ts +0 -198
  274. package/src/internal/turn-governor.test.ts +0 -137
  275. package/src/internal/update-check.test.ts +0 -485
  276. package/src/internal/workspace.test.ts +0 -207
  277. package/src/storage-backends/cursor-hosted.test.ts +0 -121
@@ -1,746 +0,0 @@
1
- import { mkdtemp, readFile, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { httpChannel } from "../channels.js";
6
- import type { AgentProject, AgentServeHandle } from "../types.js";
7
- import {
8
- CursorEventRelay,
9
- type CursorScmEvent,
10
- formatCursorScmEventLog,
11
- RelayOffsetStore,
12
- scmMetadataToGitHubPayload,
13
- } from "./cursor-event-relay.js";
14
- import type { AgentRunner } from "./sdk-runner.js";
15
- import { startServer } from "./server.js";
16
-
17
- interface RecordedRequest {
18
- method: string;
19
- url: string;
20
- body?: unknown;
21
- }
22
-
23
- /**
24
- * Minimal fake of the /v0/scm-events surface: records requests and serves a
25
- * scripted queue of poll responses (after the queue drains, polls return
26
- * empty pages). Registration echoes the requested repos as subscribed;
27
- * `registrationFailures` makes the first N registration attempts fail with
28
- * a 500.
29
- */
30
- function fakeBackend(script: {
31
- anchorOffset?: string;
32
- polls?: Array<{
33
- events: CursorScmEvent[];
34
- nextOffset: string;
35
- gapDetected?: boolean;
36
- }>;
37
- registrationFailures?: number;
38
- }) {
39
- const requests: RecordedRequest[] = [];
40
- const polls = [...(script.polls ?? [])];
41
- let registrationFailures = script.registrationFailures ?? 0;
42
- const fetchImpl: typeof fetch = async (input, init) => {
43
- const url = String(input);
44
- const method = init?.method ?? "GET";
45
- const body =
46
- typeof init?.body === "string"
47
- ? (JSON.parse(init.body) as unknown)
48
- : undefined;
49
- requests.push({ method, url, ...(body === undefined ? {} : { body }) });
50
-
51
- if (url.includes("/v0/scm-events/subscriptions")) {
52
- if (registrationFailures > 0) {
53
- registrationFailures -= 1;
54
- return new Response("storage unavailable", { status: 503 });
55
- }
56
- return jsonResponse({
57
- repos: (
58
- (body as { subscriptions?: Array<{ repo: string }> })
59
- ?.subscriptions ?? []
60
- ).map((subscription) => ({
61
- provider: "github",
62
- repo: subscription.repo,
63
- isSubscribed: true,
64
- })),
65
- expiresAt: new Date().toISOString(),
66
- });
67
- }
68
- if (url.includes("after=%24") || url.includes("after=$")) {
69
- return jsonResponse({
70
- events: [],
71
- nextOffset: script.anchorOffset ?? "",
72
- gapDetected: false,
73
- });
74
- }
75
- const page = polls.shift() ?? {
76
- events: [],
77
- nextOffset: "",
78
- gapDetected: false,
79
- };
80
- return jsonResponse(page);
81
- };
82
- return { requests, fetchImpl };
83
- }
84
-
85
- function jsonResponse(body: unknown): Response {
86
- return new Response(JSON.stringify(body), {
87
- status: 200,
88
- headers: { "content-type": "application/json" },
89
- });
90
- }
91
-
92
- function event(
93
- offset: string,
94
- eventName: string,
95
- repo = "acme/repo"
96
- ): CursorScmEvent {
97
- return {
98
- offset,
99
- provider: "github",
100
- eventName,
101
- deliveryId: `delivery-${offset}`,
102
- repo,
103
- receivedAtMs: Date.now(),
104
- };
105
- }
106
-
107
- describe("formatCursorScmEventLog", () => {
108
- it("includes PR url, action, and timestamps", () => {
109
- expect(
110
- formatCursorScmEventLog({
111
- eventName: "issue_comment",
112
- action: "created",
113
- repo: "anysphere/everysphere",
114
- prNumber: 167588,
115
- prUrl: "https://github.com/anysphere/everysphere/pull/167588",
116
- receivedAtMs: Date.parse("2026-07-13T16:00:00.000Z"),
117
- occurredAtMs: Date.parse("2026-07-13T15:59:00.000Z"),
118
- deliveryId: "abc",
119
- })
120
- ).toBe(
121
- "issue_comment.created on https://github.com/anysphere/everysphere/pull/167588 received=2026-07-13T16:00:00.000Z occurred=2026-07-13T15:59:00.000Z (abc)"
122
- );
123
- });
124
- });
125
-
126
- describe("scmMetadataToGitHubPayload", () => {
127
- it("stubs a Zod-complete pull_request when rich meta is present", () => {
128
- const payload = scmMetadataToGitHubPayload({
129
- repo: "acme/repo",
130
- action: "opened",
131
- prNumber: 12,
132
- prUrl: "https://github.com/acme/repo/pull/12",
133
- pullRequest: {
134
- number: 12,
135
- url: "https://github.com/acme/repo/pull/12",
136
- title: "Add widgets",
137
- body: null,
138
- draft: false,
139
- merged: false,
140
- authorLogin: "bob",
141
- headRef: "feature",
142
- headSha: "aaa111",
143
- baseRef: "main",
144
- baseSha: "bbb222",
145
- },
146
- });
147
- expect(payload).toEqual({
148
- action: "opened",
149
- repository: {
150
- full_name: "acme/repo",
151
- name: "repo",
152
- html_url: "https://github.com/acme/repo",
153
- owner: { login: "acme" },
154
- },
155
- pull_request: {
156
- number: 12,
157
- html_url: "https://github.com/acme/repo/pull/12",
158
- title: "Add widgets",
159
- body: null,
160
- draft: false,
161
- merged: false,
162
- user: { login: "bob" },
163
- head: { ref: "feature", sha: "aaa111" },
164
- base: { ref: "main", sha: "bbb222" },
165
- },
166
- issue: {
167
- number: 12,
168
- html_url: "https://github.com/acme/repo/pull/12",
169
- pull_request: {},
170
- },
171
- });
172
- });
173
-
174
- it("stubs merged=true so closed+merged handlers can fire", () => {
175
- const payload = scmMetadataToGitHubPayload({
176
- repo: "acme/repo",
177
- action: "closed",
178
- prNumber: 99,
179
- pullRequest: {
180
- number: 99,
181
- url: "https://github.com/acme/repo/pull/99",
182
- title: "Ship it",
183
- draft: false,
184
- merged: true,
185
- authorLogin: "alice",
186
- headRef: "ship",
187
- headSha: "aaa",
188
- baseRef: "main",
189
- baseSha: "bbb",
190
- },
191
- });
192
- expect(payload.action).toBe("closed");
193
- expect(
194
- (payload.pull_request as { merged?: boolean } | undefined)?.merged
195
- ).toBe(true);
196
- });
197
-
198
- it("falls back to identity-only stubs without head/base", () => {
199
- expect(
200
- scmMetadataToGitHubPayload({
201
- repo: "acme/repo",
202
- action: "opened",
203
- prNumber: 12,
204
- prUrl: "https://github.com/acme/repo/pull/12",
205
- pullRequest: {
206
- number: 12,
207
- url: "https://github.com/acme/repo/pull/12",
208
- },
209
- })
210
- ).toEqual({
211
- action: "opened",
212
- repository: {
213
- full_name: "acme/repo",
214
- name: "repo",
215
- html_url: "https://github.com/acme/repo",
216
- owner: { login: "acme" },
217
- },
218
- pull_request: {
219
- number: 12,
220
- html_url: "https://github.com/acme/repo/pull/12",
221
- },
222
- issue: {
223
- number: 12,
224
- html_url: "https://github.com/acme/repo/pull/12",
225
- pull_request: {},
226
- },
227
- });
228
- });
229
-
230
- it("stubs a Zod-complete comment for issue_comment envelopes", () => {
231
- const payload = scmMetadataToGitHubPayload({
232
- repo: "acme/repo",
233
- action: "created",
234
- prNumber: 12,
235
- prUrl: "https://github.com/acme/repo/pull/12",
236
- comment: {
237
- id: 5551212,
238
- body: "@my-agent rereview",
239
- htmlUrl: "https://github.com/acme/repo/pull/12#issuecomment-5551212",
240
- authorLogin: "alice",
241
- authorType: "User",
242
- },
243
- });
244
- expect(payload.comment).toEqual({
245
- id: 5551212,
246
- body: "@my-agent rereview",
247
- html_url: "https://github.com/acme/repo/pull/12#issuecomment-5551212",
248
- user: { login: "alice", type: "User" },
249
- });
250
- expect(payload.sender).toEqual({ login: "alice", type: "User" });
251
- expect(payload.issue).toEqual({
252
- number: 12,
253
- html_url: "https://github.com/acme/repo/pull/12",
254
- pull_request: {},
255
- });
256
- });
257
-
258
- it("carries review-thread anchors for pull_request_review_comment", () => {
259
- const payload = scmMetadataToGitHubPayload({
260
- repo: "acme/repo",
261
- action: "created",
262
- prNumber: 12,
263
- comment: {
264
- id: 901,
265
- body: "nit",
266
- htmlUrl: "https://github.com/acme/repo/pull/12#discussion_r901",
267
- authorLogin: "bob",
268
- path: "src/index.ts",
269
- line: 3,
270
- pullRequestReviewId: 77,
271
- inReplyToId: 900,
272
- },
273
- });
274
- expect(payload.comment).toEqual({
275
- id: 901,
276
- body: "nit",
277
- html_url: "https://github.com/acme/repo/pull/12#discussion_r901",
278
- user: { login: "bob" },
279
- path: "src/index.ts",
280
- line: 3,
281
- pull_request_review_id: 77,
282
- in_reply_to_id: 900,
283
- });
284
- });
285
- });
286
-
287
- async function waitFor(
288
- predicate: () => boolean,
289
- timeoutMs = 2_000
290
- ): Promise<void> {
291
- const deadline = Date.now() + timeoutMs;
292
- while (!predicate()) {
293
- if (Date.now() > deadline) {
294
- throw new Error("waitFor timed out");
295
- }
296
- await new Promise((resolve) => setTimeout(resolve, 10));
297
- }
298
- }
299
-
300
- describe("RelayOffsetStore", () => {
301
- let dir: string;
302
-
303
- afterEach(async () => {
304
- if (dir !== undefined) {
305
- await rm(dir, { recursive: true, force: true });
306
- }
307
- });
308
-
309
- it("round-trips offsets and tolerates a missing file", async () => {
310
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
311
- const store = new RelayOffsetStore(dir);
312
- expect(await store.load()).toBeUndefined();
313
- await store.save("123-4");
314
- expect(await store.load()).toBe("123-4");
315
- const raw = JSON.parse(await readFile(join(dir, "offset.json"), "utf8"));
316
- expect(raw.version).toBe(1);
317
- });
318
- });
319
-
320
- describe("CursorEventRelay", () => {
321
- let dir: string;
322
- let relay: CursorEventRelay | undefined;
323
-
324
- afterEach(async () => {
325
- await relay?.stop();
326
- relay = undefined;
327
- if (dir !== undefined) {
328
- await rm(dir, { recursive: true, force: true });
329
- }
330
- });
331
-
332
- function makeRelay(
333
- backend: ReturnType<typeof fakeBackend>,
334
- options?: {
335
- dispatch?: (e: CursorScmEvent) => Promise<void>;
336
- logger?: (line: string) => void;
337
- repos?: string[];
338
- events?: string[];
339
- }
340
- ): { dispatched: CursorScmEvent[] } {
341
- const dispatched: CursorScmEvent[] = [];
342
- relay = new CursorEventRelay({
343
- baseUrl: "https://api2.example",
344
- apiKey: "key",
345
- repos: options?.repos ?? ["acme/repo"],
346
- events: options?.events ?? ["pull_request", "issue_comment", "push"],
347
- stateDir: dir,
348
- dispatch:
349
- options?.dispatch ??
350
- (async (e) => {
351
- dispatched.push(e);
352
- }),
353
- logger: options?.logger ?? (() => {}),
354
- fetchImpl: backend.fetchImpl,
355
- pollWaitMs: 0,
356
- });
357
- relay.start();
358
- return { dispatched };
359
- }
360
-
361
- it("registers before consuming, anchors at now, dispatches in order, persists offset and consumer id", async () => {
362
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
363
- const backend = fakeBackend({
364
- anchorOffset: "100-0",
365
- polls: [
366
- {
367
- events: [
368
- event("101-0", "pull_request"),
369
- event("102-0", "issue_comment"),
370
- ],
371
- nextOffset: "102-0",
372
- },
373
- ],
374
- });
375
- const { dispatched } = makeRelay(backend);
376
-
377
- await waitFor(() => dispatched.length === 2);
378
- expect(dispatched.map((e) => e.eventName)).toEqual([
379
- "pull_request",
380
- "issue_comment",
381
- ]);
382
- await waitFor(() =>
383
- backend.requests.some((r) => r.url.includes("after=102-0"))
384
- );
385
- await relay?.stop();
386
- relay = undefined;
387
-
388
- const store = new RelayOffsetStore(dir);
389
- expect(await store.load()).toBe("102-0");
390
-
391
- // Consumption is registration-gated: the first request is the PUT, and
392
- // no GET happens before it.
393
- expect(backend.requests[0]?.method).toBe("PUT");
394
- const registration = backend.requests[0];
395
- expect(registration?.body).toMatchObject({
396
- subscriptions: [
397
- {
398
- provider: "github",
399
- repo: "acme/repo",
400
- events: ["pull_request", "issue_comment", "push"],
401
- },
402
- ],
403
- });
404
- const consumerId = (registration?.body as { consumerId?: string })
405
- ?.consumerId;
406
- expect(typeof consumerId).toBe("string");
407
- expect(consumerId?.length).toBeGreaterThan(0);
408
- // The consumer id is durable in the state dir.
409
- expect((await readFile(join(dir, "consumer-id"), "utf8")).trim()).toBe(
410
- consumerId
411
- );
412
- // The first poll after anchoring resumes from the anchor offset.
413
- expect(
414
- backend.requests.some(
415
- (r) => r.method === "GET" && r.url.includes("after=100-0")
416
- )
417
- ).toBe(true);
418
- });
419
-
420
- it("retries registration until it succeeds before any consumption", async () => {
421
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
422
- const backend = fakeBackend({
423
- anchorOffset: "10-0",
424
- polls: [{ events: [event("11-0", "pull_request")], nextOffset: "11-0" }],
425
- registrationFailures: 1,
426
- });
427
- const { dispatched } = makeRelay(backend);
428
-
429
- await waitFor(() => dispatched.length === 1, 10_000);
430
- const firstGetIndex = backend.requests.findIndex((r) => r.method === "GET");
431
- const successfulPutIndex = backend.requests.findIndex(
432
- (r, index) => r.method === "PUT" && index > 0
433
- );
434
- // Two PUTs (one failed, one ok) happened before the first GET.
435
- expect(firstGetIndex).toBeGreaterThan(successfulPutIndex);
436
- expect(
437
- backend.requests.filter((r) => r.method === "PUT").length
438
- ).toBeGreaterThanOrEqual(2);
439
- }, 15_000);
440
-
441
- it("deregisters on clean stop so the registration does not linger by TTL", async () => {
442
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
443
- const backend = fakeBackend({ anchorOffset: "1-0" });
444
- makeRelay(backend);
445
- await waitFor(() => backend.requests.some((r) => r.method === "GET"));
446
- const consumerId = (backend.requests[0]?.body as { consumerId?: string })
447
- ?.consumerId;
448
-
449
- await relay?.stop();
450
- relay = undefined;
451
-
452
- const lastPut = backend.requests.filter((r) => r.method === "PUT").at(-1);
453
- expect(lastPut?.body).toEqual({ consumerId, subscriptions: [] });
454
- });
455
-
456
- it("does not deregister when registration never succeeded", async () => {
457
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
458
- // Registration keeps failing; stop before it ever succeeds.
459
- const backend = fakeBackend({ registrationFailures: 1_000 });
460
- makeRelay(backend);
461
- await waitFor(() => backend.requests.some((r) => r.method === "PUT"));
462
-
463
- await relay?.stop();
464
- relay = undefined;
465
-
466
- expect(
467
- backend.requests.some(
468
- (r) =>
469
- r.method === "PUT" &&
470
- Array.isArray(
471
- (r.body as { subscriptions?: unknown[] })?.subscriptions
472
- ) &&
473
- (r.body as { subscriptions: unknown[] }).subscriptions.length === 0
474
- )
475
- ).toBe(false);
476
- });
477
-
478
- it("retries a transiently failing dispatch before advancing the offset", async () => {
479
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
480
- const backend = fakeBackend({
481
- anchorOffset: "1-0",
482
- polls: [{ events: [event("2-0", "pull_request")], nextOffset: "2-0" }],
483
- });
484
- const logs: string[] = [];
485
- const delivered: CursorScmEvent[] = [];
486
- let attempts = 0;
487
- makeRelay(backend, {
488
- logger: (line) => logs.push(line),
489
- dispatch: async (e) => {
490
- attempts += 1;
491
- if (attempts === 1) {
492
- throw new Error("read ECONNRESET");
493
- }
494
- delivered.push(e);
495
- },
496
- });
497
-
498
- await waitFor(() => delivered.length === 1, 5_000);
499
- expect(attempts).toBe(2);
500
- expect(
501
- logs.some(
502
- (line) =>
503
- line.includes("dispatch attempt 1") && line.includes("retrying")
504
- )
505
- ).toBe(true);
506
- await relay?.stop();
507
- relay = undefined;
508
- const store = new RelayOffsetStore(dir);
509
- expect(await store.load()).toBe("2-0");
510
- });
511
-
512
- it("resumes from a persisted offset and dedupes replayed events", async () => {
513
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
514
- const store = new RelayOffsetStore(dir);
515
- await store.save("200-0");
516
-
517
- const backend = fakeBackend({
518
- polls: [
519
- {
520
- // 200-0 replayed (at-least-once); only 201-0 must dispatch.
521
- events: [event("200-0", "pull_request"), event("201-0", "push")],
522
- nextOffset: "201-0",
523
- },
524
- ],
525
- });
526
- const { dispatched } = makeRelay(backend);
527
-
528
- await waitFor(() => dispatched.length >= 1);
529
- // No anchor request: the persisted offset is the resume point.
530
- expect(
531
- backend.requests.some(
532
- (r) => r.method === "GET" && r.url.includes("after=%24")
533
- )
534
- ).toBe(false);
535
- expect(
536
- backend.requests.some(
537
- (r) => r.method === "GET" && r.url.includes("after=200-0")
538
- )
539
- ).toBe(true);
540
- expect(dispatched.map((e) => e.offset)).toEqual(["201-0"]);
541
- });
542
-
543
- it("skips events from sibling consumers (foreign repos and event kinds) while advancing the offset", async () => {
544
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
545
- const backend = fakeBackend({
546
- anchorOffset: "0-1",
547
- polls: [
548
- {
549
- events: [
550
- // Foreign repo: another consumer of the same key subscribed it.
551
- event("1-0", "pull_request", "other/repo"),
552
- // Foreign event kind: not declared by this host's channels.
553
- event("2-0", "workflow_run"),
554
- // Ours.
555
- event("3-0", "pull_request"),
556
- ],
557
- nextOffset: "3-0",
558
- },
559
- ],
560
- });
561
- const logs: string[] = [];
562
- const { dispatched } = makeRelay(backend, {
563
- logger: (line) => logs.push(line),
564
- });
565
-
566
- await waitFor(() => dispatched.length === 1);
567
- expect(dispatched[0]?.offset).toBe("3-0");
568
- await relay?.stop();
569
- relay = undefined;
570
- // The skipped events still advanced the durable offset.
571
- const store = new RelayOffsetStore(dir);
572
- expect(await store.load()).toBe("3-0");
573
- expect(logs.some((line) => line.includes("other/repo"))).toBe(true);
574
- });
575
-
576
- it("keeps consuming when a dispatch throws", async () => {
577
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
578
- const backend = fakeBackend({
579
- anchorOffset: "0-1",
580
- polls: [
581
- {
582
- events: [event("1-0", "pull_request"), event("2-0", "push")],
583
- nextOffset: "2-0",
584
- },
585
- ],
586
- });
587
- const dispatched: string[] = [];
588
- const logs: string[] = [];
589
- makeRelay(backend, {
590
- logger: (line) => logs.push(line),
591
- dispatch: async (e) => {
592
- if (e.eventName === "pull_request") {
593
- throw new Error("hook exploded");
594
- }
595
- dispatched.push(e.eventName);
596
- },
597
- });
598
-
599
- await waitFor(() => dispatched.length === 1);
600
- expect(dispatched).toEqual(["push"]);
601
- expect(logs.some((line) => line.includes("dispatch failed"))).toBe(true);
602
- await relay?.stop();
603
- relay = undefined;
604
- const store = new RelayOffsetStore(dir);
605
- // The failed event still advances the offset (webhook semantics: no
606
- // infinite retry on a poison delivery).
607
- expect(await store.load()).toBe("2-0");
608
- });
609
-
610
- it("logs subscription success on a clean first registration", async () => {
611
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
612
- const backend = fakeBackend({ anchorOffset: "1-0" });
613
- const logs: string[] = [];
614
- makeRelay(backend, { logger: (line) => logs.push(line) });
615
- await waitFor(() =>
616
- logs.some((line) => line.includes("subscribed to acme/repo"))
617
- );
618
- });
619
-
620
- it("reuses the persisted consumer id across restarts", async () => {
621
- dir = await mkdtemp(join(tmpdir(), "cursor-events-"));
622
- const backendA = fakeBackend({ anchorOffset: "1-0" });
623
- makeRelay(backendA);
624
- await waitFor(() => backendA.requests.some((r) => r.method === "PUT"));
625
- await relay?.stop();
626
- relay = undefined;
627
- const firstConsumerId = (
628
- backendA.requests.find((r) => r.method === "PUT")?.body as {
629
- consumerId?: string;
630
- }
631
- )?.consumerId;
632
-
633
- const backendB = fakeBackend({ anchorOffset: "1-0" });
634
- makeRelay(backendB);
635
- await waitFor(() => backendB.requests.some((r) => r.method === "PUT"));
636
- const secondConsumerId = (
637
- backendB.requests.find((r) => r.method === "PUT")?.body as {
638
- consumerId?: string;
639
- }
640
- )?.consumerId;
641
- expect(firstConsumerId).toBeDefined();
642
- expect(secondConsumerId).toBe(firstConsumerId);
643
- });
644
- });
645
-
646
- describe("serve({ cursorEvents }) sign-in requirement", () => {
647
- const savedEnv: Record<string, string | undefined> = {};
648
- let stateRoot: string | undefined;
649
- let handle: AgentServeHandle | undefined;
650
-
651
- const runner: AgentRunner = {
652
- async runTurn() {
653
- return { status: "finished", result: "unused" };
654
- },
655
- };
656
-
657
- function fixture(): AgentProject {
658
- return {
659
- rootDir: "/tmp/cursor-events-fixture",
660
- agentDir: "/tmp/cursor-events-fixture/agent",
661
- name: "relay-fixture",
662
- agent: {
663
- name: "relay-fixture",
664
- runtime: "local",
665
- tools: [],
666
- skills: [],
667
- connections: [],
668
- subagents: [],
669
- seedFiles: [],
670
- },
671
- httpChannel: httpChannel(),
672
- channels: [],
673
- schedules: [],
674
- hooks: [],
675
- abs: [],
676
- diagnostics: [],
677
- };
678
- }
679
-
680
- beforeEach(() => {
681
- for (const key of [
682
- "CURSOR_API_KEY",
683
- "CURSOR_API_BASE_URL",
684
- "AGENT_SERVE_CONFIG_DIR",
685
- ]) {
686
- savedEnv[key] = process.env[key];
687
- delete process.env[key];
688
- }
689
- // No stored login can leak in from the developer's machine.
690
- process.env.AGENT_SERVE_CONFIG_DIR = "/nonexistent-agent-serve-config";
691
- });
692
-
693
- afterEach(async () => {
694
- await handle?.close();
695
- handle = undefined;
696
- if (stateRoot !== undefined) {
697
- await rm(stateRoot, { recursive: true, force: true });
698
- stateRoot = undefined;
699
- }
700
- for (const [key, value] of Object.entries(savedEnv)) {
701
- if (value === undefined) {
702
- delete process.env[key];
703
- } else {
704
- process.env[key] = value;
705
- }
706
- }
707
- });
708
-
709
- it("refuses to serve when the host is not signed in to Cursor", async () => {
710
- const root = await mkdtemp(join(tmpdir(), "agent-serve-cursor-events-"));
711
- stateRoot = root;
712
- await expect(
713
- startServer([{ slug: "", project: fixture() }], {
714
- port: 0,
715
- stateRoot: root,
716
- playground: false,
717
- runner,
718
- cursorEvents: { repos: ["acme/repo"] },
719
- })
720
- ).rejects.toThrow(
721
- /--cursor-events[\s\S]*signed-in Cursor account[\s\S]*agentkit login/
722
- );
723
- });
724
-
725
- it("accepts CURSOR_API_KEY instead of a stored login (CI, hosted hosts)", async () => {
726
- const root = await mkdtemp(join(tmpdir(), "agent-serve-cursor-events-"));
727
- stateRoot = root;
728
- process.env.CURSOR_API_KEY = "crsr_env";
729
- const logs: string[] = [];
730
- handle = await startServer([{ slug: "", project: fixture() }], {
731
- port: 0,
732
- stateRoot: root,
733
- playground: false,
734
- runner,
735
- logger: (line) => logs.push(line),
736
- cursorEvents: { repos: ["acme/repo"] },
737
- });
738
- // Credential present, so startup proceeds. This fixture declares no github
739
- // channel, so the relay stays off (warn-only path) without polling.
740
- expect(
741
- logs.some((line) =>
742
- line.includes("no mounted agent declares a github channel")
743
- )
744
- ).toBe(true);
745
- });
746
- });