@desplega.ai/agent-swarm 2.5.0 → 2.5.2
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.
- package/dist/{acp-adapter-11gp67nr.js → acp-adapter-csh0zz3b.js} +1 -1
- package/dist/{actions-8p2kze8j.js → actions-6xx079d1.js} +17 -17
- package/dist/{amp-adapter-z4qvhacj.js → amp-adapter-8n6f0hz8.js} +2 -2
- package/dist/{app-0qp0a2gk.js → app-ft6r6v23.js} +3 -3
- package/dist/{assistant-jedd3ktm.js → assistant-vt2cyfzd.js} +18 -18
- package/dist/{boot-reembed-n4wq5dd2.js → boot-reembed-r3e808ky.js} +1 -1
- package/dist/{boot-reembed-4xwq5tg0.js → boot-reembed-tgrfy27t.js} +1 -1
- package/dist/{boot-scrub-sweep-69h90xcq.js → boot-scrub-sweep-masdwxkm.js} +2 -2
- package/dist/{boot-seeds-3vn0m9sg.js → boot-seeds-0n82qc7q.js} +4 -4
- package/dist/{claude-adapter-221mvad5.js → claude-adapter-s7j47k06.js} +2 -2
- package/dist/{claude-managed-setup-c3hp36ph.js → claude-managed-setup-3hzrgrnk.js} +1 -1
- package/dist/{claude-sdk-session-8thjsk5e.js → claude-sdk-session-0zrdzyx2.js} +6905 -6855
- package/dist/{cli-y9jvxfhk.js → cli-0vgy5wfw.js} +2 -2
- package/dist/{cli-bgdtv62r.js → cli-14v1sxxy.js} +6 -6
- package/dist/{cli-fy1jyyym.js → cli-15xd0j7w.js} +18 -13
- package/dist/{cli-36j4jnk3.js → cli-3e9vs93g.js} +4 -4
- package/dist/{cli-5rvg1ddk.js → cli-4qkg9968.js} +3 -3
- package/dist/{cli-ba8ne1es.js → cli-4rvr7zfx.js} +10 -0
- package/dist/{cli-s4sntwt4.js → cli-6y43v79h.js} +1 -1
- package/dist/{cli-z63a269p.js → cli-7ccf7akh.js} +1 -1
- package/dist/{cli-0z3vjrpm.js → cli-86zpfje5.js} +116 -5
- package/dist/{cli-fes51ya2.js → cli-aaqnqpyw.js} +2 -2
- package/dist/{cli-mbfj11fq.js → cli-b1tv22wr.js} +1 -1
- package/dist/{cli-ftmq4nvk.js → cli-b63d08rs.js} +1 -1
- package/dist/{cli-xt768r5m.js → cli-b6tnjdvx.js} +11 -7
- package/dist/{cli-0y6f0ckq.js → cli-bpbcpd8b.js} +1 -1
- package/dist/{cli-sy8f9705.js → cli-cf63dnxj.js} +2 -2
- package/dist/{cli-fcc5qe3f.js → cli-csh8fnbg.js} +82 -16
- package/dist/{cli-x6vf4sad.js → cli-cyfgxnvb.js} +7 -7
- package/dist/{cli-9ce2xw98.js → cli-d172svcw.js} +5 -5
- package/dist/{cli-we0ra057.js → cli-e91vshb1.js} +41 -6
- package/dist/{cli-segs7hx9.js → cli-edtm741g.js} +5 -5
- package/dist/{cli-x21dqn18.js → cli-f3pfn4x3.js} +1 -1
- package/dist/{cli-nnvjb17s.js → cli-f55b6ky0.js} +1 -1
- package/dist/{cli-1pr672hz.js → cli-fgn7yg5r.js} +1 -1
- package/dist/{cli-5kzk3472.js → cli-h5d22pfs.js} +287 -78
- package/dist/{cli-r25qpaxt.js → cli-ha22h0eg.js} +20 -20
- package/dist/{cli-92mqenkc.js → cli-j3dwd4kz.js} +173 -134
- package/dist/{cli-j1eghqfm.js → cli-jmcjbjkt.js} +1 -1
- package/dist/{cli-179xcyky.js → cli-kcnwn85g.js} +4 -3
- package/dist/{cli-rh5smt8t.js → cli-mntxtkhn.js} +12 -2
- package/dist/{cli-7g40g8hq.js → cli-n2918sed.js} +9 -9
- package/dist/{cli-y4x8ww6s.js → cli-qrjv7t18.js} +1 -1
- package/dist/{cli-7bwtc6yx.js → cli-vaxw3bg0.js} +3 -3
- package/dist/{cli-4w24f7z3.js → cli-wadzapwb.js} +1 -1
- package/dist/{cli-jpxsrxcb.js → cli-xw9q173h.js} +1 -1
- package/dist/{cli-xawhpgc9.js → cli-xzge46sj.js} +2 -2
- package/dist/{cli-3zwye9sn.js → cli-y3dzwyy3.js} +3 -3
- package/dist/{cli-xd4eqtr2.js → cli-ydg6ep51.js} +1 -1
- package/dist/cli.js +16 -15
- package/dist/{codex-adapter-tcg1q3jk.js → codex-adapter-brh2tcxr.js} +1 -1
- package/dist/{codex-session-runner-xfx4wd9x.js → codex-session-runner-0dnj21qs.js} +1 -1
- package/dist/{commands-ew83nj70.js → commands-1cnvseqx.js} +2 -2
- package/dist/{controller-gnq1k1mf.js → controller-kc7s9xhn.js} +2 -2
- package/dist/{controllers-connect-tw1dan9r.js → controllers-connect-jzx7t4bn.js} +18 -18
- package/dist/{cursor-adapter-yq5xgfcg.js → cursor-adapter-qpwp5sfg.js} +1 -1
- package/dist/{db-t86xrnes.js → db-4vq88jkc.js} +3 -3
- package/dist/{db-7gbc53fd.js → db-v5ytg5pw.js} +1 -1
- package/dist/{dispatcher-64bdyhps.js → dispatcher-trtcrqae.js} +16 -16
- package/dist/{dsh-adapter-tvhc4k2d.js → dsh-adapter-f20xwn1z.js} +1 -1
- package/dist/{external-channel-e9nw24zv.js → external-channel-n9bymc2n.js} +2 -2
- package/dist/{grok-adapter-v82srhrx.js → grok-adapter-4kpmmnr6.js} +2 -2
- package/dist/{handlers-c9ajzhbn.js → handlers-ptsraqa8.js} +19 -19
- package/dist/{heartbeat-2vqs1319.js → heartbeat-60qtxtjf.js} +19 -19
- package/dist/{hook-01nk13wx.js → hook-ngtvh729.js} +1 -1
- package/dist/{hook-jzpheegg.js → hook-rbktcstg.js} +1 -1
- package/dist/{http-fc1k6ec8.js → http-z8xy72gp.js} +334 -86
- package/dist/{index-zdycv3ja.js → index-06qkmcx6.js} +10 -9
- package/dist/{index-07ef7nkm.js → index-1cscynkb.js} +16 -16
- package/dist/{index-sepja052.js → index-a9nsavhx.js} +1 -1
- package/dist/{index-q3t40ryv.js → index-r70q0xjx.js} +16 -16
- package/dist/{index-7yts8n9s.js → index-s193j5fh.js} +19 -19
- package/dist/{keepalive-5nh2rhmr.js → keepalive-a9srcphj.js} +3 -3
- package/dist/{lead-aayq64jn.js → lead-3devneqy.js} +26 -26
- package/dist/{maintenance-hjhzdt3k.js → maintenance-0er16h2y.js} +1 -1
- package/dist/{oauth-refresh-sweep-y3n1knmd.js → oauth-refresh-sweep-b65jcyw2.js} +2 -2
- package/dist/{onboard-1zq3m7db.js → onboard-wqywz5py.js} +2 -2
- package/dist/{otel-impl-h9yyza47.js → otel-impl-vk32s884.js} +1 -1
- package/dist/{pricing-refresh-nevhat8h.js → pricing-refresh-t27anv4z.js} +3 -3
- package/dist/rbac-roles-452665y0.js +26 -0
- package/dist/{rbac-roles-4y6ngvzq.js → rbac-roles-bmcgsxjn.js} +12 -2
- package/dist/{render-v2-cfsh13z3.js → render-v2-0ttsk2j4.js} +3 -3
- package/dist/{resume-5s2t0dbs.js → resume-a26kvr41.js} +5 -5
- package/dist/{session-access-dnbyp3cn.js → session-access-dgh9nkwk.js} +4 -4
- package/dist/{setup-1wj1y77j.js → setup-9vc9qahm.js} +1 -1
- package/dist/{work-objects-7mg6216g.js → work-objects-vn8nkxwn.js} +19 -19
- package/dist/{worker-nwb789mg.js → worker-1x1h8vnm.js} +26 -26
- package/openapi.json +463 -3
- package/package.json +4 -3
- package/src/be/db/session-tree.ts +89 -2
- package/src/be/db/session-visibility.ts +31 -0
- package/src/be/db/tasks/read.ts +18 -5
- package/src/be/db.ts +2 -0
- package/src/be/migrations/233_script_run_session_tokens.sql +8 -0
- package/src/be/migrations/235_chat_relay_defer.sql +86 -0
- package/src/be/migrations/237_session_inbox_cleanup.sql +75 -0
- package/src/be/rbac-roles.ts +138 -0
- package/src/be/scripts/run-credential.ts +64 -0
- package/src/be/scripts/run-saved.ts +3 -2
- package/src/be/users.ts +28 -4
- package/src/gitlab/auth.ts +8 -2
- package/src/gitlab/handlers.ts +60 -12
- package/src/gitlab/index.ts +1 -1
- package/src/http/all-routes.ts +1 -0
- package/src/http/approval-requests.ts +46 -6
- package/src/http/index.ts +2 -0
- package/src/http/schedules.ts +26 -0
- package/src/http/scripts.ts +2 -2
- package/src/http/sessions.ts +93 -2
- package/src/http/user-roles.ts +173 -0
- package/src/http/users.ts +9 -0
- package/src/http/x.ts +2 -2
- package/src/integrations/chat-sdk/commands.ts +14 -1
- package/src/integrations/chat-sdk/inbound.ts +40 -5
- package/src/integrations/chat-sdk/outbound.ts +35 -10
- package/src/integrations/chat-sdk/reactions.ts +107 -0
- package/src/integrations/chat-sdk/relay.ts +75 -7
- package/src/integrations/chat-sdk/thread-buffer.ts +2 -0
- package/src/integrations/reaction-events.ts +20 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -0
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/schedule-task.ts +44 -3
- package/src/scheduler/scheduler.ts +4 -4
- package/src/scripts-runtime/loader.ts +8 -1
- package/src/slack/ack.ts +2 -1
- package/src/slack/reaction-shortcode.ts +4 -7
- package/src/tasks/worker-follow-up.ts +30 -0
- package/src/tests/approval-requests-notifications.test.ts +189 -0
- package/src/tests/chat-sdk-commands.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +163 -9
- package/src/tests/chat-sdk-outbound-files.test.ts +9 -2
- package/src/tests/chat-sdk-relay.test.ts +361 -10
- package/src/tests/chat-sdk-tools.test.ts +104 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/gitlab-auth.test.ts +13 -5
- package/src/tests/gitlab-handlers.test.ts +270 -22
- package/src/tests/http-users.test.ts +38 -0
- package/src/tests/migration-237-session-inbox-cleanup.test.ts +136 -0
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +10 -0
- package/src/tests/schedule-http-triage-tools.test.ts +44 -0
- package/src/tests/script-api-credential.test.ts +173 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-run-config-scope.test.ts +120 -0
- package/src/tests/script-run-token.test.ts +75 -0
- package/src/tests/scripts-http.test.ts +19 -0
- package/src/tests/sessions-tree.test.ts +242 -0
- package/src/tests/user-roles-http.test.ts +204 -0
- package/src/tests/workflow-hitl-dynamic-questions.test.ts +1 -1
- package/src/tools/chat-reply.ts +2 -0
- package/src/tools/schedules/patch-schedule.ts +18 -0
- package/src/tools/schedules/update-schedule.ts +18 -0
- package/src/types.ts +16 -0
- package/src/workflows/approval-notifications.ts +214 -0
- package/src/workflows/executors/human-in-the-loop.ts +11 -172
- package/src/workflows/executors/swarm-script.ts +2 -2
- package/templates/schedules/weekly-harness-upgrade-check/content.md +1 -1
- package/templates/skills/code-quality/content.md +1 -1
- package/dist/rbac-roles-exbg8xb6.js +0 -12
|
@@ -3,14 +3,18 @@ import { unlink } from "node:fs/promises";
|
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import {
|
|
5
5
|
closeDb,
|
|
6
|
+
completeTask,
|
|
6
7
|
createAgent,
|
|
8
|
+
createScheduledTask,
|
|
7
9
|
createTaskExtended,
|
|
8
10
|
getDbClient,
|
|
9
11
|
getKv,
|
|
12
|
+
getScheduledTaskById,
|
|
10
13
|
getTaskById,
|
|
11
14
|
initDb,
|
|
12
15
|
} from "../be/db";
|
|
13
16
|
import { CHAT_CHANNELS_NAMESPACE } from "../integrations/chat-sdk/router";
|
|
17
|
+
import { createStandaloneScheduleTask } from "../scheduler/schedule-task";
|
|
14
18
|
import { createServer } from "../server";
|
|
15
19
|
import { chatSdkContextKey } from "../tasks/context-key";
|
|
16
20
|
import { registerChatOpenDmTool } from "../tools/chat-open-dm";
|
|
@@ -21,6 +25,8 @@ import {
|
|
|
21
25
|
registerRegisterChatChannelTool,
|
|
22
26
|
registerUnregisterChatChannelTool,
|
|
23
27
|
} from "../tools/register-chat-channel";
|
|
28
|
+
import { registerPatchScheduleTool } from "../tools/schedules/patch-schedule";
|
|
29
|
+
import { registerUpdateScheduleTool } from "../tools/schedules/update-schedule";
|
|
24
30
|
import { registerSendTaskTool } from "../tools/send-task";
|
|
25
31
|
import type { Agent } from "../types";
|
|
26
32
|
import { startTelegramHarness, type TelegramHarness } from "./chat-sdk-harness";
|
|
@@ -108,6 +114,8 @@ beforeAll(async () => {
|
|
|
108
114
|
registerRegisterChatChannelTool(server);
|
|
109
115
|
registerUnregisterChatChannelTool(server);
|
|
110
116
|
registerSendTaskTool(server);
|
|
117
|
+
registerPatchScheduleTool(server);
|
|
118
|
+
registerUpdateScheduleTool(server);
|
|
111
119
|
});
|
|
112
120
|
|
|
113
121
|
afterAll(async () => {
|
|
@@ -194,6 +202,8 @@ describe("chat-reply", () => {
|
|
|
194
202
|
agentId: worker.id,
|
|
195
203
|
source: "chat",
|
|
196
204
|
contextKey: chatSdkContextKey("telegram:991002"),
|
|
205
|
+
mentionMessageId: "991002:77",
|
|
206
|
+
mentionChannelId: "telegram:991002",
|
|
197
207
|
});
|
|
198
208
|
const result = await callTool(
|
|
199
209
|
"chat-reply",
|
|
@@ -212,6 +222,11 @@ describe("chat-reply", () => {
|
|
|
212
222
|
String((call.body as { chat_id?: unknown }).chat_id) === "991002",
|
|
213
223
|
);
|
|
214
224
|
expect(sends).toHaveLength(1);
|
|
225
|
+
// Anchored like the final answer: a reply to the message that started the task.
|
|
226
|
+
expect(sends[0]?.body.reply_parameters).toEqual({
|
|
227
|
+
message_id: 77,
|
|
228
|
+
allow_sending_without_reply: true,
|
|
229
|
+
});
|
|
215
230
|
});
|
|
216
231
|
});
|
|
217
232
|
|
|
@@ -518,3 +533,92 @@ describe("chat thread binding through send-task", () => {
|
|
|
518
533
|
expect(child?.contextKey).toBe(chatSdkContextKey("telegram:444444"));
|
|
519
534
|
});
|
|
520
535
|
});
|
|
536
|
+
|
|
537
|
+
describe("defer-task wake-ups and chat thread access", () => {
|
|
538
|
+
let harness: TelegramHarness;
|
|
539
|
+
|
|
540
|
+
beforeAll(async () => {
|
|
541
|
+
harness = await startTelegramHarness();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
afterAll(async () => {
|
|
545
|
+
await harness.stop();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
/** A lead chat task deferred with defer-task: the task is done, its wake-up booked. */
|
|
549
|
+
async function leadDeferral(chatId: string) {
|
|
550
|
+
const parent = await createTaskExtended("human ask", {
|
|
551
|
+
agentId: lead.id,
|
|
552
|
+
source: "chat",
|
|
553
|
+
contextKey: chatSdkContextKey(`telegram:${chatId}`),
|
|
554
|
+
mentionMessageId: `${chatId}:55`,
|
|
555
|
+
});
|
|
556
|
+
await completeTask(parent.id, "Deferred.", { deferredAt: new Date().toISOString() });
|
|
557
|
+
const schedule = await createScheduledTask({
|
|
558
|
+
name: `deferred-${parent.id.slice(0, 8)}-${crypto.randomUUID()}`,
|
|
559
|
+
taskTemplate: "finish the original ask",
|
|
560
|
+
targetType: "agent-task",
|
|
561
|
+
scheduleType: "one_time",
|
|
562
|
+
nextRunAt: new Date(Date.now() + 60_000).toISOString(),
|
|
563
|
+
targetAgentId: lead.id,
|
|
564
|
+
createdByAgentId: lead.id,
|
|
565
|
+
taskType: "deferred",
|
|
566
|
+
parentTaskId: parent.id,
|
|
567
|
+
});
|
|
568
|
+
return { parent, schedule };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
test("another worker cannot change a lead's deferral", async () => {
|
|
572
|
+
const { schedule } = await leadDeferral("992881");
|
|
573
|
+
for (const tool of ["patch-schedule", "update-schedule"]) {
|
|
574
|
+
const result = await callTool(
|
|
575
|
+
tool,
|
|
576
|
+
{ "x-agent-id": worker.id },
|
|
577
|
+
{ scheduleId: schedule.id, targetAgentId: worker.id, taskTemplate: "emit ATTACKER OUTPUT" },
|
|
578
|
+
);
|
|
579
|
+
expect(result.structuredContent.success).toBe(false);
|
|
580
|
+
}
|
|
581
|
+
const after = await getScheduledTaskById(schedule.id);
|
|
582
|
+
expect(after?.targetAgentId).toBe(lead.id);
|
|
583
|
+
expect(after?.taskTemplate).toBe("finish the original ask");
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("a deferral re-targeted to another agent wakes up outside the chat thread", async () => {
|
|
587
|
+
const chatId = "992882";
|
|
588
|
+
const { schedule } = await leadDeferral(chatId);
|
|
589
|
+
// A lead may re-target it; the new assignee still gets no thread access.
|
|
590
|
+
const patched = await callTool(
|
|
591
|
+
"patch-schedule",
|
|
592
|
+
{ "x-agent-id": lead.id },
|
|
593
|
+
{ scheduleId: schedule.id, targetAgentId: worker.id },
|
|
594
|
+
);
|
|
595
|
+
expect(patched.structuredContent.success).toBe(true);
|
|
596
|
+
const retargeted = await getScheduledTaskById(schedule.id);
|
|
597
|
+
if (!retargeted) throw new Error("schedule vanished");
|
|
598
|
+
|
|
599
|
+
const wakeUp = await createStandaloneScheduleTask(retargeted);
|
|
600
|
+
expect(wakeUp.agentId).toBe(worker.id);
|
|
601
|
+
expect(wakeUp.contextKey).toBe(`task:schedule:${schedule.id}`);
|
|
602
|
+
|
|
603
|
+
const reply = await callTool(
|
|
604
|
+
"chat-reply",
|
|
605
|
+
{ "x-agent-id": worker.id, "x-source-task-id": wakeUp.id },
|
|
606
|
+
{ text: "ATTACKER DIRECT REPLY" },
|
|
607
|
+
);
|
|
608
|
+
expect(reply.structuredContent.success).toBe(false);
|
|
609
|
+
|
|
610
|
+
await completeTask(wakeUp.id, "ATTACKER OUTPUT");
|
|
611
|
+
const obligation = await getDbClient().get(
|
|
612
|
+
"SELECT task_id FROM chat_relay_obligations WHERE task_id = ?",
|
|
613
|
+
[wakeUp.id],
|
|
614
|
+
);
|
|
615
|
+
expect(obligation).toBeNull();
|
|
616
|
+
expect(
|
|
617
|
+
harness.mock.calls.some(
|
|
618
|
+
(call) =>
|
|
619
|
+
/^send(Rich)?Message$/.test(call.method) &&
|
|
620
|
+
String((call.body as { chat_id?: unknown }).chat_id) === chatId,
|
|
621
|
+
),
|
|
622
|
+
).toBe(false);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
|
|
|
78
78
|
expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test("OpenAPI and generated pages keep all
|
|
81
|
+
test("OpenAPI and generated pages keep all 449 operations", async () => {
|
|
82
82
|
const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
|
|
83
83
|
const operationKeys = [];
|
|
84
84
|
const tags = new Set();
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(449);
|
|
107
|
+
expect(generatedKeys).toHaveLength(449);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
-
|
|
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("
|
|
104
|
-
test("defaults to agent-swarm-bot", () => {
|
|
105
|
-
|
|
106
|
-
|
|
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 {
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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:
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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: `@${
|
|
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 = `@${
|
|
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(`@${
|
|
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: `@${
|
|
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",
|