@cursor/july 0.1.20 → 0.1.21

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 (212) 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 +106 -0
  8. package/dist/channels/slack/bot-mentions.d.ts.map +1 -0
  9. package/dist/channels/slack/bot-mentions.js +243 -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 +58 -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.jDxLzWv4.js} +1 -1
  26. package/dist/docs/assets/chunks/@localSearchIndexroot.DoJHJjqF.js +1 -0
  27. package/dist/docs/assets/chunks/{VPLocalSearchBox.OMGMWH6H.js → VPLocalSearchBox.Y6bDR1-a.js} +1 -1
  28. package/dist/docs/assets/chunks/{theme.DLg4yB4D.js → theme.CLazCWlJ.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/resolved-connections.d.ts +1 -1
  78. package/dist/internal/resolved-connections.d.ts.map +1 -1
  79. package/dist/internal/resolved-connections.js +10 -0
  80. package/dist/internal/session-engine.d.ts +4 -1
  81. package/dist/internal/session-engine.d.ts.map +1 -1
  82. package/dist/internal/session-engine.js +13 -2
  83. package/dist/playground/assets/index-CjOQ4hN9.css +1 -0
  84. package/dist/playground/assets/index-dshZQJCp.js +85 -0
  85. package/dist/playground/index.html +2 -2
  86. package/dist/types.d.ts +7 -0
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js +9 -0
  89. package/package.json +24 -24
  90. package/src/bin/agent-serve.version.test.ts +62 -0
  91. package/src/channels/github/api.test.ts +64 -0
  92. package/src/channels/github/auth.test.ts +105 -0
  93. package/src/channels/github/cursor-account.test.ts +204 -0
  94. package/src/channels/github/forward.test.ts +457 -0
  95. package/src/channels/github/github.test.ts +937 -0
  96. package/src/channels/github/replay.test.ts +179 -0
  97. package/src/channels/slack/api.post-message.test.ts +148 -0
  98. package/src/channels/slack/api.ts +5 -0
  99. package/src/channels/slack/approvals.test.ts +328 -0
  100. package/src/channels/slack/block-actions.test.ts +452 -0
  101. package/src/channels/slack/bot-mentions.test.ts +217 -0
  102. package/src/channels/slack/bot-mentions.ts +315 -0
  103. package/src/channels/slack/channel-watch.test.ts +363 -0
  104. package/src/channels/slack/cursor-account.test.ts +253 -0
  105. package/src/channels/slack/defaults.final-post.test.ts +182 -0
  106. package/src/channels/slack/dispatch.test.ts +795 -0
  107. package/src/channels/slack/dispatch.ts +27 -1
  108. package/src/channels/slack/eval-directive.test.ts +273 -0
  109. package/src/channels/slack/inbound.ts +25 -4
  110. package/src/channels/slack/index.ts +1 -0
  111. package/src/channels/slack/message-body.test.ts +54 -0
  112. package/src/channels/slack/nudge-store.test.ts +143 -0
  113. package/src/channels/slack/slack-channel.ts +66 -8
  114. package/src/channels/slack/slack.test.ts +391 -0
  115. package/src/channels/slack/stop.test.ts +23 -0
  116. package/src/channels/slack/thread-context.test.ts +202 -0
  117. package/src/channels/slack/types.ts +60 -0
  118. package/src/evals/assertions.test.ts +580 -0
  119. package/src/evals/expect.test.ts +144 -0
  120. package/src/evals/judge.test.ts +181 -0
  121. package/src/evals/loaders.test.ts +132 -0
  122. package/src/evals/matchers.test.ts +95 -0
  123. package/src/evals/reporters.test.ts +303 -0
  124. package/src/evals/run-facts.test.ts +259 -0
  125. package/src/internal/ab-snapshot.test.ts +325 -0
  126. package/src/internal/approval-gate.test.ts +49 -0
  127. package/src/internal/approvals.integration.test.ts +383 -0
  128. package/src/internal/authored-loaders.test.ts +31 -0
  129. package/src/internal/builtin-tools/reminders.test.ts +201 -0
  130. package/src/internal/channel-route-schema.test.ts +294 -0
  131. package/src/internal/chat-attach.test.ts +262 -0
  132. package/src/internal/cli-deploy.test.ts +1991 -0
  133. package/src/internal/cli-mcp.test.ts +789 -0
  134. package/src/internal/cli-skills.test.ts +133 -0
  135. package/src/internal/cli-slack.test.ts +1647 -0
  136. package/src/internal/cloud-merge.test.ts +74 -0
  137. package/src/internal/cron.test.ts +22 -0
  138. package/src/internal/cursor/account-mcp.test.ts +807 -0
  139. package/src/internal/cursor/backend-client.test.ts +591 -0
  140. package/src/internal/cursor/credentials.test.ts +351 -0
  141. package/src/internal/cursor/github-credentials.test.ts +136 -0
  142. package/src/internal/cursor-account-mcp-auth.test.ts +310 -0
  143. package/src/internal/cursor-account.integration.test.ts +441 -0
  144. package/src/internal/cursor-event-relay.test.ts +746 -0
  145. package/src/internal/cursor-github-credentials.integration.test.ts +271 -0
  146. package/src/internal/cursor-slack-relay.test.ts +525 -0
  147. package/src/internal/deploy-source.test.ts +111 -0
  148. package/src/internal/discovery.builtin-tools.test.ts +94 -0
  149. package/src/internal/discovery.concurrency.test.ts +60 -0
  150. package/src/internal/discovery.cursor-account.test.ts +133 -0
  151. package/src/internal/discovery.cwd.test.ts +83 -0
  152. package/src/internal/discovery.hosting.test.ts +80 -0
  153. package/src/internal/discovery.identity.test.ts +44 -0
  154. package/src/internal/docs-site.test.ts +66 -0
  155. package/src/internal/duration.test.ts +29 -0
  156. package/src/internal/eval-judge-model.test.ts +187 -0
  157. package/src/internal/eval-run-store.cancel.test.ts +142 -0
  158. package/src/internal/eval-run-store.storage.test.ts +211 -0
  159. package/src/internal/eval-runner.http.test.ts +403 -0
  160. package/src/internal/eval-runner.run.test.ts +928 -0
  161. package/src/internal/evals-client.test.ts +307 -0
  162. package/src/internal/event-mapper.test.ts +243 -0
  163. package/src/internal/github-fanout.test.ts +213 -0
  164. package/src/internal/handleAgentServeTrigger.test.ts +179 -0
  165. package/src/internal/host-kv.test.ts +82 -0
  166. package/src/internal/host-platforms.test.ts +126 -0
  167. package/src/internal/http-channel.test.ts +402 -0
  168. package/src/internal/init-project.test.ts +269 -0
  169. package/src/internal/install-cursor-skills.test.ts +262 -0
  170. package/src/internal/local-env.test.ts +100 -0
  171. package/src/internal/log-ring.test.ts +31 -0
  172. package/src/internal/logs-client.test.ts +350 -0
  173. package/src/internal/mcp-endpoint.test.ts +436 -0
  174. package/src/internal/mcp-host.test.ts +298 -0
  175. package/src/internal/mcp-oauth.test.ts +148 -0
  176. package/src/internal/net.test.ts +17 -0
  177. package/src/internal/peer-connections.test.ts +128 -0
  178. package/src/internal/peer-mcp.integration.test.ts +289 -0
  179. package/src/internal/playground/toolchain.test.ts +53 -0
  180. package/src/internal/playground-cli.test.ts +187 -0
  181. package/src/internal/playground-proxy.test.ts +376 -0
  182. package/src/internal/prompt-context.integration.test.ts +232 -0
  183. package/src/internal/prompt-context.test.ts +127 -0
  184. package/src/internal/reminder-runner.test.ts +390 -0
  185. package/src/internal/reminder-store.test.ts +53 -0
  186. package/src/internal/request-headers.test.ts +27 -0
  187. package/src/internal/resolve-prod-target.test.ts +787 -0
  188. package/src/internal/resolved-connections.test.ts +295 -0
  189. package/src/internal/resolved-connections.ts +18 -1
  190. package/src/internal/router.test.ts +57 -0
  191. package/src/internal/sdk-runner.test.ts +290 -0
  192. package/src/internal/session-engine.coalesce.test.ts +169 -0
  193. package/src/internal/session-engine.concurrency.test.ts +250 -0
  194. package/src/internal/session-engine.host-oauth-mcp.test.ts +110 -0
  195. package/src/internal/session-engine.interrupt.test.ts +577 -0
  196. package/src/internal/session-engine.storage.test.ts +547 -0
  197. package/src/internal/session-engine.ts +15 -1
  198. package/src/internal/session-urls.test.ts +28 -0
  199. package/src/internal/sessions-client.test.ts +518 -0
  200. package/src/internal/storage-coordinator.test.ts +517 -0
  201. package/src/internal/tool-call.test.ts +458 -0
  202. package/src/internal/tool-result.test.ts +52 -0
  203. package/src/internal/trajectory.approvals.test.ts +83 -0
  204. package/src/internal/trajectory.subagents.test.ts +198 -0
  205. package/src/internal/turn-governor.test.ts +137 -0
  206. package/src/internal/update-check.test.ts +485 -0
  207. package/src/internal/workspace.test.ts +81 -0
  208. package/src/storage-backends/cursor-hosted.test.ts +121 -0
  209. package/src/types.ts +12 -0
  210. package/dist/docs/assets/chunks/@localSearchIndexroot.CvejISXF.js +0 -1
  211. package/dist/playground/assets/index-CidizGZv.css +0 -1
  212. package/dist/playground/assets/index-CuiAKZaP.js +0 -85
@@ -17,6 +17,11 @@ import type {
17
17
  SendMessageFn,
18
18
  } from "../../types.js";
19
19
  import { slackContinuationKey } from "./api.js";
20
+ import {
21
+ normalizeBotMentionAllowlist,
22
+ SLACK_BOT_MENTION_DEFAULTS,
23
+ SlackBotMentionGate,
24
+ } from "./bot-mentions.js";
20
25
  import {
21
26
  type ChannelPostWatcher,
22
27
  createChannelPostWatcher,
@@ -94,6 +99,14 @@ export function slackChannel<const Name extends string = string>(
94
99
  "slackChannel({ cursorAccount: true }) does not support engagement.channelPosts — agents must be explicitly addressed (@mention, DM, or claimed-thread reply). Channel watching is Socket Mode only."
95
100
  );
96
101
  }
102
+ // Bot-mention wake needs the app's own bot identity (auth.test) and
103
+ // Socket Mode event delivery; the Cursor relay drops bot-authored
104
+ // mentions before they reach the agent.
105
+ if (options.engagement?.botMentions !== undefined) {
106
+ throw new Error(
107
+ "slackChannel({ cursorAccount: true }) does not support engagement.botMentions — bot-to-bot mention wake is Socket Mode only (author a dedicated Slack app channel)."
108
+ );
109
+ }
97
110
  // Mentions/DMs are always routed by the backend. Disabling them
98
111
  // client-side would black-hole events the webhook already stole from
99
112
  // native Cursor handling — reject at construction instead.
@@ -156,6 +169,10 @@ export function slackChannel<const Name extends string = string>(
156
169
  );
157
170
  }
158
171
  }
172
+ const botMentions = engagement.botMentions;
173
+ if (botMentions !== undefined) {
174
+ normalizeBotMentionAllowlist(botMentions.allowFrom);
175
+ }
159
176
  const eventIds = createEventIdDedupe();
160
177
  const toolApprovals = options.toolApprovals === true;
161
178
  const interactivity = options.interactivity === true;
@@ -192,6 +209,7 @@ export function slackChannel<const Name extends string = string>(
192
209
 
193
210
  let socketHandle: SlackSocketModeHandle | null = null;
194
211
  let channelWatch: ChannelPostWatcher | undefined;
212
+ let botMentionGate: SlackBotMentionGate | undefined;
195
213
 
196
214
  function inboundDeps(args: {
197
215
  send: SendMessageFn;
@@ -232,6 +250,7 @@ export function slackChannel<const Name extends string = string>(
232
250
  ? {}
233
251
  : { interruptContinuation: args.interruptContinuation }),
234
252
  ...(channelWatch === undefined ? {} : { channelWatch }),
253
+ ...(botMentionGate === undefined ? {} : { botMentionGate }),
235
254
  };
236
255
  }
237
256
 
@@ -305,22 +324,60 @@ export function slackChannel<const Name extends string = string>(
305
324
  return;
306
325
  }
307
326
 
308
- if (channelPosts !== undefined) {
309
- const allowlist = new SlackChannelAllowlist(channelPosts.allow);
310
- let botUserId: string | undefined;
311
- let botId: string | undefined;
327
+ let botUserId: string | undefined;
328
+ let botId: string | undefined;
329
+ let webClient: InstanceType<
330
+ typeof import("@slack/web-api").WebClient
331
+ > | null = null;
332
+ if (channelPosts !== undefined || botMentions !== undefined) {
312
333
  try {
313
334
  const { WebClient } = await import("@slack/web-api");
314
- const client = new WebClient(botToken);
315
- const identity = await client.auth.test();
335
+ webClient = new WebClient(botToken);
336
+ const identity = await webClient.auth.test();
316
337
  botUserId =
317
338
  typeof identity.user_id === "string" ? identity.user_id : undefined;
318
339
  botId =
319
340
  typeof identity.bot_id === "string" ? identity.bot_id : undefined;
341
+ } catch (error) {
342
+ getLog().error("bot identity lookup degraded", {
343
+ error: error instanceof Error ? error.message : String(error),
344
+ });
345
+ }
346
+ }
347
+
348
+ if (botMentions !== undefined) {
349
+ if (botUserId === undefined && botId === undefined) {
350
+ getLog().error("bot mentions disabled", {
351
+ reason:
352
+ "engagement.botMentions requires bot identity from auth.test (loop safety)",
353
+ });
354
+ } else {
355
+ botMentionGate = new SlackBotMentionGate(
356
+ botMentions,
357
+ { botUserId, botId },
358
+ { log: getLog() }
359
+ );
360
+ getLog().info("bot mentions enabled", {
361
+ allow_from: botMentions.allowFrom.join(","),
362
+ max_depth:
363
+ botMentions.maxDepth ?? SLACK_BOT_MENTION_DEFAULTS.maxDepth,
364
+ max_per_thread_per_hour:
365
+ botMentions.maxPerThreadPerHour ??
366
+ SLACK_BOT_MENTION_DEFAULTS.maxPerThreadPerHour,
367
+ max_per_thread_total:
368
+ botMentions.maxPerThreadTotal ??
369
+ SLACK_BOT_MENTION_DEFAULTS.maxPerThreadTotal,
370
+ });
371
+ }
372
+ }
373
+
374
+ if (channelPosts !== undefined) {
375
+ const allowlist = new SlackChannelAllowlist(channelPosts.allow);
376
+ try {
320
377
  const names = allowlist.unresolvedNames();
321
- if (names.length > 0) {
378
+ if (names.length > 0 && webClient !== null) {
322
379
  allowlist.applyResolvedNames(
323
- await resolveChannelNames(client, names, getLog())
380
+ await resolveChannelNames(webClient, names, getLog())
324
381
  );
325
382
  }
326
383
  } catch (error) {
@@ -472,6 +529,7 @@ export function slackChannel<const Name extends string = string>(
472
529
  getLog().info("channel stop");
473
530
  channelWatch?.stop();
474
531
  channelWatch = undefined;
532
+ botMentionGate = undefined;
475
533
  await socketHandle?.stop();
476
534
  socketHandle = null;
477
535
  await options.onStop?.();
@@ -0,0 +1,391 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { SLACK_LOADING_MESSAGE_MAX_LEN } from "./constants.js";
3
+ import {
4
+ buildSlackManifest,
5
+ computeStreamAppend,
6
+ createSlackLogger,
7
+ credentialsFromEnv,
8
+ envPrefixFromName,
9
+ extractGithubPrRef,
10
+ parseAppMentionEvent,
11
+ parseDirectMessageEvent,
12
+ SLACK_EXTERNAL_APP_MANAGEMENT_URL,
13
+ slackChannel,
14
+ slackContinuationKey,
15
+ slackEnvKeys,
16
+ slackTextToMarkdown,
17
+ slackThreadPermalink,
18
+ truncateTypingStatus,
19
+ tryResolveAppToken,
20
+ tryResolveBotToken,
21
+ } from "./index.js";
22
+
23
+ describe("slackContinuationKey", () => {
24
+ it("joins channel and thread", () => {
25
+ expect(slackContinuationKey("C123", "1719.000")).toBe("C123:1719.000");
26
+ });
27
+ });
28
+
29
+ describe("slackThreadPermalink", () => {
30
+ it("prefers the client thread deep link when team is known", () => {
31
+ expect(
32
+ slackThreadPermalink({
33
+ teamId: "T1",
34
+ channelId: "C9",
35
+ threadTs: "1783451265.725729",
36
+ })
37
+ ).toBe("https://app.slack.com/client/T1/C9/thread/C9-1783451265.725729");
38
+ });
39
+
40
+ it("falls back to archives permalink without team", () => {
41
+ expect(
42
+ slackThreadPermalink({
43
+ channelId: "C9",
44
+ threadTs: "1783451265.725729",
45
+ })
46
+ ).toBe(
47
+ "https://app.slack.com/archives/C9/p1783451265725729?thread_ts=1783451265.725729"
48
+ );
49
+ });
50
+ });
51
+
52
+ describe("createSlackLogger", () => {
53
+ it("formats levels and fields", () => {
54
+ const lines: string[] = [];
55
+ const log = createSlackLogger((line) => lines.push(line));
56
+ log.info("inbound", { kind: "app_mention", channel: "C9" });
57
+ log.warn("chat.startStream unavailable", { error: "missing_scope" });
58
+ expect(lines[0]).toBe(
59
+ "[agentkit/slack] inbound kind=app_mention channel=C9"
60
+ );
61
+ expect(lines[1]).toBe(
62
+ "[agentkit/slack] warn chat.startStream unavailable error=missing_scope"
63
+ );
64
+ });
65
+ });
66
+
67
+ describe("inbound parsers", () => {
68
+ it("parses app_mention", () => {
69
+ const message = parseAppMentionEvent({
70
+ type: "event_callback",
71
+ team_id: "T1",
72
+ event: {
73
+ type: "app_mention",
74
+ user: "U1",
75
+ text: "<@Ubot> hi there",
76
+ ts: "1.0",
77
+ channel: "C9",
78
+ },
79
+ });
80
+ expect(message).not.toBeNull();
81
+ expect(message?.channelId).toBe("C9");
82
+ expect(message?.threadTs).toBe("1.0");
83
+ expect(message?.markdown).toContain("hi there");
84
+ expect(message?.teamId).toBe("T1");
85
+ });
86
+
87
+ it("parses DMs and drops bot messages", () => {
88
+ const dm = parseDirectMessageEvent({
89
+ type: "event_callback",
90
+ team_id: "T1",
91
+ event: {
92
+ type: "message",
93
+ channel_type: "im",
94
+ user: "U1",
95
+ text: "hello",
96
+ ts: "2.0",
97
+ channel: "D1",
98
+ },
99
+ });
100
+ expect(dm?.markdown).toBe("hello");
101
+
102
+ const bot = parseDirectMessageEvent({
103
+ type: "event_callback",
104
+ event: {
105
+ type: "message",
106
+ channel_type: "im",
107
+ bot_id: "B1",
108
+ text: "noop",
109
+ ts: "2.0",
110
+ channel: "D1",
111
+ },
112
+ });
113
+ expect(bot).toBeNull();
114
+ });
115
+
116
+ it("drops bot-authored app_mention events", () => {
117
+ expect(
118
+ parseAppMentionEvent({
119
+ type: "event_callback",
120
+ event: {
121
+ type: "app_mention",
122
+ bot_id: "B1",
123
+ text: "<@Ubot> loop",
124
+ ts: "1.0",
125
+ channel: "C9",
126
+ },
127
+ })
128
+ ).toBeNull();
129
+ });
130
+ });
131
+
132
+ describe("slackTextToMarkdown", () => {
133
+ it("strips user mentions", () => {
134
+ expect(slackTextToMarkdown("<@U123> please help")).toBe(
135
+ "@U123 please help"
136
+ );
137
+ });
138
+ });
139
+
140
+ describe("buildSlackManifest", () => {
141
+ it("enables Socket Mode without request_url", () => {
142
+ const manifest = buildSlackManifest({
143
+ name: "Weather Bot",
144
+ env: "dev",
145
+ });
146
+ expect(manifest.display_information.name).toBe("Weather Bot (dev)");
147
+ expect(manifest.features.bot_user.display_name).toContain("dev");
148
+ expect(manifest.settings.socket_mode_enabled).toBe(true);
149
+ expect(
150
+ (manifest.settings.event_subscriptions as { request_url?: string })
151
+ .request_url
152
+ ).toBeUndefined();
153
+ expect(manifest.oauth_config.scopes.bot).toContain("chat:write");
154
+ expect(manifest.settings.event_subscriptions.bot_events).toContain(
155
+ "app_mention"
156
+ );
157
+ expect(manifest.settings.interactivity.is_enabled).toBe(false);
158
+ });
159
+
160
+ it("hides tokens and Slack-side installs, pointing management at Cursor", () => {
161
+ for (const env of ["dev", "prod"] as const) {
162
+ expect(
163
+ buildSlackManifest({ name: "Weather Bot", env }).settings
164
+ .managed_app_settings
165
+ ).toEqual({
166
+ is_install_from_slack_disabled: true,
167
+ external_app_management_url: SLACK_EXTERNAL_APP_MANAGEMENT_URL,
168
+ });
169
+ }
170
+ expect(SLACK_EXTERNAL_APP_MANAGEMENT_URL).toBe(
171
+ "https://cursor.com/dashboard/deployed-agents"
172
+ );
173
+ });
174
+
175
+ it("omits managed_app_settings for hand-imported manifests", () => {
176
+ const manifest = buildSlackManifest({
177
+ name: "Weather Bot",
178
+ env: "prod",
179
+ managedByCursor: false,
180
+ });
181
+ expect("managed_app_settings" in manifest.settings).toBe(false);
182
+ });
183
+
184
+ it("pins Benny's Slack app to benny-dev with org deploy", () => {
185
+ for (const env of ["dev", "prod"] as const) {
186
+ const manifest = buildSlackManifest({
187
+ name: "Benny",
188
+ env,
189
+ channelPosts: true,
190
+ });
191
+ expect(manifest.display_information.name).toBe("benny-dev");
192
+ expect(manifest.features.bot_user.display_name).toBe("benny-dev");
193
+ expect(manifest.settings.org_deploy_enabled).toBe(true);
194
+ expect(manifest.settings.socket_mode_enabled).toBe(true);
195
+ expect(manifest.settings.token_rotation_enabled).toBe(false);
196
+ expect(manifest.settings.is_mcp_enabled).toBe(false);
197
+ }
198
+ expect(
199
+ buildSlackManifest({ name: "benny", env: "prod" }).features.bot_user
200
+ .display_name
201
+ ).toBe("benny-dev");
202
+ });
203
+
204
+ it("enables interactivity when opted in", () => {
205
+ const manifest = buildSlackManifest({
206
+ name: "Weather Bot",
207
+ env: "prod",
208
+ interactivity: true,
209
+ });
210
+ expect(manifest.settings.interactivity.is_enabled).toBe(true);
211
+ });
212
+
213
+ it("omits channel-post events by default and adds them when opted in", () => {
214
+ const defaults = buildSlackManifest({ name: "Bot", env: "dev" });
215
+ expect(defaults.settings.event_subscriptions.bot_events).not.toContain(
216
+ "message.channels"
217
+ );
218
+
219
+ const watching = buildSlackManifest({
220
+ name: "Bot",
221
+ env: "dev",
222
+ channelPosts: true,
223
+ });
224
+ expect(watching.settings.event_subscriptions.bot_events).toContain(
225
+ "message.channels"
226
+ );
227
+ expect(watching.settings.event_subscriptions.bot_events).toContain(
228
+ "message.groups"
229
+ );
230
+ });
231
+
232
+ it("defaults the description from the name and honors an explicit one", () => {
233
+ const bare = buildSlackManifest({ name: "Bot", env: "prod" });
234
+ expect(bare.display_information.description).toBe(
235
+ "Bot — an AI agent built with Cursor"
236
+ );
237
+ expect(bare.features.assistant_view?.assistant_description).toBe(
238
+ "Bot — an AI agent built with Cursor"
239
+ );
240
+
241
+ const described = buildSlackManifest({
242
+ name: "Bot",
243
+ env: "prod",
244
+ description: "Does things",
245
+ });
246
+ expect(described.display_information.description).toBe("Does things");
247
+ expect(described.features.assistant_view?.assistant_description).toBe(
248
+ "Does things"
249
+ );
250
+ });
251
+
252
+ it("keeps the default description inside Slack's 140-char bound", () => {
253
+ const manifest = buildSlackManifest({
254
+ name: "x".repeat(200),
255
+ env: "prod",
256
+ });
257
+ expect(manifest.display_information.description).toHaveLength(140);
258
+ expect(manifest.display_information.description).toContain(
259
+ "an AI agent built with Cursor"
260
+ );
261
+ });
262
+ });
263
+
264
+ describe("slackChannel engagement", () => {
265
+ it("rejects an empty channelPosts allowlist", () => {
266
+ expect(() =>
267
+ slackChannel({
268
+ engagement: { channelPosts: { allow: [" "] } },
269
+ })
270
+ ).toThrow(/allow must list at least one channel/);
271
+ });
272
+
273
+ it("surfaces watched channels in channel meta", () => {
274
+ const channel = slackChannel({
275
+ engagement: { channelPosts: { allow: ["#issues-glass", "C123"] } },
276
+ });
277
+ expect(channel.meta?.kind).toBe("slack");
278
+ expect(channel.meta?.slackChannelPosts).toEqual(["#issues-glass", "C123"]);
279
+
280
+ const plain = slackChannel();
281
+ expect(plain.meta?.slackChannelPosts).toBeUndefined();
282
+ });
283
+ });
284
+
285
+ describe("socket mode event payload", () => {
286
+ it("parses app_mention from the event body Slack delivers over Socket Mode", () => {
287
+ const message = parseAppMentionEvent({
288
+ type: "event_callback",
289
+ team_id: "T1",
290
+ event_id: "EvSOCKET1",
291
+ event: {
292
+ type: "app_mention",
293
+ user: "U1",
294
+ text: "<@Ubot> via socket hello",
295
+ ts: "9.1",
296
+ channel: "C9",
297
+ },
298
+ });
299
+ expect(message?.markdown).toContain("via socket hello");
300
+ expect(message?.channelId).toBe("C9");
301
+ });
302
+ });
303
+
304
+ describe("computeStreamAppend", () => {
305
+ it("returns full text when nothing was posted", () => {
306
+ expect(computeStreamAppend(null, "Hello")).toBe("Hello");
307
+ expect(computeStreamAppend("", "Hello")).toBe("Hello");
308
+ });
309
+
310
+ it("returns the unsent suffix", () => {
311
+ expect(computeStreamAppend("Hello", "Hello world")).toBe(" world");
312
+ });
313
+
314
+ it("returns empty when text does not extend lastPosted", () => {
315
+ expect(computeStreamAppend("Hello", "Hi there")).toBe("");
316
+ });
317
+ });
318
+
319
+ describe("truncateTypingStatus", () => {
320
+ it("keeps loading_messages under Slack's 51-char limit", () => {
321
+ const long = "a".repeat(80);
322
+ const truncated = truncateTypingStatus(long);
323
+ expect(truncated.length).toBe(SLACK_LOADING_MESSAGE_MAX_LEN);
324
+ expect(truncated.endsWith("…")).toBe(true);
325
+ });
326
+ });
327
+
328
+ describe("env prefixes", () => {
329
+ it("normalizes agent names", () => {
330
+ expect(envPrefixFromName("bugbot-cloud")).toBe("BUGBOT_CLOUD");
331
+ expect(envPrefixFromName("security-reviewer")).toBe("SECURITY_REVIEWER");
332
+ });
333
+
334
+ it("builds per-agent env keys", () => {
335
+ expect(slackEnvKeys("BUGBOT")).toEqual({
336
+ bot: "BUGBOT_SLACK_BOT_TOKEN",
337
+ app: "BUGBOT_SLACK_APP_TOKEN",
338
+ });
339
+ expect(slackEnvKeys()).toEqual({
340
+ bot: "SLACK_BOT_TOKEN",
341
+ app: "SLACK_APP_TOKEN",
342
+ });
343
+ });
344
+
345
+ it("does not fall through to shared tokens for prefixed credentials", async () => {
346
+ const previousBot = process.env.SLACK_BOT_TOKEN;
347
+ const previousApp = process.env.SLACK_APP_TOKEN;
348
+ process.env.SLACK_BOT_TOKEN = "xoxb-shared";
349
+ process.env.SLACK_APP_TOKEN = "xapp-shared";
350
+ delete process.env.BUGBOT_SLACK_BOT_TOKEN;
351
+ delete process.env.BUGBOT_SLACK_APP_TOKEN;
352
+ try {
353
+ const credentials = credentialsFromEnv("BUGBOT");
354
+ expect(await tryResolveBotToken(credentials)).toBeUndefined();
355
+ expect(await tryResolveAppToken(credentials)).toBeUndefined();
356
+ } finally {
357
+ if (previousBot === undefined) {
358
+ delete process.env.SLACK_BOT_TOKEN;
359
+ } else {
360
+ process.env.SLACK_BOT_TOKEN = previousBot;
361
+ }
362
+ if (previousApp === undefined) {
363
+ delete process.env.SLACK_APP_TOKEN;
364
+ } else {
365
+ process.env.SLACK_APP_TOKEN = previousApp;
366
+ }
367
+ }
368
+ });
369
+ });
370
+
371
+ describe("extractGithubPrRef", () => {
372
+ it("parses URLs and owner/repo#N", () => {
373
+ expect(
374
+ extractGithubPrRef("please review https://github.com/acme/widget/pull/42")
375
+ ).toEqual({
376
+ owner: "acme",
377
+ repo: "widget",
378
+ number: 42,
379
+ url: "https://github.com/acme/widget/pull/42",
380
+ label: "acme/widget#42",
381
+ });
382
+ expect(extractGithubPrRef("drive acme/widget#7 for me")).toEqual({
383
+ owner: "acme",
384
+ repo: "widget",
385
+ number: 7,
386
+ url: "https://github.com/acme/widget/pull/7",
387
+ label: "acme/widget#7",
388
+ });
389
+ expect(extractGithubPrRef("no pr here")).toBeUndefined();
390
+ });
391
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isWholeMessageStop, stripLeadingSlackMentions } from "./stop.js";
3
+
4
+ describe("isWholeMessageStop", () => {
5
+ it("matches bare stop and @mention stop", () => {
6
+ expect(isWholeMessageStop("stop")).toBe(true);
7
+ expect(isWholeMessageStop("STOP")).toBe(true);
8
+ expect(isWholeMessageStop("<@Ubot> stop")).toBe(true);
9
+ expect(isWholeMessageStop("<@Ubot> stop ")).toBe(true);
10
+ });
11
+
12
+ it("does not match embedded stop", () => {
13
+ expect(isWholeMessageStop("please stop investigating")).toBe(false);
14
+ expect(isWholeMessageStop("<@Ubot> stop the deploy")).toBe(false);
15
+ expect(isWholeMessageStop("")).toBe(false);
16
+ });
17
+ });
18
+
19
+ describe("stripLeadingSlackMentions", () => {
20
+ it("strips repeated leading mentions", () => {
21
+ expect(stripLeadingSlackMentions("<@A> <@B> stop")).toBe("stop");
22
+ });
23
+ });