@desplega.ai/agent-swarm 2.5.0 → 2.5.1

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 (82) hide show
  1. package/dist/{acp-adapter-11gp67nr.js → acp-adapter-7g6bpwbk.js} +1 -1
  2. package/dist/{actions-8p2kze8j.js → actions-w8xd014k.js} +16 -16
  3. package/dist/{app-0qp0a2gk.js → app-nndcvftq.js} +3 -3
  4. package/dist/{assistant-jedd3ktm.js → assistant-hjmqyscx.js} +17 -17
  5. package/dist/{boot-reembed-n4wq5dd2.js → boot-reembed-r8rhf9h8.js} +1 -1
  6. package/dist/{boot-reembed-4xwq5tg0.js → boot-reembed-vwcj2y5d.js} +1 -1
  7. package/dist/{boot-scrub-sweep-69h90xcq.js → boot-scrub-sweep-fr9q36ne.js} +2 -2
  8. package/dist/{boot-seeds-3vn0m9sg.js → boot-seeds-9jqwnegq.js} +4 -4
  9. package/dist/{claude-adapter-221mvad5.js → claude-adapter-2f8b85df.js} +2 -2
  10. package/dist/{claude-sdk-session-8thjsk5e.js → claude-sdk-session-05kat8rv.js} +2 -2
  11. package/dist/{cli-x6vf4sad.js → cli-0jy85zz4.js} +5 -5
  12. package/dist/{cli-5rvg1ddk.js → cli-1dp1zj04.js} +2 -2
  13. package/dist/{cli-179xcyky.js → cli-22rmcjkg.js} +1 -1
  14. package/dist/{cli-7g40g8hq.js → cli-2jg61wab.js} +8 -8
  15. package/dist/{cli-r25qpaxt.js → cli-2wba1ac9.js} +12 -12
  16. package/dist/{cli-y4x8ww6s.js → cli-3ee5z0q0.js} +1 -1
  17. package/dist/{cli-0y6f0ckq.js → cli-9mm5ade4.js} +1 -1
  18. package/dist/{cli-x21dqn18.js → cli-ajqhxtnz.js} +1 -1
  19. package/dist/{cli-segs7hx9.js → cli-cah5detr.js} +5 -5
  20. package/dist/{cli-4w24f7z3.js → cli-cwyafhw1.js} +1 -1
  21. package/dist/{cli-fy1jyyym.js → cli-e9btqw4y.js} +10 -10
  22. package/dist/{cli-bgdtv62r.js → cli-eym3jg7w.js} +6 -6
  23. package/dist/{cli-xt768r5m.js → cli-gwcf52gz.js} +1 -1
  24. package/dist/{cli-nnvjb17s.js → cli-hadv0725.js} +1 -1
  25. package/dist/{cli-5kzk3472.js → cli-kcsd1ez8.js} +60 -32
  26. package/dist/{cli-we0ra057.js → cli-mq7kg1b1.js} +2 -2
  27. package/dist/{cli-y9jvxfhk.js → cli-mz2ygeyq.js} +2 -2
  28. package/dist/{cli-3zwye9sn.js → cli-nj12tn2c.js} +2 -2
  29. package/dist/{cli-92mqenkc.js → cli-pyy4c9ch.js} +7 -7
  30. package/dist/{cli-fcc5qe3f.js → cli-q9pzwxvk.js} +3 -3
  31. package/dist/{cli-sy8f9705.js → cli-qfjm4gvt.js} +2 -2
  32. package/dist/{cli-mbfj11fq.js → cli-qfm6my86.js} +1 -1
  33. package/dist/{cli-1pr672hz.js → cli-r9fy0ddc.js} +1 -1
  34. package/dist/{cli-s4sntwt4.js → cli-rngde2jv.js} +1 -1
  35. package/dist/{cli-7bwtc6yx.js → cli-sn1vbtg2.js} +3 -3
  36. package/dist/{cli-36j4jnk3.js → cli-v2g93k4v.js} +3 -3
  37. package/dist/{cli-ftmq4nvk.js → cli-xc65x52q.js} +1 -1
  38. package/dist/{cli-9ce2xw98.js → cli-y3zyaee5.js} +4 -4
  39. package/dist/{cli-0z3vjrpm.js → cli-z2dv25mf.js} +1 -1
  40. package/dist/{cli-xawhpgc9.js → cli-znvq0k3t.js} +1 -1
  41. package/dist/cli.js +11 -11
  42. package/dist/{codex-adapter-tcg1q3jk.js → codex-adapter-p4ptwar5.js} +1 -1
  43. package/dist/{codex-session-runner-xfx4wd9x.js → codex-session-runner-964c7dnk.js} +1 -1
  44. package/dist/{commands-ew83nj70.js → commands-mbvhbrcy.js} +2 -2
  45. package/dist/{controllers-connect-tw1dan9r.js → controllers-connect-6dkm71tg.js} +16 -16
  46. package/dist/{db-7gbc53fd.js → db-88wcymxt.js} +1 -1
  47. package/dist/{db-t86xrnes.js → db-af9y8nkf.js} +3 -3
  48. package/dist/{dispatcher-64bdyhps.js → dispatcher-606ydx5v.js} +15 -15
  49. package/dist/{external-channel-e9nw24zv.js → external-channel-y8q5z8z2.js} +2 -2
  50. package/dist/{grok-adapter-v82srhrx.js → grok-adapter-39aha52t.js} +2 -2
  51. package/dist/{handlers-c9ajzhbn.js → handlers-rk9dqech.js} +18 -18
  52. package/dist/{heartbeat-2vqs1319.js → heartbeat-jfe9zfjr.js} +17 -17
  53. package/dist/{hook-jzpheegg.js → hook-b1mez1m8.js} +1 -1
  54. package/dist/{hook-01nk13wx.js → hook-ffevzed1.js} +1 -1
  55. package/dist/{http-fc1k6ec8.js → http-d5b44f1r.js} +36 -36
  56. package/dist/{index-q3t40ryv.js → index-4q3vj1yr.js} +15 -15
  57. package/dist/{index-sepja052.js → index-5en23s5z.js} +1 -1
  58. package/dist/{index-07ef7nkm.js → index-d2qeem97.js} +15 -15
  59. package/dist/{index-7yts8n9s.js → index-mj46rk9k.js} +17 -17
  60. package/dist/{index-zdycv3ja.js → index-nq8hhv28.js} +7 -7
  61. package/dist/{keepalive-5nh2rhmr.js → keepalive-wsby1btq.js} +3 -3
  62. package/dist/{lead-aayq64jn.js → lead-paa141j6.js} +22 -22
  63. package/dist/{maintenance-hjhzdt3k.js → maintenance-vnshcyv9.js} +1 -1
  64. package/dist/{oauth-refresh-sweep-y3n1knmd.js → oauth-refresh-sweep-d6sqn5np.js} +2 -2
  65. package/dist/{onboard-1zq3m7db.js → onboard-nr4xhpzy.js} +2 -2
  66. package/dist/{otel-impl-h9yyza47.js → otel-impl-ytn9wyv9.js} +1 -1
  67. package/dist/{pricing-refresh-nevhat8h.js → pricing-refresh-pva04zee.js} +3 -3
  68. package/dist/{rbac-roles-4y6ngvzq.js → rbac-roles-7rc1q3g4.js} +1 -1
  69. package/dist/{rbac-roles-exbg8xb6.js → rbac-roles-ff1ffmec.js} +2 -2
  70. package/dist/{render-v2-cfsh13z3.js → render-v2-pyfsg9zk.js} +3 -3
  71. package/dist/{resume-5s2t0dbs.js → resume-phj8xhqr.js} +5 -5
  72. package/dist/{session-access-dnbyp3cn.js → session-access-jzx2z9h5.js} +4 -4
  73. package/dist/{setup-1wj1y77j.js → setup-rfg0f0sg.js} +1 -1
  74. package/dist/{work-objects-7mg6216g.js → work-objects-44z2yjg8.js} +18 -18
  75. package/dist/{worker-nwb789mg.js → worker-zsfqzxhm.js} +22 -22
  76. package/openapi.json +1 -1
  77. package/package.json +1 -1
  78. package/src/gitlab/auth.ts +8 -2
  79. package/src/gitlab/handlers.ts +60 -12
  80. package/src/gitlab/index.ts +1 -1
  81. package/src/tests/gitlab-auth.test.ts +13 -5
  82. package/src/tests/gitlab-handlers.test.ts +270 -22
@@ -8,13 +8,21 @@
8
8
  * - Detects bot mentions
9
9
  */
10
10
 
11
- import { failTask, findTaskByVcs, getAllAgents, incrKv, upsertKv } from "../be/db";
11
+ import {
12
+ failTask,
13
+ findActiveTasksByVcs,
14
+ findTaskByVcs,
15
+ getAllAgents,
16
+ incrKv,
17
+ upsertKv,
18
+ } from "../be/db";
12
19
  import { renderIdentity, resolveIdentity } from "../be/identity";
13
20
  import { findOrCreateUserByEmail, findUserByExternalId, linkIdentity } from "../be/users";
14
21
  import { resolveTemplate } from "../prompts/resolver";
15
22
  import { gitlabContextKey } from "../tasks/context-key";
16
23
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
17
- import { GITLAB_BOT_NAME } from "./auth";
24
+ import type { AgentTask } from "../types";
25
+ import { getGitLabBotName } from "./auth";
18
26
  import { addGitLabNoteReaction, addGitLabReaction } from "./reactions";
19
27
  import type { GitLabUser } from "./types";
20
28
  // Side-effect import: registers all GitLab event templates in the in-memory registry
@@ -40,11 +48,37 @@ function isDuplicate(key: string): boolean {
40
48
 
41
49
  function detectMention(text: string | null | undefined): boolean {
42
50
  if (!text) return false;
43
- return new RegExp(`@${GITLAB_BOT_NAME}\\b`, "i").test(text);
51
+ return new RegExp(`@${getGitLabBotName()}\\b`, "i").test(text);
44
52
  }
45
53
 
46
54
  function extractMentionContext(text: string): string {
47
- return text.replace(new RegExp(`@${GITLAB_BOT_NAME}\\b`, "gi"), "").trim();
55
+ return text.replace(new RegExp(`@${getGitLabBotName()}\\b`, "gi"), "").trim();
56
+ }
57
+
58
+ // Legacy rows written before contextKey existed: scoped only on positive evidence.
59
+ const LEGACY_SCOPE = {
60
+ mr: {
61
+ taskTypes: new Set(["gitlab-mr", "gitlab-ci"]),
62
+ eventTypes: new Set(["merge_request", "note_on_mr", "pipeline"]),
63
+ },
64
+ issue: {
65
+ taskTypes: new Set(["gitlab-issue"]),
66
+ eventTypes: new Set(["issue", "note_on_issue"]),
67
+ },
68
+ };
69
+
70
+ /**
71
+ * GitLab issue and MR iids are separate per-project sequences, so a vcsNumber
72
+ * match alone hits both issue #N and MR !N. Match on the contextKey instead; a
73
+ * row without one counts only when its taskType or vcsEventType names the kind.
74
+ */
75
+ function isScopedTask(task: AgentTask, kind: "mr" | "issue", contextKey: string): boolean {
76
+ if (task.contextKey) return task.contextKey === contextKey;
77
+ const legacy = LEGACY_SCOPE[kind];
78
+ return (
79
+ (task.taskType !== undefined && legacy.taskTypes.has(task.taskType)) ||
80
+ (task.vcsEventType !== undefined && legacy.eventTypes.has(task.vcsEventType))
81
+ );
48
82
  }
49
83
 
50
84
  async function findLeadAgent() {
@@ -206,12 +240,18 @@ export async function handleMergeRequest(
206
240
 
207
241
  case "close":
208
242
  case "merge": {
209
- // Cancel existing tasks for this MR
210
- const existingTask = await findTaskByVcs(repo, mr.iid, "gitlab");
211
- if (existingTask) {
212
- const reason = action === "merge" ? "MR was merged" : "MR was closed";
213
- console.log(`[GitLab] Cancelling task ${existingTask.id} — ${reason}`);
214
- await failTask(existingTask.id, reason);
243
+ // Every active task, not only the newest: a note task is a child of the
244
+ // MR task, and failTask does not cascade up to the parent.
245
+ const reason = action === "merge" ? "MR was merged" : "MR was closed";
246
+ const mrContextKey = gitlabContextKey({
247
+ projectId: String(project.id),
248
+ kind: "mr",
249
+ iid: mr.iid,
250
+ });
251
+ const tasks = await findActiveTasksByVcs(repo, mr.iid, "gitlab");
252
+ for (const task of tasks.filter((t) => isScopedTask(t, "mr", mrContextKey))) {
253
+ console.log(`[GitLab] Cancelling task ${task.id} — ${reason}`);
254
+ await failTask(task.id, reason);
215
255
  }
216
256
  return { created: false };
217
257
  }
@@ -262,7 +302,7 @@ export async function handleIssue(
262
302
  if (!hasMention) {
263
303
  // Check if bot is in assignees
264
304
  const isBotAssigned = event.assignees?.some(
265
- (a) => a.username.toLowerCase() === GITLAB_BOT_NAME.toLowerCase(),
305
+ (a) => a.username.toLowerCase() === getGitLabBotName().toLowerCase(),
266
306
  );
267
307
  if (!isBotAssigned) {
268
308
  console.log(`[GitLab] Issue opened without bot mention/assignment, skipping`);
@@ -318,7 +358,15 @@ export async function handleIssue(
318
358
  }
319
359
 
320
360
  case "close": {
321
- const existingTask = await findTaskByVcs(repo, issue.iid, "gitlab");
361
+ const issueContextKey = gitlabContextKey({
362
+ projectId: String(project.id),
363
+ kind: "issue",
364
+ iid: issue.iid,
365
+ });
366
+ // Newest issue-scoped task only; MR !N shares the iid but not the issue.
367
+ const existingTask = (await findActiveTasksByVcs(repo, issue.iid, "gitlab")).find((t) =>
368
+ isScopedTask(t, "issue", issueContextKey),
369
+ );
322
370
  if (existingTask) {
323
371
  console.log(`[GitLab] Cancelling task ${existingTask.id} — issue closed`);
324
372
  await failTask(existingTask.id, "Issue was closed");
@@ -1,6 +1,6 @@
1
1
  // GitLab Integration
2
2
  export {
3
- GITLAB_BOT_NAME,
3
+ getGitLabBotName,
4
4
  getGitLabToken,
5
5
  getGitLabUrl,
6
6
  initGitLab,
@@ -1,6 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import {
3
- GITLAB_BOT_NAME,
3
+ getGitLabBotName,
4
4
  getGitLabToken,
5
5
  getGitLabUrl,
6
6
  initGitLab,
@@ -100,10 +100,18 @@ describe("GitLab auth", () => {
100
100
  });
101
101
  });
102
102
 
103
- describe("GITLAB_BOT_NAME", () => {
104
- test("defaults to agent-swarm-bot", () => {
105
- // The default is set at module load time from env
106
- expect(typeof GITLAB_BOT_NAME).toBe("string");
103
+ describe("getGitLabBotName", () => {
104
+ test("defaults to agent-swarm-bot and follows GITLAB_BOT_NAME", () => {
105
+ const saved = process.env.GITLAB_BOT_NAME;
106
+ try {
107
+ delete process.env.GITLAB_BOT_NAME;
108
+ expect(getGitLabBotName()).toBe("agent-swarm-bot");
109
+ process.env.GITLAB_BOT_NAME = "renamed-bot";
110
+ expect(getGitLabBotName()).toBe("renamed-bot");
111
+ } finally {
112
+ if (saved === undefined) delete process.env.GITLAB_BOT_NAME;
113
+ else process.env.GITLAB_BOT_NAME = saved;
114
+ }
107
115
  });
108
116
  });
109
117
  });
@@ -13,7 +13,7 @@ import {
13
13
  initDb,
14
14
  } from "../be/db";
15
15
  import { findUserByExternalId, linkIdentity } from "../be/users";
16
- import { GITLAB_BOT_NAME } from "../gitlab/auth";
16
+ import { getGitLabBotName } from "../gitlab/auth";
17
17
  import { handleIssue, handleMergeRequest, handleNote, handlePipeline } from "../gitlab/handlers";
18
18
  import type { IssueEvent, MergeRequestEvent, NoteEvent, PipelineEvent } from "../gitlab/types";
19
19
 
@@ -37,7 +37,7 @@ function makeMREvent(overrides: Partial<MergeRequestEvent> = {}): MergeRequestEv
37
37
  id: 100,
38
38
  iid: 1,
39
39
  title: "Test MR",
40
- description: `@${GITLAB_BOT_NAME} please review`,
40
+ description: `@${getGitLabBotName()} please review`,
41
41
  state: "opened",
42
42
  action: "open",
43
43
  source_branch: "feature",
@@ -66,7 +66,7 @@ function makeIssueEvent(overrides: Partial<IssueEvent> = {}): IssueEvent {
66
66
  id: 200,
67
67
  iid: 10,
68
68
  title: "Test Issue",
69
- description: `@${GITLAB_BOT_NAME} fix this`,
69
+ description: `@${getGitLabBotName()} fix this`,
70
70
  state: "opened",
71
71
  action: "open",
72
72
  url: "https://gitlab.com/group/project/-/issues/10",
@@ -90,7 +90,7 @@ function makeNoteEvent(overrides: Partial<NoteEvent> = {}): NoteEvent {
90
90
  },
91
91
  object_attributes: {
92
92
  id: 300,
93
- note: `@${GITLAB_BOT_NAME} can you help?`,
93
+ note: `@${getGitLabBotName()} can you help?`,
94
94
  noteable_type: "MergeRequest",
95
95
  noteable_id: 100,
96
96
  url: "https://gitlab.com/group/project/-/merge_requests/1#note_300",
@@ -181,7 +181,7 @@ describe("handleMergeRequest", () => {
181
181
  id: 101,
182
182
  iid: 50,
183
183
  title: "MR with mention",
184
- description: `@${GITLAB_BOT_NAME} review this please`,
184
+ description: `@${getGitLabBotName()} review this please`,
185
185
  state: "opened",
186
186
  action: "open",
187
187
  source_branch: "feat-a",
@@ -302,13 +302,207 @@ describe("handleMergeRequest", () => {
302
302
  expect(existing).toBeNull();
303
303
  });
304
304
 
305
+ test("a merge cancels every active task for the MR, not only the newest", async () => {
306
+ const mrTask = await createTaskExtended("[GitLab MR #62] Test", {
307
+ source: "gitlab",
308
+ vcsProvider: "gitlab",
309
+ vcsRepo: "group/project",
310
+ vcsEventType: "merge_request",
311
+ vcsNumber: 62,
312
+ agentId: "lead-gl-001",
313
+ });
314
+ const noteTask = await createTaskExtended("[GitLab MR #62] Comment", {
315
+ source: "gitlab",
316
+ vcsProvider: "gitlab",
317
+ vcsRepo: "group/project",
318
+ vcsEventType: "note_on_mr",
319
+ vcsNumber: 62,
320
+ agentId: "lead-gl-001",
321
+ parentTaskId: mrTask.id,
322
+ });
323
+
324
+ const event = makeMREvent({
325
+ object_attributes: {
326
+ id: 105,
327
+ iid: 62,
328
+ title: "MR to merge",
329
+ description: null,
330
+ state: "merged",
331
+ action: "merge",
332
+ source_branch: "feat-e",
333
+ target_branch: "main",
334
+ url: "https://gitlab.com/group/project/-/merge_requests/62",
335
+ last_commit: null,
336
+ author_id: 1,
337
+ },
338
+ });
339
+ await handleMergeRequest(event);
340
+
341
+ for (const id of [mrTask.id, noteTask.id]) {
342
+ const task = await getTaskById(id);
343
+ expect(task?.status).toBe("failed");
344
+ expect(task?.failureReason).toBe("MR was merged");
345
+ }
346
+ });
347
+
348
+ test("closing MR !5 leaves issue #5 and its notes active", async () => {
349
+ // GitLab issue and MR iids are separate sequences: issue #5 and MR !5 coexist.
350
+ const issue = await handleIssue(
351
+ makeIssueEvent({
352
+ object_attributes: {
353
+ id: 205,
354
+ iid: 5,
355
+ title: "Issue five",
356
+ description: `@${getGitLabBotName()} fix this`,
357
+ state: "opened",
358
+ action: "open",
359
+ url: "https://gitlab.com/group/project/-/issues/5",
360
+ author_id: 1,
361
+ },
362
+ }),
363
+ );
364
+ const issueNote = await handleNote(
365
+ makeNoteEvent({
366
+ object_attributes: {
367
+ id: 395,
368
+ note: `@${getGitLabBotName()} any update?`,
369
+ noteable_type: "Issue",
370
+ noteable_id: 205,
371
+ url: "https://gitlab.com/group/project/-/issues/5#note_395",
372
+ author_id: 1,
373
+ type: null,
374
+ },
375
+ merge_request: undefined,
376
+ issue: {
377
+ id: 205,
378
+ iid: 5,
379
+ title: "Issue five",
380
+ description: null,
381
+ state: "opened",
382
+ action: "open",
383
+ url: "https://gitlab.com/group/project/-/issues/5",
384
+ author_id: 1,
385
+ },
386
+ }),
387
+ );
388
+ const mr = await handleMergeRequest(
389
+ makeMREvent({
390
+ object_attributes: {
391
+ id: 155,
392
+ iid: 5,
393
+ title: "MR five",
394
+ description: `@${getGitLabBotName()} please review`,
395
+ state: "opened",
396
+ action: "open",
397
+ source_branch: "feat-five",
398
+ target_branch: "main",
399
+ url: "https://gitlab.com/group/project/-/merge_requests/5",
400
+ last_commit: null,
401
+ author_id: 1,
402
+ },
403
+ }),
404
+ );
405
+ const mrNote = await handleNote(
406
+ makeNoteEvent({
407
+ object_attributes: {
408
+ id: 396,
409
+ note: `@${getGitLabBotName()} looks good?`,
410
+ noteable_type: "MergeRequest",
411
+ noteable_id: 155,
412
+ url: "https://gitlab.com/group/project/-/merge_requests/5#note_396",
413
+ author_id: 1,
414
+ type: null,
415
+ },
416
+ merge_request: {
417
+ id: 155,
418
+ iid: 5,
419
+ title: "MR five",
420
+ description: null,
421
+ state: "opened",
422
+ action: "open",
423
+ source_branch: "feat-five",
424
+ target_branch: "main",
425
+ url: "https://gitlab.com/group/project/-/merge_requests/5",
426
+ last_commit: null,
427
+ author_id: 1,
428
+ },
429
+ }),
430
+ );
431
+ // A pre-contextKey issue row must not be swept up either.
432
+ const legacyIssue = await createTaskExtended("[GitLab Issue #5] Legacy", {
433
+ source: "gitlab",
434
+ vcsProvider: "gitlab",
435
+ vcsRepo: "group/project",
436
+ vcsEventType: "issue",
437
+ vcsNumber: 5,
438
+ agentId: "lead-gl-001",
439
+ });
440
+ for (const r of [issue, issueNote, mr, mrNote]) expect(r.created).toBe(true);
441
+
442
+ await handleMergeRequest(
443
+ makeMREvent({
444
+ object_attributes: {
445
+ id: 155,
446
+ iid: 5,
447
+ title: "MR five",
448
+ description: null,
449
+ state: "closed",
450
+ action: "close",
451
+ source_branch: "feat-five",
452
+ target_branch: "main",
453
+ url: "https://gitlab.com/group/project/-/merge_requests/5",
454
+ last_commit: null,
455
+ author_id: 1,
456
+ },
457
+ }),
458
+ );
459
+
460
+ for (const id of [mr.taskId!, mrNote.taskId!]) {
461
+ const task = await getTaskById(id);
462
+ expect(task?.status).toBe("failed");
463
+ expect(task?.failureReason).toBe("MR was closed");
464
+ }
465
+ for (const id of [issue.taskId!, issueNote.taskId!, legacyIssue.id]) {
466
+ const task = await getTaskById(id);
467
+ expect(task?.status).not.toBe("failed");
468
+ }
469
+ });
470
+
471
+ test("reads GITLAB_BOT_NAME at match time, so a config reload applies", async () => {
472
+ const mrEvent = (iid: number, description: string) =>
473
+ makeMREvent({
474
+ object_attributes: {
475
+ id: 1000 + iid,
476
+ iid,
477
+ title: "MR for renamed bot",
478
+ description,
479
+ state: "opened",
480
+ action: "open",
481
+ source_branch: "feat-renamed",
482
+ target_branch: "main",
483
+ url: `https://gitlab.com/group/project/-/merge_requests/${iid}`,
484
+ last_commit: null,
485
+ author_id: 1,
486
+ },
487
+ });
488
+ process.env.GITLAB_BOT_NAME = "renamed-bot";
489
+ try {
490
+ expect((await handleMergeRequest(mrEvent(63, "@renamed-bot review"))).created).toBe(true);
491
+ expect((await handleMergeRequest(mrEvent(64, "@agent-swarm-bot review"))).created).toBe(
492
+ false,
493
+ );
494
+ } finally {
495
+ delete process.env.GITLAB_BOT_NAME;
496
+ }
497
+ });
498
+
305
499
  test("ignores update action when no active task", async () => {
306
500
  const event = makeMREvent({
307
501
  object_attributes: {
308
502
  id: 105,
309
503
  iid: 62,
310
504
  title: "Updated MR",
311
- description: `@${GITLAB_BOT_NAME} review`,
505
+ description: `@${getGitLabBotName()} review`,
312
506
  state: "opened",
313
507
  action: "update",
314
508
  source_branch: "feat-e",
@@ -329,7 +523,7 @@ describe("handleMergeRequest", () => {
329
523
  id: 106,
330
524
  iid: 70,
331
525
  title: "Dedup MR",
332
- description: `@${GITLAB_BOT_NAME} review`,
526
+ description: `@${getGitLabBotName()} review`,
333
527
  state: "opened",
334
528
  action: "open",
335
529
  source_branch: "feat-dedup",
@@ -360,7 +554,7 @@ describe("handleIssue", () => {
360
554
  id: 201,
361
555
  iid: 20,
362
556
  title: "Issue with mention",
363
- description: `@${GITLAB_BOT_NAME} implement this feature`,
557
+ description: `@${getGitLabBotName()} implement this feature`,
364
558
  state: "opened",
365
559
  action: "open",
366
560
  url: "https://gitlab.com/group/project/-/issues/20",
@@ -391,7 +585,7 @@ describe("handleIssue", () => {
391
585
  url: "https://gitlab.com/group/project/-/issues/21",
392
586
  author_id: 1,
393
587
  },
394
- assignees: [{ id: 99, name: "Bot", username: GITLAB_BOT_NAME, avatar_url: "" }],
588
+ assignees: [{ id: 99, name: "Bot", username: getGitLabBotName(), avatar_url: "" }],
395
589
  });
396
590
 
397
591
  const result = await handleIssue(event);
@@ -447,6 +641,60 @@ describe("handleIssue", () => {
447
641
  const task = await findTaskByVcs("group/project", 30, "gitlab");
448
642
  expect(task).toBeNull();
449
643
  });
644
+
645
+ test("closing issue #6 fails the issue task, not the newer MR !6 task", async () => {
646
+ const issue = await handleIssue(
647
+ makeIssueEvent({
648
+ object_attributes: {
649
+ id: 206,
650
+ iid: 6,
651
+ title: "Issue six",
652
+ description: `@${getGitLabBotName()} fix this`,
653
+ state: "opened",
654
+ action: "open",
655
+ url: "https://gitlab.com/group/project/-/issues/6",
656
+ author_id: 1,
657
+ },
658
+ }),
659
+ );
660
+ // Created second, so it is the newest row for vcsNumber 6.
661
+ const mr = await handleMergeRequest(
662
+ makeMREvent({
663
+ object_attributes: {
664
+ id: 156,
665
+ iid: 6,
666
+ title: "MR six",
667
+ description: `@${getGitLabBotName()} please review`,
668
+ state: "opened",
669
+ action: "open",
670
+ source_branch: "feat-six",
671
+ target_branch: "main",
672
+ url: "https://gitlab.com/group/project/-/merge_requests/6",
673
+ last_commit: null,
674
+ author_id: 1,
675
+ },
676
+ }),
677
+ );
678
+ expect(issue.created && mr.created).toBe(true);
679
+
680
+ await handleIssue(
681
+ makeIssueEvent({
682
+ object_attributes: {
683
+ id: 206,
684
+ iid: 6,
685
+ title: "Issue six",
686
+ description: null,
687
+ state: "closed",
688
+ action: "close",
689
+ url: "https://gitlab.com/group/project/-/issues/6",
690
+ author_id: 1,
691
+ },
692
+ }),
693
+ );
694
+
695
+ expect((await getTaskById(issue.taskId!))?.failureReason).toBe("Issue was closed");
696
+ expect((await getTaskById(mr.taskId!))?.status).not.toBe("failed");
697
+ });
450
698
  });
451
699
 
452
700
  // ═══════════════════════════════════════════════════════
@@ -458,7 +706,7 @@ describe("handleNote", () => {
458
706
  const event = makeNoteEvent({
459
707
  object_attributes: {
460
708
  id: 301,
461
- note: `@${GITLAB_BOT_NAME} please check this code`,
709
+ note: `@${getGitLabBotName()} please check this code`,
462
710
  noteable_type: "MergeRequest",
463
711
  noteable_id: 100,
464
712
  url: "https://gitlab.com/group/project/-/merge_requests/1#note_301",
@@ -493,7 +741,7 @@ describe("handleNote", () => {
493
741
  const event = makeNoteEvent({
494
742
  object_attributes: {
495
743
  id: 302,
496
- note: `@${GITLAB_BOT_NAME} what do you think?`,
744
+ note: `@${getGitLabBotName()} what do you think?`,
497
745
  noteable_type: "Issue",
498
746
  noteable_id: 200,
499
747
  url: "https://gitlab.com/group/project/-/issues/40#note_302",
@@ -542,7 +790,7 @@ describe("handleNote", () => {
542
790
  const event = makeNoteEvent({
543
791
  object_attributes: {
544
792
  id: 304,
545
- note: `@${GITLAB_BOT_NAME} review this commit`,
793
+ note: `@${getGitLabBotName()} review this commit`,
546
794
  noteable_type: "Commit",
547
795
  noteable_id: 999,
548
796
  url: "https://gitlab.com/group/project/-/commit/abc#note_304",
@@ -572,7 +820,7 @@ describe("handleNote", () => {
572
820
  const event = makeNoteEvent({
573
821
  object_attributes: {
574
822
  id: 305,
575
- note: `@${GITLAB_BOT_NAME} also fix the linting`,
823
+ note: `@${getGitLabBotName()} also fix the linting`,
576
824
  noteable_type: "MergeRequest",
577
825
  noteable_id: 100,
578
826
  url: "https://gitlab.com/group/project/-/merge_requests/85#note_305",
@@ -742,7 +990,7 @@ describe("identity resolution — MR handler", () => {
742
990
  id: 901,
743
991
  iid: 901,
744
992
  title: "Known MR",
745
- description: `@${GITLAB_BOT_NAME} review`,
993
+ description: `@${getGitLabBotName()} review`,
746
994
  state: "opened",
747
995
  action: "open",
748
996
  source_branch: "feat-known",
@@ -779,7 +1027,7 @@ describe("identity resolution — MR handler", () => {
779
1027
  id: 902,
780
1028
  iid: 902,
781
1029
  title: "Inline-email MR",
782
- description: `@${GITLAB_BOT_NAME} please`,
1030
+ description: `@${getGitLabBotName()} please`,
783
1031
  state: "opened",
784
1032
  action: "open",
785
1033
  source_branch: "feat-inline",
@@ -816,7 +1064,7 @@ describe("identity resolution — MR handler", () => {
816
1064
  id: 903,
817
1065
  iid: 903,
818
1066
  title: "Ghost MR",
819
- description: `@${GITLAB_BOT_NAME} ping`,
1067
+ description: `@${getGitLabBotName()} ping`,
820
1068
  state: "opened",
821
1069
  action: "open",
822
1070
  source_branch: "feat-ghost",
@@ -850,7 +1098,7 @@ describe("identity resolution — MR handler", () => {
850
1098
  id: 910 + i,
851
1099
  iid: 910 + i,
852
1100
  title: `Ghost MR ${i}`,
853
- description: `@${GITLAB_BOT_NAME} ping`,
1101
+ description: `@${getGitLabBotName()} ping`,
854
1102
  state: "opened",
855
1103
  action: "open",
856
1104
  source_branch: `feat-g-${i}`,
@@ -879,7 +1127,7 @@ describe("identity resolution — MR handler", () => {
879
1127
  id: 920,
880
1128
  iid: 920,
881
1129
  title: "Empty email MR",
882
- description: `@${GITLAB_BOT_NAME} review`,
1130
+ description: `@${getGitLabBotName()} review`,
883
1131
  state: "opened",
884
1132
  action: "open",
885
1133
  source_branch: "feat-empty",
@@ -914,7 +1162,7 @@ describe("identity resolution — Issue handler", () => {
914
1162
  id: 950,
915
1163
  iid: 950,
916
1164
  title: "Ghost issue",
917
- description: `@${GITLAB_BOT_NAME} fix`,
1165
+ description: `@${getGitLabBotName()} fix`,
918
1166
  state: "opened",
919
1167
  action: "open",
920
1168
  url: "https://gitlab.com/group/project/-/issues/950",
@@ -942,7 +1190,7 @@ describe("identity resolution — Note handler", () => {
942
1190
  });
943
1191
 
944
1192
  test("unknown user WITHOUT email → unmapped entry tagged 'note'", async () => {
945
- const noteBody = `@${GITLAB_BOT_NAME} can you look at this comment that is somewhat longer than usual to verify the slice(0,100) truncation downstream`;
1193
+ const noteBody = `@${getGitLabBotName()} can you look at this comment that is somewhat longer than usual to verify the slice(0,100) truncation downstream`;
946
1194
  const event = makeNoteEvent({
947
1195
  user: { id: 31, name: "Note Ghost", username: "noteghost", avatar_url: "" },
948
1196
  object_attributes: {
@@ -976,7 +1224,7 @@ describe("identity resolution — Note handler", () => {
976
1224
  expect(meta).not.toBeNull();
977
1225
  expect(meta?.sampleEventType).toBe("note");
978
1226
  expect((meta?.sampleContext as string).length).toBeLessThanOrEqual(100);
979
- expect((meta?.sampleContext as string).startsWith(`@${GITLAB_BOT_NAME}`)).toBe(true);
1227
+ expect((meta?.sampleContext as string).startsWith(`@${getGitLabBotName()}`)).toBe(true);
980
1228
  expect(await getUnmappedCount("noteghost")).toBe(1);
981
1229
 
982
1230
  // The dead-coded `_requestedByUserId` is now wired: undefined for an
@@ -996,7 +1244,7 @@ describe("identity resolution — Note handler", () => {
996
1244
  user: { id: 32, name: "Known Commenter", username: "knowncommenter", avatar_url: "" },
997
1245
  object_attributes: {
998
1246
  id: 961,
999
- note: `@${GITLAB_BOT_NAME} please take a look`,
1247
+ note: `@${getGitLabBotName()} please take a look`,
1000
1248
  noteable_type: "MergeRequest",
1001
1249
  noteable_id: 101,
1002
1250
  url: "https://gitlab.com/group/project/-/merge_requests/2#note_961",