@cursor/july 0.1.20 → 0.1.22

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 (216) hide show
  1. package/dist/bin/agent-serve.js +0 -0
  2. package/dist/channels/github/instrument.d.ts +20 -0
  3. package/dist/channels/github/instrument.d.ts.map +1 -0
  4. package/dist/channels/slack/api.d.ts +2 -0
  5. package/dist/channels/slack/api.d.ts.map +1 -1
  6. package/dist/channels/slack/api.js +3 -0
  7. package/dist/channels/slack/bot-mentions.d.ts +108 -0
  8. package/dist/channels/slack/bot-mentions.d.ts.map +1 -0
  9. package/dist/channels/slack/bot-mentions.js +264 -0
  10. package/dist/channels/slack/dispatch.d.ts +6 -0
  11. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  12. package/dist/channels/slack/dispatch.js +24 -3
  13. package/dist/channels/slack/inbound.d.ts +10 -1
  14. package/dist/channels/slack/inbound.d.ts.map +1 -1
  15. package/dist/channels/slack/inbound.js +12 -4
  16. package/dist/channels/slack/index.d.ts +1 -0
  17. package/dist/channels/slack/index.d.ts.map +1 -1
  18. package/dist/channels/slack/index.js +1 -0
  19. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  20. package/dist/channels/slack/slack-channel.js +61 -22
  21. package/dist/channels/slack/types.d.ts +69 -0
  22. package/dist/channels/slack/types.d.ts.map +1 -1
  23. package/dist/docs/404.html +1 -1
  24. package/dist/docs/ab.html +2 -2
  25. package/dist/docs/assets/{app.T-5ImnM3.js → app.BKNKMLp4.js} +1 -1
  26. package/dist/docs/assets/chunks/@localSearchIndexroot.YFE6WoMB.js +1 -0
  27. package/dist/docs/assets/chunks/{VPLocalSearchBox.OMGMWH6H.js → VPLocalSearchBox.sOSfEZR6.js} +1 -1
  28. package/dist/docs/assets/chunks/{theme.DLg4yB4D.js → theme.BvCHT5lj.js} +2 -2
  29. package/dist/docs/building-with-agents.html +2 -2
  30. package/dist/docs/concepts.html +2 -2
  31. package/dist/docs/deployment.html +2 -2
  32. package/dist/docs/evals.html +2 -2
  33. package/dist/docs/example-agents/approval-buddy.html +2 -2
  34. package/dist/docs/example-agents/benny.html +2 -2
  35. package/dist/docs/example-agents/bugbot.html +2 -2
  36. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  37. package/dist/docs/example-agents/codeowners-review.html +2 -2
  38. package/dist/docs/example-agents/concierge.html +2 -2
  39. package/dist/docs/example-agents/fsd.html +2 -2
  40. package/dist/docs/example-agents/index.html +2 -2
  41. package/dist/docs/example-agents/knowledge-base.html +2 -2
  42. package/dist/docs/example-agents/oncall.html +2 -2
  43. package/dist/docs/example-agents/security-reviewer.html +2 -2
  44. package/dist/docs/example-agents/slack-agent.html +2 -2
  45. package/dist/docs/example-agents/weather-agent.html +2 -2
  46. package/dist/docs/guides/agent-to-agent.html +2 -2
  47. package/dist/docs/guides/cloud-runtime.html +2 -2
  48. package/dist/docs/guides/github.html +2 -2
  49. package/dist/docs/guides/human-in-the-loop.html +2 -2
  50. package/dist/docs/guides/mcp-oauth.html +2 -2
  51. package/dist/docs/guides/slack.html +2 -2
  52. package/dist/docs/guides/webhooks.html +2 -2
  53. package/dist/docs/hillclimbing.html +2 -2
  54. package/dist/docs/index.html +2 -2
  55. package/dist/docs/quickstart.html +2 -2
  56. package/dist/docs/reference/agent-config.html +2 -2
  57. package/dist/docs/reference/channels.html +2 -2
  58. package/dist/docs/reference/cli.html +2 -2
  59. package/dist/docs/reference/connections.html +2 -2
  60. package/dist/docs/reference/hooks.html +2 -2
  61. package/dist/docs/reference/http-api.html +2 -2
  62. package/dist/docs/reference/instructions.html +2 -2
  63. package/dist/docs/reference/playground.html +2 -2
  64. package/dist/docs/reference/project-layout.html +2 -2
  65. package/dist/docs/reference/prompt.html +2 -2
  66. package/dist/docs/reference/schedules.html +2 -2
  67. package/dist/docs/reference/sessions.html +2 -2
  68. package/dist/docs/reference/skills.html +2 -2
  69. package/dist/docs/reference/subagents.html +2 -2
  70. package/dist/docs/reference/tools.html +2 -2
  71. package/dist/docs/scaffolding-agents.html +2 -2
  72. package/dist/docs/storage.html +2 -2
  73. package/dist/docs/troubleshooting.html +2 -2
  74. package/dist/internal/json-dir-store.d.ts +32 -0
  75. package/dist/internal/json-dir-store.d.ts.map +1 -0
  76. package/dist/internal/json-dir-store.js +100 -0
  77. package/dist/internal/local-env.d.ts +6 -4
  78. package/dist/internal/local-env.d.ts.map +1 -1
  79. package/dist/internal/local-env.js +14 -6
  80. package/dist/internal/resolved-connections.d.ts +1 -1
  81. package/dist/internal/resolved-connections.d.ts.map +1 -1
  82. package/dist/internal/resolved-connections.js +10 -0
  83. package/dist/internal/session-engine.d.ts +4 -1
  84. package/dist/internal/session-engine.d.ts.map +1 -1
  85. package/dist/internal/session-engine.js +13 -2
  86. package/dist/playground/assets/index-CjOQ4hN9.css +1 -0
  87. package/dist/playground/assets/index-dshZQJCp.js +85 -0
  88. package/dist/playground/index.html +2 -2
  89. package/dist/types.d.ts +7 -0
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/types.js +9 -0
  92. package/package.json +24 -24
  93. package/src/bin/agent-serve.version.test.ts +62 -0
  94. package/src/channels/github/api.test.ts +64 -0
  95. package/src/channels/github/auth.test.ts +105 -0
  96. package/src/channels/github/cursor-account.test.ts +204 -0
  97. package/src/channels/github/forward.test.ts +457 -0
  98. package/src/channels/github/github.test.ts +937 -0
  99. package/src/channels/github/replay.test.ts +179 -0
  100. package/src/channels/slack/api.post-message.test.ts +148 -0
  101. package/src/channels/slack/api.ts +5 -0
  102. package/src/channels/slack/approvals.test.ts +328 -0
  103. package/src/channels/slack/block-actions.test.ts +452 -0
  104. package/src/channels/slack/bot-mentions.test.ts +267 -0
  105. package/src/channels/slack/bot-mentions.ts +339 -0
  106. package/src/channels/slack/channel-watch.test.ts +363 -0
  107. package/src/channels/slack/cursor-account.test.ts +253 -0
  108. package/src/channels/slack/defaults.final-post.test.ts +182 -0
  109. package/src/channels/slack/dispatch.test.ts +795 -0
  110. package/src/channels/slack/dispatch.ts +27 -1
  111. package/src/channels/slack/eval-directive.test.ts +273 -0
  112. package/src/channels/slack/inbound.ts +25 -4
  113. package/src/channels/slack/index.ts +1 -0
  114. package/src/channels/slack/message-body.test.ts +54 -0
  115. package/src/channels/slack/nudge-store.test.ts +143 -0
  116. package/src/channels/slack/slack-channel.ts +66 -8
  117. package/src/channels/slack/slack.test.ts +391 -0
  118. package/src/channels/slack/stop.test.ts +23 -0
  119. package/src/channels/slack/thread-context.test.ts +202 -0
  120. package/src/channels/slack/types.ts +71 -0
  121. package/src/evals/assertions.test.ts +580 -0
  122. package/src/evals/expect.test.ts +144 -0
  123. package/src/evals/judge.test.ts +181 -0
  124. package/src/evals/loaders.test.ts +132 -0
  125. package/src/evals/matchers.test.ts +95 -0
  126. package/src/evals/reporters.test.ts +303 -0
  127. package/src/evals/run-facts.test.ts +259 -0
  128. package/src/internal/ab-snapshot.test.ts +325 -0
  129. package/src/internal/approval-gate.test.ts +49 -0
  130. package/src/internal/approvals.integration.test.ts +383 -0
  131. package/src/internal/authored-loaders.test.ts +31 -0
  132. package/src/internal/builtin-tools/reminders.test.ts +201 -0
  133. package/src/internal/channel-route-schema.test.ts +294 -0
  134. package/src/internal/chat-attach.test.ts +262 -0
  135. package/src/internal/cli-deploy.test.ts +1991 -0
  136. package/src/internal/cli-mcp.test.ts +789 -0
  137. package/src/internal/cli-skills.test.ts +133 -0
  138. package/src/internal/cli-slack.test.ts +1647 -0
  139. package/src/internal/cloud-merge.test.ts +74 -0
  140. package/src/internal/cron.test.ts +22 -0
  141. package/src/internal/cursor/account-mcp.test.ts +807 -0
  142. package/src/internal/cursor/backend-client.test.ts +591 -0
  143. package/src/internal/cursor/credentials.test.ts +351 -0
  144. package/src/internal/cursor/github-credentials.test.ts +136 -0
  145. package/src/internal/cursor-account-mcp-auth.test.ts +310 -0
  146. package/src/internal/cursor-account.integration.test.ts +441 -0
  147. package/src/internal/cursor-event-relay.test.ts +746 -0
  148. package/src/internal/cursor-github-credentials.integration.test.ts +271 -0
  149. package/src/internal/cursor-slack-relay.test.ts +525 -0
  150. package/src/internal/deploy-source.test.ts +111 -0
  151. package/src/internal/discovery.builtin-tools.test.ts +94 -0
  152. package/src/internal/discovery.concurrency.test.ts +60 -0
  153. package/src/internal/discovery.cursor-account.test.ts +133 -0
  154. package/src/internal/discovery.cwd.test.ts +83 -0
  155. package/src/internal/discovery.hosting.test.ts +80 -0
  156. package/src/internal/discovery.identity.test.ts +44 -0
  157. package/src/internal/docs-site.test.ts +66 -0
  158. package/src/internal/duration.test.ts +29 -0
  159. package/src/internal/eval-judge-model.test.ts +187 -0
  160. package/src/internal/eval-run-store.cancel.test.ts +142 -0
  161. package/src/internal/eval-run-store.storage.test.ts +211 -0
  162. package/src/internal/eval-runner.http.test.ts +403 -0
  163. package/src/internal/eval-runner.run.test.ts +928 -0
  164. package/src/internal/evals-client.test.ts +307 -0
  165. package/src/internal/event-mapper.test.ts +243 -0
  166. package/src/internal/github-fanout.test.ts +213 -0
  167. package/src/internal/handleAgentServeTrigger.test.ts +179 -0
  168. package/src/internal/host-kv.test.ts +82 -0
  169. package/src/internal/host-platforms.test.ts +126 -0
  170. package/src/internal/http-channel.test.ts +402 -0
  171. package/src/internal/init-project.test.ts +269 -0
  172. package/src/internal/install-cursor-skills.test.ts +262 -0
  173. package/src/internal/local-env.test.ts +120 -0
  174. package/src/internal/local-env.ts +14 -6
  175. package/src/internal/log-ring.test.ts +31 -0
  176. package/src/internal/logs-client.test.ts +350 -0
  177. package/src/internal/mcp-endpoint.test.ts +436 -0
  178. package/src/internal/mcp-host.test.ts +298 -0
  179. package/src/internal/mcp-oauth.test.ts +148 -0
  180. package/src/internal/net.test.ts +17 -0
  181. package/src/internal/peer-connections.test.ts +128 -0
  182. package/src/internal/peer-mcp.integration.test.ts +289 -0
  183. package/src/internal/playground/toolchain.test.ts +53 -0
  184. package/src/internal/playground-cli.test.ts +187 -0
  185. package/src/internal/playground-proxy.test.ts +376 -0
  186. package/src/internal/prompt-context.integration.test.ts +232 -0
  187. package/src/internal/prompt-context.test.ts +127 -0
  188. package/src/internal/reminder-runner.test.ts +390 -0
  189. package/src/internal/reminder-store.test.ts +53 -0
  190. package/src/internal/request-headers.test.ts +27 -0
  191. package/src/internal/resolve-prod-target.test.ts +787 -0
  192. package/src/internal/resolved-connections.test.ts +295 -0
  193. package/src/internal/resolved-connections.ts +18 -1
  194. package/src/internal/router.test.ts +57 -0
  195. package/src/internal/sdk-runner.test.ts +290 -0
  196. package/src/internal/session-engine.coalesce.test.ts +169 -0
  197. package/src/internal/session-engine.concurrency.test.ts +250 -0
  198. package/src/internal/session-engine.host-oauth-mcp.test.ts +110 -0
  199. package/src/internal/session-engine.interrupt.test.ts +577 -0
  200. package/src/internal/session-engine.storage.test.ts +547 -0
  201. package/src/internal/session-engine.ts +15 -1
  202. package/src/internal/session-urls.test.ts +28 -0
  203. package/src/internal/sessions-client.test.ts +518 -0
  204. package/src/internal/storage-coordinator.test.ts +517 -0
  205. package/src/internal/tool-call.test.ts +458 -0
  206. package/src/internal/tool-result.test.ts +52 -0
  207. package/src/internal/trajectory.approvals.test.ts +83 -0
  208. package/src/internal/trajectory.subagents.test.ts +198 -0
  209. package/src/internal/turn-governor.test.ts +137 -0
  210. package/src/internal/update-check.test.ts +485 -0
  211. package/src/internal/workspace.test.ts +81 -0
  212. package/src/storage-backends/cursor-hosted.test.ts +121 -0
  213. package/src/types.ts +12 -0
  214. package/dist/docs/assets/chunks/@localSearchIndexroot.CvejISXF.js +0 -1
  215. package/dist/playground/assets/index-CidizGZv.css +0 -1
  216. package/dist/playground/assets/index-CuiAKZaP.js +0 -85
@@ -0,0 +1,363 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ createChannelPostWatcher,
4
+ SlackChannelAllowlist,
5
+ } from "./channel-watch.js";
6
+ import {
7
+ parseChannelMessageEvent,
8
+ type SlackChannelMessageEvent,
9
+ } from "./inbound.js";
10
+ import type { SlackMessage } from "./types.js";
11
+
12
+ function channelPostBody(args: {
13
+ channel?: string;
14
+ ts?: string;
15
+ threadTs?: string;
16
+ text?: string;
17
+ subtype?: string;
18
+ botId?: string;
19
+ channelType?: string;
20
+ user?: string | null;
21
+ }): unknown {
22
+ return {
23
+ type: "event_callback",
24
+ event_id: "Ev1",
25
+ team_id: "T1",
26
+ event: {
27
+ type: "message",
28
+ channel: args.channel ?? "C123",
29
+ channel_type: args.channelType ?? "channel",
30
+ ts: args.ts ?? "100.1",
31
+ ...(args.threadTs === undefined ? {} : { thread_ts: args.threadTs }),
32
+ text: args.text ?? "new bug report",
33
+ ...(args.user === null ? {} : { user: args.user ?? "U1" }),
34
+ ...(args.subtype === undefined ? {} : { subtype: args.subtype }),
35
+ ...(args.botId === undefined ? {} : { bot_id: args.botId }),
36
+ },
37
+ };
38
+ }
39
+
40
+ describe("parseChannelMessageEvent", () => {
41
+ it("parses a top-level channel post", () => {
42
+ const parsed = parseChannelMessageEvent(channelPostBody({}));
43
+ expect(parsed).not.toBeNull();
44
+ expect(parsed?.kind).toBe("post");
45
+ if (parsed?.kind === "post") {
46
+ expect(parsed.message.channelId).toBe("C123");
47
+ expect(parsed.message.ts).toBe("100.1");
48
+ expect(parsed.message.threadTs).toBe("100.1");
49
+ }
50
+ });
51
+
52
+ it("parses private-channel (group) posts", () => {
53
+ const parsed = parseChannelMessageEvent(
54
+ channelPostBody({ channel: "G9", channelType: "group" })
55
+ );
56
+ expect(parsed?.kind).toBe("post");
57
+ });
58
+
59
+ it("drops bot-authored posts", () => {
60
+ expect(
61
+ parseChannelMessageEvent(channelPostBody({ botId: "B77" }))
62
+ ).toBeNull();
63
+ });
64
+
65
+ it("keeps bot-authored posts when includeBotAuthored is set", () => {
66
+ const parsed = parseChannelMessageEvent(channelPostBody({ botId: "B77" }), {
67
+ includeBotAuthored: true,
68
+ });
69
+ expect(parsed?.kind).toBe("post");
70
+ if (parsed?.kind === "post") {
71
+ expect(parsed.message.author?.isBot).toBe(true);
72
+ expect(parsed.message.author?.botId).toBe("B77");
73
+ }
74
+ });
75
+
76
+ it("keeps userless bot_message webhook posts when includeBotAuthored is set", () => {
77
+ const body = channelPostBody({
78
+ botId: "B77",
79
+ subtype: "bot_message",
80
+ user: null,
81
+ text: "PagerDuty: high error rate on api",
82
+ });
83
+ expect(parseChannelMessageEvent(body)).toBeNull();
84
+ const parsed = parseChannelMessageEvent(body, {
85
+ includeBotAuthored: true,
86
+ });
87
+ expect(parsed?.kind).toBe("post");
88
+ if (parsed?.kind === "post") {
89
+ expect(parsed.message.author).toEqual({
90
+ userId: "",
91
+ isBot: true,
92
+ botId: "B77",
93
+ });
94
+ }
95
+ });
96
+
97
+ it("recovers alert content from attachments when top-level text is empty", () => {
98
+ const parsed = parseChannelMessageEvent(
99
+ {
100
+ type: "event_callback",
101
+ team_id: "T1",
102
+ event: {
103
+ type: "message",
104
+ subtype: "bot_message",
105
+ channel: "C123",
106
+ channel_type: "channel",
107
+ ts: "100.1",
108
+ bot_id: "B77",
109
+ username: "Datadog",
110
+ text: "",
111
+ attachments: [
112
+ {
113
+ title:
114
+ "Triggered: [service-monitor prod:temporal-bugbot] High cache error rate",
115
+ title_link: "https://us5.datadoghq.com/monitors/19610160",
116
+ text: "Service temporal-bugbot has a high cache error rate.",
117
+ },
118
+ ],
119
+ },
120
+ },
121
+ { includeBotAuthored: true }
122
+ );
123
+ expect(parsed?.kind).toBe("post");
124
+ if (parsed?.kind === "post") {
125
+ expect(parsed.message.text).toBe("");
126
+ expect(parsed.message.body).toContain("temporal-bugbot");
127
+ // Raw body keeps the monitor link; markdown is the model-facing view.
128
+ expect(parsed.message.body).toContain(
129
+ "https://us5.datadoghq.com/monitors/19610160"
130
+ );
131
+ expect(parsed.message.markdown).toContain("High cache error rate");
132
+ }
133
+ });
134
+
135
+ it("drops DMs and unknown subtypes", () => {
136
+ expect(
137
+ parseChannelMessageEvent(
138
+ channelPostBody({ channel: "D5", channelType: "im" })
139
+ )
140
+ ).toBeNull();
141
+ expect(
142
+ parseChannelMessageEvent(channelPostBody({ subtype: "channel_join" }))
143
+ ).toBeNull();
144
+ });
145
+
146
+ it("parses message_changed into an edit with the new text", () => {
147
+ const parsed = parseChannelMessageEvent({
148
+ type: "event_callback",
149
+ event: {
150
+ type: "message",
151
+ subtype: "message_changed",
152
+ channel: "C123",
153
+ channel_type: "channel",
154
+ ts: "101.0",
155
+ message: { type: "message", ts: "100.1", text: "edited!", user: "U1" },
156
+ },
157
+ });
158
+ expect(parsed?.kind).toBe("edited");
159
+ if (parsed?.kind === "edited") {
160
+ expect(parsed.message.ts).toBe("100.1");
161
+ expect(parsed.message.text).toBe("edited!");
162
+ }
163
+ });
164
+
165
+ it("parses message_deleted into a deletion", () => {
166
+ const parsed = parseChannelMessageEvent({
167
+ type: "event_callback",
168
+ event: {
169
+ type: "message",
170
+ subtype: "message_deleted",
171
+ channel: "C123",
172
+ channel_type: "channel",
173
+ ts: "101.0",
174
+ deleted_ts: "100.1",
175
+ },
176
+ });
177
+ expect(parsed).toEqual({ kind: "deleted", channelId: "C123", ts: "100.1" });
178
+ });
179
+ });
180
+
181
+ describe("SlackChannelAllowlist", () => {
182
+ it("matches ids directly and names after resolution", () => {
183
+ const allowlist = new SlackChannelAllowlist(["C123", "#issues-glass"]);
184
+ expect(allowlist.matches("C123")).toBe(true);
185
+ expect(allowlist.matches("C999")).toBe(false);
186
+ expect(allowlist.unresolvedNames()).toEqual(["issues-glass"]);
187
+ allowlist.applyResolvedNames(new Map([["issues-glass", "C999"]]));
188
+ expect(allowlist.matches("C999")).toBe(true);
189
+ });
190
+
191
+ it("treats bare names as names, not ids", () => {
192
+ const allowlist = new SlackChannelAllowlist(["issues-glass"]);
193
+ expect(allowlist.unresolvedNames()).toEqual(["issues-glass"]);
194
+ });
195
+ });
196
+
197
+ describe("createChannelPostWatcher", () => {
198
+ const dispatched: Array<{ message: SlackMessage; eventId?: string }> = [];
199
+
200
+ function watcher(args?: {
201
+ posts?: "top-level" | "all";
202
+ debounceMs?: number;
203
+ includeBotPosts?: boolean;
204
+ botUserId?: string;
205
+ botId?: string;
206
+ allow?: string[];
207
+ }) {
208
+ return createChannelPostWatcher({
209
+ options: {
210
+ allow: args?.allow ?? ["C123"],
211
+ ...(args?.posts === undefined ? {} : { posts: args.posts }),
212
+ ...(args?.debounceMs === undefined
213
+ ? {}
214
+ : { debounceMs: args.debounceMs }),
215
+ ...(args?.includeBotPosts === undefined
216
+ ? {}
217
+ : { includeBotPosts: args.includeBotPosts }),
218
+ },
219
+ allowlist: new SlackChannelAllowlist(args?.allow ?? ["C123"]),
220
+ getBotUserId: () => args?.botUserId,
221
+ getBotId: () => args?.botId,
222
+ dispatch: (message, eventId) =>
223
+ dispatched.push(
224
+ eventId === undefined ? { message } : { message, eventId }
225
+ ),
226
+ });
227
+ }
228
+
229
+ function post(args: {
230
+ channel?: string;
231
+ ts?: string;
232
+ threadTs?: string;
233
+ text?: string;
234
+ subtype?: string;
235
+ botId?: string;
236
+ user?: string | null;
237
+ }): SlackChannelMessageEvent {
238
+ const parsed = parseChannelMessageEvent(channelPostBody(args), {
239
+ includeBotAuthored: true,
240
+ });
241
+ if (parsed === null) {
242
+ throw new Error("fixture did not parse");
243
+ }
244
+ return parsed;
245
+ }
246
+
247
+ beforeEach(() => {
248
+ dispatched.length = 0;
249
+ });
250
+ afterEach(() => {
251
+ vi.useRealTimers();
252
+ });
253
+
254
+ it("dispatches allowlisted top-level posts immediately by default", () => {
255
+ const w = watcher();
256
+ expect(w.handle(post({}), "Ev1")).toBe(true);
257
+ expect(dispatched).toHaveLength(1);
258
+ expect(dispatched[0]?.eventId).toBe("Ev1");
259
+ });
260
+
261
+ it("skips channels outside the allowlist", () => {
262
+ const w = watcher();
263
+ w.handle(post({ channel: "C999" }));
264
+ expect(dispatched).toHaveLength(0);
265
+ });
266
+
267
+ it("skips thread replies unless posts is 'all'", () => {
268
+ const w = watcher();
269
+ w.handle(post({ ts: "100.2", threadTs: "100.1" }));
270
+ expect(dispatched).toHaveLength(0);
271
+
272
+ const all = watcher({ posts: "all" });
273
+ all.handle(post({ ts: "100.2", threadTs: "100.1" }));
274
+ expect(dispatched).toHaveLength(1);
275
+ });
276
+
277
+ it("skips posts that mention the bot (app_mention path owns them)", () => {
278
+ const w = watcher({ botUserId: "UBOT" });
279
+ w.handle(post({ text: "<@UBOT> please triage" }));
280
+ expect(dispatched).toHaveLength(0);
281
+ });
282
+
283
+ it("skips bot-authored posts unless includeBotPosts is set", () => {
284
+ const w = watcher();
285
+ w.handle(post({ botId: "B77", user: null, subtype: "bot_message" }));
286
+ expect(dispatched).toHaveLength(0);
287
+
288
+ const withBots = watcher({
289
+ includeBotPosts: true,
290
+ botId: "BSELF",
291
+ botUserId: "UBOT",
292
+ });
293
+ withBots.handle(post({ botId: "B77", user: null, subtype: "bot_message" }));
294
+ expect(dispatched).toHaveLength(1);
295
+ expect(dispatched[0]?.message.author?.botId).toBe("B77");
296
+ });
297
+
298
+ it("skips bot-authored posts when includeBotPosts is on but bot identity is unknown", () => {
299
+ const w = watcher({ includeBotPosts: true });
300
+ w.handle(post({ botId: "B77", user: null, subtype: "bot_message" }));
301
+ expect(dispatched).toHaveLength(0);
302
+ });
303
+
304
+ it("always skips the watching bot's own posts (loop safety)", () => {
305
+ const w = watcher({
306
+ includeBotPosts: true,
307
+ botUserId: "UBOT",
308
+ botId: "BSELF",
309
+ });
310
+ w.handle(post({ botId: "BSELF", user: null, subtype: "bot_message" }));
311
+ expect(dispatched).toHaveLength(0);
312
+
313
+ // Same app posting with its bot user id attached.
314
+ w.handle(post({ botId: "B99", user: "UBOT" }));
315
+ expect(dispatched).toHaveLength(0);
316
+
317
+ // A different bot still dispatches.
318
+ w.handle(post({ botId: "B77", user: null, subtype: "bot_message" }));
319
+ expect(dispatched).toHaveLength(1);
320
+ });
321
+
322
+ it("debounces: edits update the pending text, deletes cancel", () => {
323
+ vi.useFakeTimers();
324
+ const w = watcher({ debounceMs: 1_000 });
325
+
326
+ w.handle(post({ ts: "100.1", text: "first" }));
327
+ expect(dispatched).toHaveLength(0);
328
+ const edit = parseChannelMessageEvent({
329
+ type: "event_callback",
330
+ event: {
331
+ type: "message",
332
+ subtype: "message_changed",
333
+ channel: "C123",
334
+ channel_type: "channel",
335
+ ts: "101.0",
336
+ message: { type: "message", ts: "100.1", text: "edited", user: "U1" },
337
+ },
338
+ });
339
+ if (edit === null) {
340
+ throw new Error("edit fixture did not parse");
341
+ }
342
+ w.handle(edit);
343
+ vi.advanceTimersByTime(1_000);
344
+ expect(dispatched).toHaveLength(1);
345
+ expect(dispatched[0]?.message.text).toBe("edited");
346
+
347
+ w.handle(post({ ts: "200.1", text: "will be deleted" }));
348
+ w.handle({ kind: "deleted", channelId: "C123", ts: "200.1" });
349
+ vi.advanceTimersByTime(5_000);
350
+ expect(dispatched).toHaveLength(1);
351
+
352
+ w.stop();
353
+ });
354
+
355
+ it("stop() cancels pending dispatches", () => {
356
+ vi.useFakeTimers();
357
+ const w = watcher({ debounceMs: 1_000 });
358
+ w.handle(post({}));
359
+ w.stop();
360
+ vi.advanceTimersByTime(5_000);
361
+ expect(dispatched).toHaveLength(0);
362
+ });
363
+ });
@@ -0,0 +1,253 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { HostContext, SendMessageFn } from "../../types.js";
3
+ import {
4
+ type CursorSlackEnvelope,
5
+ createPendingSlackApiTransport,
6
+ dispatchCursorSlackEnvelope,
7
+ getSlackCursorAccountRuntime,
8
+ } from "./cursor-account.js";
9
+ import { slackChannel } from "./slack-channel.js";
10
+
11
+ function envelope(
12
+ overrides: Partial<CursorSlackEnvelope> = {}
13
+ ): CursorSlackEnvelope {
14
+ return {
15
+ eventName: "app_mention",
16
+ slackTeamId: "T0TEST",
17
+ channelId: "C0100",
18
+ channelType: "channel",
19
+ threadTs: "111.222",
20
+ messageTs: "111.222",
21
+ slackUserId: "U0AAA",
22
+ text: "benny hello <@U0BBB>",
23
+ agentName: "Benny",
24
+ consumerId: "consumer-1",
25
+ matchedBy: "name",
26
+ eventId: "Ev1",
27
+ receivedAtMs: Date.now(),
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ function fakeHost(): HostContext {
33
+ return {} as HostContext;
34
+ }
35
+
36
+ describe("slackChannel({ cursorAccount: true })", () => {
37
+ it("registers a runtime with a pending transport and no Socket Mode routes", () => {
38
+ const definition = slackChannel({ cursorAccount: true });
39
+ const runtime = getSlackCursorAccountRuntime(definition);
40
+ expect(runtime).toBeDefined();
41
+ expect(runtime?.transport.kind).toBe("api-transport");
42
+ expect(definition.meta?.slackCursorAccount).toBe(true);
43
+ // Plain socket-mode channels register nothing.
44
+ expect(
45
+ getSlackCursorAccountRuntime(slackChannel({ envPrefix: "X" }))
46
+ ).toBeUndefined();
47
+ });
48
+
49
+ it("rejects mixing cursorAccount with a dedicated Slack app credentials", () => {
50
+ expect(() =>
51
+ slackChannel({ cursorAccount: true, envPrefix: "BUGBOT" })
52
+ ).toThrow(/do not also set credentials\/envPrefix/);
53
+ expect(() =>
54
+ slackChannel({
55
+ cursorAccount: true,
56
+ credentials: { botToken: "xoxb-x" },
57
+ })
58
+ ).toThrow(/do not also set credentials\/envPrefix/);
59
+ });
60
+
61
+ it("rejects channelPosts, toolApprovals, interactivity, and mention/DM off", () => {
62
+ expect(() =>
63
+ slackChannel({
64
+ cursorAccount: true,
65
+ engagement: { channelPosts: { allow: ["C0TEST"] } },
66
+ })
67
+ ).toThrow(/channelPosts/);
68
+ // Approvals ride Socket Mode interactive envelopes; there is no relay
69
+ // tap over the Cursor connection, so both toggles reject at construction.
70
+ expect(() =>
71
+ slackChannel({
72
+ cursorAccount: true,
73
+ toolApprovals: true,
74
+ })
75
+ ).toThrow(/toolApprovals/);
76
+ expect(() =>
77
+ slackChannel({
78
+ cursorAccount: true,
79
+ interactivity: true,
80
+ })
81
+ ).toThrow(/interactivity/);
82
+ expect(() =>
83
+ slackChannel({
84
+ cursorAccount: true,
85
+ onBlockAction: () => null,
86
+ })
87
+ ).toThrow(/onBlockAction/);
88
+ expect(() =>
89
+ slackChannel({
90
+ cursorAccount: true,
91
+ engagement: { mentions: false },
92
+ })
93
+ ).toThrow(/mentions/);
94
+ expect(() =>
95
+ slackChannel({
96
+ cursorAccount: true,
97
+ engagement: { directMessages: false },
98
+ })
99
+ ).toThrow(/directMessages/);
100
+ });
101
+
102
+ it("rejects agentName values that contain whitespace", () => {
103
+ expect(() =>
104
+ slackChannel({
105
+ cursorAccount: true,
106
+ // Cast: string literals with spaces are a type error; runtime still
107
+ // guards dynamic / widened strings.
108
+ agentName: "Approval Buddy" as string,
109
+ })
110
+ ).toThrow(/single token with no whitespace/);
111
+ });
112
+
113
+ it("calls through the transport once bound, soft-failing before", async () => {
114
+ const transport = createPendingSlackApiTransport();
115
+ expect(await transport.call("chat.postMessage", {})).toEqual({
116
+ ok: false,
117
+ error: "cursor_slack_relay_not_started",
118
+ });
119
+ const calls: Array<{ method: string; args: Record<string, unknown> }> = [];
120
+ transport.bind(async (method, args) => {
121
+ calls.push({ method, args });
122
+ return { ok: true, ts: "1.2" };
123
+ });
124
+ expect(await transport.call("chat.postMessage", { channel: "C1" })).toEqual(
125
+ { ok: true, ts: "1.2" }
126
+ );
127
+ expect(calls).toEqual([
128
+ { method: "chat.postMessage", args: { channel: "C1" } },
129
+ ]);
130
+ });
131
+ });
132
+
133
+ describe("dispatchCursorSlackEnvelope", () => {
134
+ function setup(options?: Parameters<typeof slackChannel>[0]) {
135
+ const definition = slackChannel({
136
+ cursorAccount: true,
137
+ agentName: "Benny",
138
+ ...options,
139
+ });
140
+ const runtime = getSlackCursorAccountRuntime(definition);
141
+ if (runtime === undefined) {
142
+ throw new Error("runtime not registered");
143
+ }
144
+ const apiCalls: Array<{ method: string; args: Record<string, unknown> }> =
145
+ [];
146
+ runtime.transport.bind(async (method, args) => {
147
+ apiCalls.push({ method, args });
148
+ return { ok: true, ts: "999.111" };
149
+ });
150
+ const sent: Array<{ message: string; options: any }> = [];
151
+ const send: SendMessageFn = async (message, sendOptions) => {
152
+ sent.push({ message, options: sendOptions });
153
+ return {
154
+ id: "ses_1",
155
+ isNew: true,
156
+ channelId: "slack",
157
+ continuationToken: "next",
158
+ } as any;
159
+ };
160
+ return { runtime, apiCalls, sent, send };
161
+ }
162
+
163
+ it("dispatches a mention into a thread-keyed session and proxies thread setup calls", async () => {
164
+ const { runtime, apiCalls, sent, send } = setup();
165
+ await dispatchCursorSlackEnvelope({
166
+ runtime,
167
+ envelope: envelope(),
168
+ send,
169
+ host: fakeHost(),
170
+ });
171
+
172
+ expect(sent).toHaveLength(1);
173
+ expect(sent[0].options.continuationToken).toBe("C0100:111.222");
174
+ expect(sent[0].options.auth).toMatchObject({
175
+ authenticator: "slack",
176
+ principalId: "slack:T0TEST:C0100:111.222",
177
+ });
178
+ expect(sent[0].message).toContain("benny hello @U0BBB");
179
+ expect(sent[0].message).toContain("<slack_context>");
180
+
181
+ // Root message: title + typing go through the bound transport.
182
+ const methods = apiCalls.map((call) => call.method);
183
+ expect(methods).toContain("assistant.threads.setTitle");
184
+ expect(methods).toContain("assistant.threads.setStatus");
185
+ });
186
+
187
+ it("routes DMs to the direct-message handler", async () => {
188
+ const kinds: string[] = [];
189
+ const { runtime, send } = setup({
190
+ onAppMention: (ctx, message) => {
191
+ kinds.push("mention");
192
+ return null;
193
+ },
194
+ onDirectMessage: (ctx, message) => {
195
+ kinds.push("dm");
196
+ return null;
197
+ },
198
+ });
199
+ await dispatchCursorSlackEnvelope({
200
+ runtime,
201
+ envelope: envelope({
202
+ channelType: "im",
203
+ channelId: "D0200",
204
+ eventId: "Ev2",
205
+ }),
206
+ send,
207
+ host: fakeHost(),
208
+ });
209
+ await dispatchCursorSlackEnvelope({
210
+ runtime,
211
+ envelope: envelope({ eventId: "Ev3" }),
212
+ send,
213
+ host: fakeHost(),
214
+ });
215
+ expect(kinds).toEqual(["dm", "mention"]);
216
+ });
217
+
218
+ it("dedupes by Slack event id", async () => {
219
+ const { runtime, sent, send } = setup();
220
+ await dispatchCursorSlackEnvelope({
221
+ runtime,
222
+ envelope: envelope({ eventId: "EvDup" }),
223
+ send,
224
+ host: fakeHost(),
225
+ });
226
+ await dispatchCursorSlackEnvelope({
227
+ runtime,
228
+ envelope: envelope({ eventId: "EvDup" }),
229
+ send,
230
+ host: fakeHost(),
231
+ });
232
+ expect(sent).toHaveLength(1);
233
+ });
234
+
235
+ it("claimed-thread replies dispatch as mention-kind follow-ups on the same session key", async () => {
236
+ const { runtime, sent, send } = setup();
237
+ await dispatchCursorSlackEnvelope({
238
+ runtime,
239
+ envelope: envelope({
240
+ eventName: "message",
241
+ matchedBy: "thread",
242
+ messageTs: "111.333",
243
+ text: "and tomorrow?",
244
+ eventId: "Ev4",
245
+ }),
246
+ send,
247
+ host: fakeHost(),
248
+ });
249
+ expect(sent).toHaveLength(1);
250
+ expect(sent[0].options.continuationToken).toBe("C0100:111.222");
251
+ expect(sent[0].message).toContain("and tomorrow?");
252
+ });
253
+ });