@copilotkit/channels-slack 0.1.0

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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/dist/__tests__/assistant.test.d.ts +2 -0
  4. package/dist/__tests__/assistant.test.d.ts.map +1 -0
  5. package/dist/__tests__/assistant.test.js +153 -0
  6. package/dist/__tests__/auto-close-streaming.test.d.ts +2 -0
  7. package/dist/__tests__/auto-close-streaming.test.d.ts.map +1 -0
  8. package/dist/__tests__/auto-close-streaming.test.js +139 -0
  9. package/dist/__tests__/built-ins.test.d.ts +2 -0
  10. package/dist/__tests__/built-ins.test.d.ts.map +1 -0
  11. package/dist/__tests__/built-ins.test.js +67 -0
  12. package/dist/__tests__/chunked-message-stream.test.d.ts +2 -0
  13. package/dist/__tests__/chunked-message-stream.test.d.ts.map +1 -0
  14. package/dist/__tests__/chunked-message-stream.test.js +226 -0
  15. package/dist/__tests__/conversation-store.test.d.ts +2 -0
  16. package/dist/__tests__/conversation-store.test.d.ts.map +1 -0
  17. package/dist/__tests__/conversation-store.test.js +209 -0
  18. package/dist/__tests__/download-files.test.d.ts +2 -0
  19. package/dist/__tests__/download-files.test.d.ts.map +1 -0
  20. package/dist/__tests__/download-files.test.js +142 -0
  21. package/dist/__tests__/ephemeral.test.d.ts +2 -0
  22. package/dist/__tests__/ephemeral.test.d.ts.map +1 -0
  23. package/dist/__tests__/ephemeral.test.js +85 -0
  24. package/dist/__tests__/event-renderer.test.d.ts +2 -0
  25. package/dist/__tests__/event-renderer.test.d.ts.map +1 -0
  26. package/dist/__tests__/event-renderer.test.js +521 -0
  27. package/dist/__tests__/markdown-to-mrkdwn.test.d.ts +2 -0
  28. package/dist/__tests__/markdown-to-mrkdwn.test.d.ts.map +1 -0
  29. package/dist/__tests__/markdown-to-mrkdwn.test.js +80 -0
  30. package/dist/__tests__/message-stream.test.d.ts +2 -0
  31. package/dist/__tests__/message-stream.test.d.ts.map +1 -0
  32. package/dist/__tests__/message-stream.test.js +116 -0
  33. package/dist/__tests__/native-renderer.test.d.ts +2 -0
  34. package/dist/__tests__/native-renderer.test.d.ts.map +1 -0
  35. package/dist/__tests__/native-renderer.test.js +236 -0
  36. package/dist/__tests__/native-stream.test.d.ts +2 -0
  37. package/dist/__tests__/native-stream.test.d.ts.map +1 -0
  38. package/dist/__tests__/native-stream.test.js +235 -0
  39. package/dist/__tests__/reactions.test.d.ts +2 -0
  40. package/dist/__tests__/reactions.test.d.ts.map +1 -0
  41. package/dist/__tests__/reactions.test.js +193 -0
  42. package/dist/__tests__/slack-listener.test.d.ts +2 -0
  43. package/dist/__tests__/slack-listener.test.d.ts.map +1 -0
  44. package/dist/__tests__/slack-listener.test.js +559 -0
  45. package/dist/__tests__/views.test.d.ts +2 -0
  46. package/dist/__tests__/views.test.d.ts.map +1 -0
  47. package/dist/__tests__/views.test.js +143 -0
  48. package/dist/adapter.d.ts +188 -0
  49. package/dist/adapter.d.ts.map +1 -0
  50. package/dist/adapter.js +859 -0
  51. package/dist/adapter.test.d.ts +2 -0
  52. package/dist/adapter.test.d.ts.map +1 -0
  53. package/dist/adapter.test.js +368 -0
  54. package/dist/assistant.d.ts +19 -0
  55. package/dist/assistant.d.ts.map +1 -0
  56. package/dist/assistant.js +124 -0
  57. package/dist/auto-close-streaming.d.ts +62 -0
  58. package/dist/auto-close-streaming.d.ts.map +1 -0
  59. package/dist/auto-close-streaming.js +273 -0
  60. package/dist/built-in-context.d.ts +22 -0
  61. package/dist/built-in-context.d.ts.map +1 -0
  62. package/dist/built-in-context.js +65 -0
  63. package/dist/built-in-tools.d.ts +23 -0
  64. package/dist/built-in-tools.d.ts.map +1 -0
  65. package/dist/built-in-tools.js +45 -0
  66. package/dist/chunked-message-stream.d.ts +67 -0
  67. package/dist/chunked-message-stream.d.ts.map +1 -0
  68. package/dist/chunked-message-stream.js +134 -0
  69. package/dist/codec.d.ts +18 -0
  70. package/dist/codec.d.ts.map +1 -0
  71. package/dist/codec.js +18 -0
  72. package/dist/codec.test.d.ts +2 -0
  73. package/dist/codec.test.d.ts.map +1 -0
  74. package/dist/codec.test.js +19 -0
  75. package/dist/conversation-store.d.ts +92 -0
  76. package/dist/conversation-store.d.ts.map +1 -0
  77. package/dist/conversation-store.js +196 -0
  78. package/dist/download-files.d.ts +59 -0
  79. package/dist/download-files.d.ts.map +1 -0
  80. package/dist/download-files.js +116 -0
  81. package/dist/event-renderer.d.ts +98 -0
  82. package/dist/event-renderer.d.ts.map +1 -0
  83. package/dist/event-renderer.js +493 -0
  84. package/dist/index.d.ts +34 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +23 -0
  87. package/dist/ingress-normalize.d.ts +75 -0
  88. package/dist/ingress-normalize.d.ts.map +1 -0
  89. package/dist/ingress-normalize.js +153 -0
  90. package/dist/ingress-normalize.test.d.ts +2 -0
  91. package/dist/ingress-normalize.test.d.ts.map +1 -0
  92. package/dist/ingress-normalize.test.js +187 -0
  93. package/dist/interaction.d.ts +30 -0
  94. package/dist/interaction.d.ts.map +1 -0
  95. package/dist/interaction.js +219 -0
  96. package/dist/interaction.test.d.ts +2 -0
  97. package/dist/interaction.test.d.ts.map +1 -0
  98. package/dist/interaction.test.js +137 -0
  99. package/dist/markdown-to-mrkdwn.d.ts +21 -0
  100. package/dist/markdown-to-mrkdwn.d.ts.map +1 -0
  101. package/dist/markdown-to-mrkdwn.js +99 -0
  102. package/dist/message-stream.d.ts +44 -0
  103. package/dist/message-stream.d.ts.map +1 -0
  104. package/dist/message-stream.js +70 -0
  105. package/dist/native-stream.d.ts +131 -0
  106. package/dist/native-stream.d.ts.map +1 -0
  107. package/dist/native-stream.js +208 -0
  108. package/dist/render/block-kit.d.ts +35 -0
  109. package/dist/render/block-kit.d.ts.map +1 -0
  110. package/dist/render/block-kit.js +399 -0
  111. package/dist/render/block-kit.test.d.ts +2 -0
  112. package/dist/render/block-kit.test.d.ts.map +1 -0
  113. package/dist/render/block-kit.test.js +289 -0
  114. package/dist/render/budget.d.ts +24 -0
  115. package/dist/render/budget.d.ts.map +1 -0
  116. package/dist/render/budget.js +30 -0
  117. package/dist/render/budget.test.d.ts +2 -0
  118. package/dist/render/budget.test.d.ts.map +1 -0
  119. package/dist/render/budget.test.js +49 -0
  120. package/dist/render/modal.d.ts +13 -0
  121. package/dist/render/modal.d.ts.map +1 -0
  122. package/dist/render/modal.js +108 -0
  123. package/dist/render/transport.d.ts +42 -0
  124. package/dist/render/transport.d.ts.map +1 -0
  125. package/dist/render/transport.js +1 -0
  126. package/dist/render.d.ts +23 -0
  127. package/dist/render.d.ts.map +1 -0
  128. package/dist/render.js +20 -0
  129. package/dist/sanitizing-http-agent.d.ts +28 -0
  130. package/dist/sanitizing-http-agent.d.ts.map +1 -0
  131. package/dist/sanitizing-http-agent.js +51 -0
  132. package/dist/slack-listener.d.ts +73 -0
  133. package/dist/slack-listener.d.ts.map +1 -0
  134. package/dist/slack-listener.js +126 -0
  135. package/dist/slack-listener.test.d.ts +2 -0
  136. package/dist/slack-listener.test.d.ts.map +1 -0
  137. package/dist/slack-listener.test.js +115 -0
  138. package/dist/types.d.ts +153 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +20 -0
  141. package/package.json +73 -0
@@ -0,0 +1,559 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { attachSlackListener } from "../slack-listener.js";
3
+ import { DM_SCOPE, resolveSlackRespondToOptions } from "../types.js";
4
+ const BOT_USER_ID = "UBOT0001";
5
+ function makeFakeApp() {
6
+ let mention;
7
+ let message;
8
+ let command;
9
+ const app = {
10
+ event(name, handler) {
11
+ if (name === "app_mention")
12
+ mention = handler;
13
+ },
14
+ message(handler) {
15
+ message = handler;
16
+ },
17
+ command(_name, handler) {
18
+ command = handler;
19
+ },
20
+ };
21
+ const client = {};
22
+ return {
23
+ app: app,
24
+ client,
25
+ fireMention: (event) => mention?.({ event, client }),
26
+ fireMessage: (m) => message?.({ message: m, client }),
27
+ fireCommand: (cmd) => command?.({ command: cmd, ack: async () => { }, client }),
28
+ };
29
+ }
30
+ /**
31
+ * A tiny stand-in for SlackConversationStore. The listener only ever
32
+ * calls `store.has(...)`; everything else (getOrCreate, save) is the
33
+ * turn-runner's concern. The test controls which conversation keys are
34
+ * "owned".
35
+ */
36
+ function fakeStore(owned = []) {
37
+ const ownedKeys = new Set(owned.map((o) => `${o.channelId}::${o.scope}`));
38
+ return {
39
+ has: vi.fn(async (k) => ownedKeys.has(`${k.channelId}::${k.scope}`)),
40
+ getOrCreate: vi.fn(),
41
+ save: vi.fn(),
42
+ };
43
+ }
44
+ function setup(opts) {
45
+ const store = fakeStore((opts?.ownedThreads ?? []).map((t) => ({
46
+ channelId: t.channelId,
47
+ scope: t.threadTs,
48
+ })));
49
+ const turns = [];
50
+ const onTurn = vi.fn(async (turn) => {
51
+ turns.push(turn);
52
+ });
53
+ const commands = [];
54
+ const onCommand = vi.fn(async (cmd) => {
55
+ commands.push(cmd);
56
+ });
57
+ const fake = makeFakeApp();
58
+ const assistantKeys = new Set((opts?.assistantThreads ?? []).map((t) => `${t.channel}::${t.threadTs}`));
59
+ attachSlackListener({
60
+ app: fake.app,
61
+ store,
62
+ botUserId: BOT_USER_ID,
63
+ respondTo: opts?.respondTo,
64
+ onTurn,
65
+ onCommand,
66
+ isAssistantThread: opts?.assistantThreads
67
+ ? (channel, threadTs) => assistantKeys.has(`${channel}::${threadTs}`)
68
+ : undefined,
69
+ });
70
+ return { ...fake, turns, onTurn, commands, onCommand, store };
71
+ }
72
+ describe("slack-listener", () => {
73
+ it("resolves partial respondTo config against Slack routing defaults", () => {
74
+ expect(resolveSlackRespondToOptions()).toEqual({
75
+ directMessages: true,
76
+ appMentions: { reply: "thread" },
77
+ threadReplies: "mentionsOnly",
78
+ });
79
+ expect(resolveSlackRespondToOptions({ threadReplies: "afterBotReply" })).toEqual({
80
+ directMessages: true,
81
+ appMentions: { reply: "thread" },
82
+ threadReplies: "afterBotReply",
83
+ });
84
+ });
85
+ it("turns an @mention into a turn with the right reply target", async () => {
86
+ const f = setup();
87
+ await f.fireMention({
88
+ type: "app_mention",
89
+ channel: "C1",
90
+ ts: "100.0",
91
+ text: `<@${BOT_USER_ID}> hello there`,
92
+ });
93
+ expect(f.turns).toHaveLength(1);
94
+ expect(f.turns[0]).toMatchObject({
95
+ conversation: { channelId: "C1", scope: "100.0" },
96
+ replyTarget: { channel: "C1", threadTs: "100.0" },
97
+ userText: "hello there",
98
+ });
99
+ });
100
+ it("ignores @mentions when appMentions is disabled", async () => {
101
+ const f = setup({
102
+ respondTo: {
103
+ directMessages: true,
104
+ appMentions: false,
105
+ threadReplies: "mentionsOnly",
106
+ },
107
+ });
108
+ await f.fireMention({
109
+ type: "app_mention",
110
+ channel: "C1",
111
+ ts: "100.0",
112
+ text: `<@${BOT_USER_ID}> hello there`,
113
+ });
114
+ expect(f.turns).toHaveLength(0);
115
+ });
116
+ it("can reply to @mentions in-channel instead of in-thread", async () => {
117
+ const f = setup({
118
+ respondTo: {
119
+ directMessages: true,
120
+ appMentions: { reply: "channel" },
121
+ threadReplies: "mentionsOnly",
122
+ },
123
+ });
124
+ await f.fireMention({
125
+ type: "app_mention",
126
+ channel: "C1",
127
+ ts: "100.0",
128
+ text: `<@${BOT_USER_ID}> hello there`,
129
+ });
130
+ expect(f.turns).toHaveLength(1);
131
+ expect(f.turns[0]).toMatchObject({
132
+ conversation: { channelId: "C1", scope: "100.0" },
133
+ replyTarget: { channel: "C1" },
134
+ userText: "hello there",
135
+ });
136
+ expect(f.turns[0].replyTarget.threadTs).toBeUndefined();
137
+ });
138
+ it("threads a follow-up @mention into the existing thread", async () => {
139
+ const f = setup();
140
+ await f.fireMention({
141
+ type: "app_mention",
142
+ channel: "C1",
143
+ ts: "200.0",
144
+ thread_ts: "100.0",
145
+ text: `<@${BOT_USER_ID}> follow-up`,
146
+ });
147
+ expect(f.turns[0].replyTarget).toEqual({
148
+ channel: "C1",
149
+ threadTs: "100.0",
150
+ });
151
+ });
152
+ it("ignores @mentions with no real text", async () => {
153
+ const f = setup();
154
+ await f.fireMention({
155
+ type: "app_mention",
156
+ channel: "C1",
157
+ ts: "100.0",
158
+ text: `<@${BOT_USER_ID}> `,
159
+ });
160
+ expect(f.turns).toHaveLength(0);
161
+ });
162
+ it("treats DM messages as flat replies (no threadTs)", async () => {
163
+ const f = setup();
164
+ await f.fireMessage({
165
+ type: "message",
166
+ channel: "D1",
167
+ channel_type: "im",
168
+ user: "UATAI001",
169
+ ts: "300.0",
170
+ text: "hi bot",
171
+ });
172
+ expect(f.turns[0]).toMatchObject({
173
+ conversation: { channelId: "D1", scope: DM_SCOPE },
174
+ replyTarget: { channel: "D1" },
175
+ userText: "hi bot",
176
+ });
177
+ expect(f.turns[0].replyTarget.threadTs).toBeUndefined();
178
+ });
179
+ it("ignores DM messages when directMessages is disabled", async () => {
180
+ const f = setup({
181
+ respondTo: {
182
+ directMessages: false,
183
+ appMentions: { reply: "thread" },
184
+ threadReplies: "mentionsOnly",
185
+ },
186
+ });
187
+ await f.fireMessage({
188
+ type: "message",
189
+ channel: "D1",
190
+ channel_type: "im",
191
+ user: "UATAI001",
192
+ ts: "300.0",
193
+ text: "hi bot",
194
+ });
195
+ expect(f.turns).toHaveLength(0);
196
+ });
197
+ it("skips a pane message (assistant thread) — owned by the Assistant middleware", async () => {
198
+ const f = setup({
199
+ assistantThreads: [{ channel: "D1", threadTs: "100.0" }],
200
+ });
201
+ await f.fireMessage({
202
+ type: "message",
203
+ channel: "D1",
204
+ channel_type: "im",
205
+ user: "UATAI001",
206
+ ts: "300.0",
207
+ thread_ts: "100.0",
208
+ text: "in the pane",
209
+ });
210
+ // Exactly one turn per pane message is the Assistant middleware's job; the
211
+ // listener must NOT double-deliver it.
212
+ expect(f.turns).toHaveLength(0);
213
+ });
214
+ it("still flows an ordinary threaded DM that is NOT an assistant thread (per-thread gate)", async () => {
215
+ const f = setup({
216
+ assistantThreads: [{ channel: "D1", threadTs: "999.0" }],
217
+ });
218
+ await f.fireMessage({
219
+ type: "message",
220
+ channel: "D1",
221
+ channel_type: "im",
222
+ user: "UATAI001",
223
+ ts: "300.0",
224
+ thread_ts: "100.0", // a different thread — not the assistant one
225
+ text: "ordinary threaded dm",
226
+ });
227
+ // The guard is per-thread, never per-config: a non-assistant threaded DM
228
+ // is unaffected and flows as a flat DM (shipped behavior).
229
+ expect(f.turns).toHaveLength(1);
230
+ expect(f.turns[0]).toMatchObject({
231
+ conversation: { channelId: "D1", scope: DM_SCOPE },
232
+ replyTarget: { channel: "D1" },
233
+ });
234
+ });
235
+ it("leaves flat DMs untouched even when an assistant predicate is present", async () => {
236
+ const f = setup({
237
+ assistantThreads: [{ channel: "D1", threadTs: "100.0" }],
238
+ });
239
+ await f.fireMessage({
240
+ type: "message",
241
+ channel: "D1",
242
+ channel_type: "im",
243
+ user: "UATAI001",
244
+ ts: "300.0",
245
+ text: "flat dm, no thread", // no thread_ts → not a pane message
246
+ });
247
+ expect(f.turns).toHaveLength(1);
248
+ expect(f.turns[0].replyTarget.threadTs).toBeUndefined();
249
+ });
250
+ it("ignores a tracked thread plain reply by default", async () => {
251
+ const f = setup({ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }] });
252
+ await f.fireMessage({
253
+ type: "message",
254
+ channel: "C1",
255
+ user: "UATAI001",
256
+ ts: "400.0",
257
+ thread_ts: "100.0",
258
+ text: "carry on",
259
+ });
260
+ expect(f.turns).toHaveLength(0);
261
+ });
262
+ it("continues a tracked thread on a plain reply when configured for legacy behavior", async () => {
263
+ const f = setup({
264
+ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }],
265
+ respondTo: {
266
+ directMessages: true,
267
+ appMentions: { reply: "thread" },
268
+ threadReplies: "afterBotReply",
269
+ },
270
+ });
271
+ await f.fireMessage({
272
+ type: "message",
273
+ channel: "C1",
274
+ user: "UATAI001",
275
+ ts: "400.0",
276
+ thread_ts: "100.0",
277
+ text: "carry on",
278
+ });
279
+ expect(f.turns).toHaveLength(1);
280
+ expect(f.turns[0].userText).toBe("carry on");
281
+ });
282
+ it("ignores plain replies in threads it doesn't own", async () => {
283
+ const f = setup(); // no seed
284
+ await f.fireMessage({
285
+ type: "message",
286
+ channel: "C1",
287
+ user: "UOTHER01",
288
+ ts: "400.0",
289
+ thread_ts: "999.0",
290
+ text: "random thread chatter",
291
+ });
292
+ expect(f.turns).toHaveLength(0);
293
+ });
294
+ it("ignores top-level channel chatter with no @mention", async () => {
295
+ const f = setup();
296
+ await f.fireMessage({
297
+ type: "message",
298
+ channel: "C1",
299
+ user: "UATAI001",
300
+ ts: "500.0",
301
+ text: "just talking",
302
+ });
303
+ expect(f.turns).toHaveLength(0);
304
+ });
305
+ it("skips messages from any bot (bot_id set, no user)", async () => {
306
+ const f = setup({ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }] });
307
+ await f.fireMessage({
308
+ type: "message",
309
+ channel: "C1",
310
+ bot_id: "BOTHER01",
311
+ ts: "600.0",
312
+ thread_ts: "100.0",
313
+ text: "bot chatter",
314
+ });
315
+ expect(f.turns).toHaveLength(0);
316
+ });
317
+ it("skips its own messages (user matches botUserId)", async () => {
318
+ const f = setup({ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }] });
319
+ await f.fireMessage({
320
+ type: "message",
321
+ channel: "C1",
322
+ user: BOT_USER_ID,
323
+ ts: "600.0",
324
+ thread_ts: "100.0",
325
+ text: "my own echo",
326
+ });
327
+ expect(f.turns).toHaveLength(0);
328
+ });
329
+ it("skips subtyped events (edits, joins, etc.)", async () => {
330
+ const f = setup({ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }] });
331
+ await f.fireMessage({
332
+ type: "message",
333
+ subtype: "message_changed",
334
+ channel: "C1",
335
+ ts: "700.0",
336
+ thread_ts: "100.0",
337
+ text: "edited text",
338
+ });
339
+ expect(f.turns).toHaveLength(0);
340
+ });
341
+ it("skips the message.channels echo of an @mention (duplicate event)", async () => {
342
+ const f = setup({ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }] });
343
+ await f.fireMessage({
344
+ type: "message",
345
+ channel: "C1",
346
+ user: "UATAI001",
347
+ ts: "100.0",
348
+ thread_ts: "100.0",
349
+ text: `<@${BOT_USER_ID}> hello`,
350
+ });
351
+ expect(f.turns).toHaveLength(0);
352
+ });
353
+ describe("subtype filter (cases F4..F7)", () => {
354
+ const subtypes = [
355
+ "message_changed",
356
+ "message_deleted",
357
+ "channel_join",
358
+ "channel_leave",
359
+ "channel_rename",
360
+ "channel_topic",
361
+ "channel_purpose",
362
+ "bot_message",
363
+ "thread_broadcast",
364
+ "pinned_item",
365
+ "unpinned_item",
366
+ ];
367
+ for (const subtype of subtypes) {
368
+ it(`ignores subtype=${subtype}`, async () => {
369
+ const f = setup({
370
+ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }],
371
+ });
372
+ await f.fireMessage({
373
+ type: "message",
374
+ subtype,
375
+ channel: "C1",
376
+ ts: "999.0",
377
+ thread_ts: "100.0",
378
+ text: "subtyped",
379
+ });
380
+ expect(f.turns).toHaveLength(0);
381
+ });
382
+ }
383
+ it("ignores a file_share upload in an owned thread by default", async () => {
384
+ const f = setup({
385
+ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }],
386
+ });
387
+ await f.fireMessage({
388
+ type: "message",
389
+ subtype: "file_share",
390
+ channel: "C1",
391
+ user: "UATAI001",
392
+ ts: "999.0",
393
+ thread_ts: "100.0",
394
+ text: "",
395
+ files: [{ id: "F1", name: "data.csv", mimetype: "text/csv" }],
396
+ });
397
+ expect(f.turns).toHaveLength(0);
398
+ });
399
+ it("processes a file_share upload in an owned thread when legacy continuation is enabled", async () => {
400
+ const f = setup({
401
+ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }],
402
+ respondTo: {
403
+ directMessages: true,
404
+ appMentions: { reply: "thread" },
405
+ threadReplies: "afterBotReply",
406
+ },
407
+ });
408
+ await f.fireMessage({
409
+ type: "message",
410
+ subtype: "file_share",
411
+ channel: "C1",
412
+ user: "UATAI001",
413
+ ts: "999.0",
414
+ thread_ts: "100.0",
415
+ text: "",
416
+ files: [{ id: "F1", name: "data.csv", mimetype: "text/csv" }],
417
+ });
418
+ expect(f.turns).toHaveLength(1);
419
+ });
420
+ });
421
+ it("treats a group-DM (mpim) like a non-IM channel (ignored without thread_ts)", async () => {
422
+ const f = setup();
423
+ await f.fireMessage({
424
+ type: "message",
425
+ channel: "G1",
426
+ channel_type: "mpim",
427
+ user: "UATAI001",
428
+ ts: "300.0",
429
+ text: "ping in group dm",
430
+ });
431
+ expect(f.turns).toHaveLength(0);
432
+ });
433
+ it("strips multiple @mentions of the bot from a single message", async () => {
434
+ const f = setup();
435
+ await f.fireMention({
436
+ type: "app_mention",
437
+ channel: "C1",
438
+ ts: "100.0",
439
+ text: `<@${BOT_USER_ID}> hello there <@${BOT_USER_ID}> again`,
440
+ });
441
+ expect(f.turns).toHaveLength(1);
442
+ expect(f.turns[0].userText).toBe("hello there again");
443
+ });
444
+ it("strips @mentions of OTHER users too (so the agent sees clean text)", async () => {
445
+ const f = setup();
446
+ await f.fireMention({
447
+ type: "app_mention",
448
+ channel: "C1",
449
+ ts: "100.0",
450
+ text: `<@${BOT_USER_ID}> tell <@UOTHER01> hi`,
451
+ });
452
+ expect(f.turns[0].userText).toBe("tell hi");
453
+ });
454
+ it("conversation key is keyed on channelId + scope", async () => {
455
+ const f = setup();
456
+ await f.fireMention({
457
+ type: "app_mention",
458
+ channel: "C1",
459
+ ts: "100.0",
460
+ text: `<@${BOT_USER_ID}> first`,
461
+ });
462
+ await f.fireMention({
463
+ type: "app_mention",
464
+ channel: "C1",
465
+ ts: "200.0",
466
+ text: `<@${BOT_USER_ID}> second`,
467
+ });
468
+ expect(f.turns[0].conversation.scope).toBe("100.0");
469
+ expect(f.turns[1].conversation.scope).toBe("200.0");
470
+ expect(f.turns[0].conversation.scope).not.toBe(f.turns[1].conversation.scope);
471
+ });
472
+ it("same thread_ts in different channels = different conversations (and not owned)", async () => {
473
+ const f = setup({ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }] });
474
+ await f.fireMessage({
475
+ type: "message",
476
+ channel: "C2",
477
+ user: "UATAI001",
478
+ ts: "101.0",
479
+ thread_ts: "100.0",
480
+ text: "stranger in another channel",
481
+ });
482
+ expect(f.turns).toHaveLength(0);
483
+ });
484
+ it("forwards a slash command to onCommand with normalized fields (flat reply target)", async () => {
485
+ const f = setup();
486
+ await f.fireCommand({
487
+ command: "/triage",
488
+ channel_id: "C1",
489
+ user_id: "UATAI001",
490
+ text: "hello via slash",
491
+ });
492
+ expect(f.commands).toHaveLength(1);
493
+ expect(f.commands[0].command).toBe("/triage");
494
+ expect(f.commands[0].text).toBe("hello via slash");
495
+ expect(f.commands[0].replyTarget).toEqual({ channel: "C1" });
496
+ expect(f.commands[0].conversation.scope).toBe("slash::UATAI001");
497
+ // A slash command is NOT a turn — routing/handling is the engine's job.
498
+ expect(f.turns).toHaveLength(0);
499
+ });
500
+ it("forwards a command even with empty text (the handler decides what to do)", async () => {
501
+ const f = setup();
502
+ await f.fireCommand({
503
+ command: "/triage",
504
+ channel_id: "C1",
505
+ user_id: "UATAI001",
506
+ text: " ",
507
+ });
508
+ expect(f.commands).toHaveLength(1);
509
+ expect(f.commands[0].text).toBe("");
510
+ });
511
+ it("forwards trigger_id from a slash command to the SlackCommand", async () => {
512
+ const f = setup();
513
+ await f.fireCommand({
514
+ command: "/triage",
515
+ channel_id: "C1",
516
+ user_id: "UATAI001",
517
+ text: "hello",
518
+ trigger_id: "T999.888",
519
+ });
520
+ expect(f.commands).toHaveLength(1);
521
+ expect(f.commands[0].triggerId).toBe("T999.888");
522
+ });
523
+ it("repeat command from same user reuses the same conversation scope", async () => {
524
+ const f = setup();
525
+ await f.fireCommand({
526
+ command: "/triage",
527
+ channel_id: "C1",
528
+ user_id: "UATAI001",
529
+ text: "one",
530
+ });
531
+ await f.fireCommand({
532
+ command: "/triage",
533
+ channel_id: "C1",
534
+ user_id: "UATAI001",
535
+ text: "two",
536
+ });
537
+ expect(f.commands[0].conversation.scope).toBe(f.commands[1].conversation.scope);
538
+ });
539
+ it("F-user-token: thread reply with both user and bot_id (xoxp- post) is treated as real user message", async () => {
540
+ const f = setup({
541
+ ownedThreads: [{ channelId: "C1", threadTs: "100.0" }],
542
+ respondTo: {
543
+ directMessages: true,
544
+ appMentions: { reply: "thread" },
545
+ threadReplies: "afterBotReply",
546
+ },
547
+ });
548
+ await f.fireMessage({
549
+ type: "message",
550
+ channel: "C1",
551
+ user: "UATAI001",
552
+ bot_id: "B0XYZAPP",
553
+ ts: "400.0",
554
+ thread_ts: "100.0",
555
+ text: "follow-up via user token",
556
+ });
557
+ expect(f.turns).toHaveLength(1);
558
+ });
559
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=views.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"views.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/views.test.ts"],"names":[],"mappings":""}