@desplega.ai/agent-swarm 1.100.2 → 1.100.4
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/openapi.json +1 -1
- package/package.json +1 -1
- package/src/be/db.ts +131 -4
- package/src/be/memory/raters/retrieval.ts +6 -3
- package/src/be/migrations/097_memory_retrieval_grouping.sql +10 -0
- package/src/github/handlers.ts +84 -7
- package/src/github/templates.ts +6 -2
- package/src/heartbeat/heartbeat.ts +191 -5
- package/src/providers/claude-adapter.ts +41 -4
- package/src/slack/assistant.ts +28 -0
- package/src/slack/channel-join.ts +38 -3
- package/src/slack/handlers.ts +4 -1
- package/src/tasks/worker-follow-up.ts +181 -20
- package/src/tests/claude-adapter-binary.test.ts +74 -0
- package/src/tests/github-handlers-inline-comments.test.ts +308 -0
- package/src/tests/heartbeat-reroute-decision.test.ts +570 -0
- package/src/tests/heartbeat-supersede-resume.test.ts +137 -0
- package/src/tests/heartbeat.test.ts +4 -2
- package/src/tests/memory-rater-implicit-citation.test.ts +31 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -1
- package/src/tests/slack-assistant-comention-production.test.ts +319 -0
- package/src/tests/slack-assistant-comention.test.ts +139 -0
- package/src/tests/slack-channel-join.test.ts +150 -16
- package/src/tests/workflow-swarm-script.test.ts +225 -0
- package/src/tests/workflow-template.test.ts +17 -0
- package/src/tools/send-task.ts +51 -1
- package/src/tools/templates.ts +61 -0
- package/src/workflows/engine.ts +22 -1
- package/src/workflows/retry-poller.ts +2 -3
- package/src/workflows/template.ts +48 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for inline PR review comment surfacing in handlePullRequestReview.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - CHANGES_REQUESTED review with inline comments → task description includes them
|
|
6
|
+
* - commented-no-body-with-inline-comments → task is created (not skipped)
|
|
7
|
+
* - commented-no-body-no-inline-comments → task is skipped (existing behavior preserved)
|
|
8
|
+
* - commented-no-body-no-installation → graceful fallback, task is skipped
|
|
9
|
+
*/
|
|
10
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
11
|
+
import { unlink } from "node:fs/promises";
|
|
12
|
+
import { closeDb, createAgent, getDb, initDb } from "../be/db";
|
|
13
|
+
import { handleComment, handlePullRequestReview } from "../github/handlers";
|
|
14
|
+
import { GITHUB_BOT_NAME } from "../github/mentions";
|
|
15
|
+
import type { CommentEvent, PullRequestReviewEvent } from "../github/types";
|
|
16
|
+
import { getTemplateDefinition } from "../prompts/registry";
|
|
17
|
+
|
|
18
|
+
// Side-effect import: registers all GitHub templates on first load
|
|
19
|
+
import "../github/templates";
|
|
20
|
+
|
|
21
|
+
async function ensureTemplatesRegistered(): Promise<void> {
|
|
22
|
+
if (getTemplateDefinition("github.pull_request.review_submitted")) return;
|
|
23
|
+
const ts = Date.now();
|
|
24
|
+
await import(`../github/templates?t=${ts}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Mock GitHub App credentials so fetchReviewComments can obtain a token
|
|
28
|
+
// without a real RSA key. Must come before the handlers import is evaluated.
|
|
29
|
+
mock.module("../github/app", () => ({
|
|
30
|
+
getInstallationToken: async (installationId: number) => {
|
|
31
|
+
if (installationId > 0) return "mock-token-for-tests";
|
|
32
|
+
return null;
|
|
33
|
+
},
|
|
34
|
+
isReactionsEnabled: () => false,
|
|
35
|
+
initGitHub: () => true,
|
|
36
|
+
resetGitHub: () => {},
|
|
37
|
+
getWebhookSecret: () => null,
|
|
38
|
+
isGitHubEnabled: () => true,
|
|
39
|
+
verifyWebhookSignature: async () => false,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const TEST_DB_PATH = "./test-github-handlers-inline.sqlite";
|
|
43
|
+
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
await unlink(TEST_DB_PATH).catch(() => {});
|
|
46
|
+
await unlink(`${TEST_DB_PATH}-wal`).catch(() => {});
|
|
47
|
+
await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
|
|
48
|
+
initDb(TEST_DB_PATH);
|
|
49
|
+
createAgent({
|
|
50
|
+
id: "lead-inline-test",
|
|
51
|
+
name: "InlineTestLead",
|
|
52
|
+
status: "idle",
|
|
53
|
+
isLead: true,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterAll(async () => {
|
|
58
|
+
closeDb();
|
|
59
|
+
await unlink(TEST_DB_PATH).catch(() => {});
|
|
60
|
+
await unlink(`${TEST_DB_PATH}-wal`).catch(() => {});
|
|
61
|
+
await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
beforeEach(async () => {
|
|
65
|
+
await ensureTemplatesRegistered();
|
|
66
|
+
getDb().prepare("DELETE FROM agent_tasks").run();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── Helpers ──
|
|
70
|
+
|
|
71
|
+
const BASE_REPO = { full_name: "test/repo", html_url: "https://github.com/test/repo" };
|
|
72
|
+
|
|
73
|
+
let reviewIdCounter = 9000;
|
|
74
|
+
|
|
75
|
+
function makeReviewEvent(opts: {
|
|
76
|
+
state: "changes_requested" | "commented";
|
|
77
|
+
body: string | null;
|
|
78
|
+
installationId?: number;
|
|
79
|
+
prUserLogin?: string;
|
|
80
|
+
}): PullRequestReviewEvent {
|
|
81
|
+
const id = ++reviewIdCounter;
|
|
82
|
+
return {
|
|
83
|
+
action: "submitted",
|
|
84
|
+
review: {
|
|
85
|
+
id,
|
|
86
|
+
body: opts.body,
|
|
87
|
+
state: opts.state,
|
|
88
|
+
html_url: `https://github.com/test/repo/pull/99#pullrequestreview-${id}`,
|
|
89
|
+
user: { login: "reviewer" },
|
|
90
|
+
submitted_at: "2026-01-01T00:00:00Z",
|
|
91
|
+
},
|
|
92
|
+
pull_request: {
|
|
93
|
+
number: 99,
|
|
94
|
+
title: "Bot PR",
|
|
95
|
+
body: null,
|
|
96
|
+
html_url: "https://github.com/test/repo/pull/99",
|
|
97
|
+
user: { login: opts.prUserLogin ?? GITHUB_BOT_NAME },
|
|
98
|
+
head: { ref: "feature" },
|
|
99
|
+
base: { ref: "main" },
|
|
100
|
+
},
|
|
101
|
+
repository: BASE_REPO,
|
|
102
|
+
sender: { login: "reviewer" },
|
|
103
|
+
...(opts.installationId !== undefined ? { installation: { id: opts.installationId } } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const SAMPLE_INLINE_COMMENTS = [
|
|
108
|
+
{
|
|
109
|
+
id: 1001,
|
|
110
|
+
path: "src/domain_tables.go",
|
|
111
|
+
line: 77,
|
|
112
|
+
body: "This logic looks wrong — should use a map instead.",
|
|
113
|
+
html_url: "https://github.com/test/repo/pull/99#discussion_r1001",
|
|
114
|
+
diff_hunk: "@@ -75,6 +75,8 @@ func buildTable() {",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 1002,
|
|
118
|
+
path: "config/table-renderers.json",
|
|
119
|
+
line: 7,
|
|
120
|
+
body: "Why is this hardcoded? Should come from config.",
|
|
121
|
+
html_url: "https://github.com/test/repo/pull/99#discussion_r1002",
|
|
122
|
+
diff_hunk: "@@ -5,7 +5,9 @@ {",
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
function mockFetchWithComments(
|
|
127
|
+
comments: typeof SAMPLE_INLINE_COMMENTS | [],
|
|
128
|
+
): ReturnType<typeof spyOn> {
|
|
129
|
+
return spyOn(globalThis, "fetch").mockImplementationOnce(
|
|
130
|
+
async () =>
|
|
131
|
+
new Response(JSON.stringify(comments), {
|
|
132
|
+
status: 200,
|
|
133
|
+
headers: { "Content-Type": "application/json" },
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getLastTaskText(): string | undefined {
|
|
139
|
+
const row = getDb()
|
|
140
|
+
.prepare<{ task: string }, never>("SELECT task FROM agent_tasks ORDER BY rowid DESC LIMIT 1")
|
|
141
|
+
.get();
|
|
142
|
+
return row?.task;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Tests ──
|
|
146
|
+
|
|
147
|
+
describe("inline review comment surfacing", () => {
|
|
148
|
+
test("CHANGES_REQUESTED review with inline comments: task includes path:line and bodies", async () => {
|
|
149
|
+
const fetchSpy = mockFetchWithComments(SAMPLE_INLINE_COMMENTS);
|
|
150
|
+
|
|
151
|
+
const event = makeReviewEvent({
|
|
152
|
+
state: "changes_requested",
|
|
153
|
+
body: "CI is not green",
|
|
154
|
+
installationId: 123,
|
|
155
|
+
});
|
|
156
|
+
const result = await handlePullRequestReview(event);
|
|
157
|
+
|
|
158
|
+
fetchSpy.mockRestore();
|
|
159
|
+
|
|
160
|
+
expect(result.created).toBe(true);
|
|
161
|
+
const text = getLastTaskText();
|
|
162
|
+
expect(text).toBeDefined();
|
|
163
|
+
expect(text).toContain("src/domain_tables.go:77");
|
|
164
|
+
expect(text).toContain("This logic looks wrong");
|
|
165
|
+
expect(text).toContain("config/table-renderers.json:7");
|
|
166
|
+
expect(text).toContain("Why is this hardcoded?");
|
|
167
|
+
expect(text).toContain("Inline review comments");
|
|
168
|
+
expect(text).toContain("reply to and resolve each inline review thread");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("commented review with no body but with inline comments: task is created, not skipped", async () => {
|
|
172
|
+
const fetchSpy = mockFetchWithComments([SAMPLE_INLINE_COMMENTS[0]]);
|
|
173
|
+
|
|
174
|
+
const event = makeReviewEvent({ state: "commented", body: null, installationId: 123 });
|
|
175
|
+
const result = await handlePullRequestReview(event);
|
|
176
|
+
|
|
177
|
+
fetchSpy.mockRestore();
|
|
178
|
+
|
|
179
|
+
expect(result.created).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("commented review with no body and no inline comments: task is skipped", async () => {
|
|
183
|
+
const fetchSpy = mockFetchWithComments([]);
|
|
184
|
+
|
|
185
|
+
const event = makeReviewEvent({ state: "commented", body: null, installationId: 123 });
|
|
186
|
+
const result = await handlePullRequestReview(event);
|
|
187
|
+
|
|
188
|
+
fetchSpy.mockRestore();
|
|
189
|
+
|
|
190
|
+
expect(result.created).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("commented review with no body and no installation: graceful fallback, task is skipped", async () => {
|
|
194
|
+
const event = makeReviewEvent({ state: "commented", body: null });
|
|
195
|
+
const result = await handlePullRequestReview(event);
|
|
196
|
+
expect(result.created).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("review comments fetch failure: task still created (graceful degradation)", async () => {
|
|
200
|
+
const fetchSpy = spyOn(globalThis, "fetch").mockImplementationOnce(
|
|
201
|
+
async () => new Response("Internal Server Error", { status: 500 }),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const event = makeReviewEvent({
|
|
205
|
+
state: "changes_requested",
|
|
206
|
+
body: "Needs work",
|
|
207
|
+
installationId: 123,
|
|
208
|
+
});
|
|
209
|
+
const result = await handlePullRequestReview(event);
|
|
210
|
+
|
|
211
|
+
fetchSpy.mockRestore();
|
|
212
|
+
|
|
213
|
+
// Task should still be created even if the inline comments fetch fails
|
|
214
|
+
expect(result.created).toBe(true);
|
|
215
|
+
const text = getLastTaskText();
|
|
216
|
+
expect(text).toContain("Needs work");
|
|
217
|
+
// No inline comments section when fetch failed
|
|
218
|
+
expect(text).not.toContain("Inline review comments");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("pagination: two-page review comment response surfaces all comments from both pages", async () => {
|
|
222
|
+
const page1 = [SAMPLE_INLINE_COMMENTS[0]];
|
|
223
|
+
const page2 = [SAMPLE_INLINE_COMMENTS[1]];
|
|
224
|
+
const nextUrl =
|
|
225
|
+
"https://api.github.com/repos/test/repo/pulls/99/reviews/9001/comments?per_page=100&page=2";
|
|
226
|
+
|
|
227
|
+
const fetchSpy = spyOn(globalThis, "fetch")
|
|
228
|
+
.mockImplementationOnce(
|
|
229
|
+
async () =>
|
|
230
|
+
new Response(JSON.stringify(page1), {
|
|
231
|
+
status: 200,
|
|
232
|
+
headers: {
|
|
233
|
+
"Content-Type": "application/json",
|
|
234
|
+
Link: `<${nextUrl}>; rel="next", <${nextUrl}>; rel="last"`,
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
.mockImplementationOnce(
|
|
239
|
+
async () =>
|
|
240
|
+
new Response(JSON.stringify(page2), {
|
|
241
|
+
status: 200,
|
|
242
|
+
headers: { "Content-Type": "application/json" },
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const event = makeReviewEvent({
|
|
247
|
+
state: "changes_requested",
|
|
248
|
+
body: "Multi-page review",
|
|
249
|
+
installationId: 123,
|
|
250
|
+
});
|
|
251
|
+
const result = await handlePullRequestReview(event);
|
|
252
|
+
fetchSpy.mockRestore();
|
|
253
|
+
|
|
254
|
+
expect(result.created).toBe(true);
|
|
255
|
+
const text = getLastTaskText();
|
|
256
|
+
expect(text).toBeDefined();
|
|
257
|
+
// Both pages of inline comments must appear in the task
|
|
258
|
+
expect(text).toContain("src/domain_tables.go:77"); // page 1 comment
|
|
259
|
+
expect(text).toContain("config/table-renderers.json:7"); // page 2 comment
|
|
260
|
+
expect(text).toContain("Inline review comments (2)");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("no-double-spawn: review-attached inline comment via pull_request_review_comment event does not create a second task", async () => {
|
|
264
|
+
// Step 1: submitted review creates exactly ONE bundle task
|
|
265
|
+
const fetchSpy = mockFetchWithComments(SAMPLE_INLINE_COMMENTS);
|
|
266
|
+
const reviewEvent = makeReviewEvent({
|
|
267
|
+
state: "changes_requested",
|
|
268
|
+
body: "Please address my comments",
|
|
269
|
+
installationId: 123,
|
|
270
|
+
});
|
|
271
|
+
const reviewResult = await handlePullRequestReview(reviewEvent);
|
|
272
|
+
fetchSpy.mockRestore();
|
|
273
|
+
|
|
274
|
+
expect(reviewResult.created).toBe(true);
|
|
275
|
+
const taskCountAfterReview = getDb()
|
|
276
|
+
.prepare<{ n: number }, never>("SELECT COUNT(*) AS n FROM agent_tasks")
|
|
277
|
+
.get()!.n;
|
|
278
|
+
expect(taskCountAfterReview).toBe(1);
|
|
279
|
+
|
|
280
|
+
// Step 2: GitHub also delivers the same inline comments as individual
|
|
281
|
+
// pull_request_review_comment events (no @agent-swarm mention). These
|
|
282
|
+
// must NOT spawn additional tasks — the mention gate in handleComment blocks them.
|
|
283
|
+
const inlineCommentEvent: CommentEvent = {
|
|
284
|
+
action: "created",
|
|
285
|
+
comment: {
|
|
286
|
+
id: SAMPLE_INLINE_COMMENTS[0].id,
|
|
287
|
+
body: SAMPLE_INLINE_COMMENTS[0].body, // no @agent-swarm mention
|
|
288
|
+
html_url: SAMPLE_INLINE_COMMENTS[0].html_url,
|
|
289
|
+
user: { login: "reviewer" },
|
|
290
|
+
},
|
|
291
|
+
pull_request: {
|
|
292
|
+
number: 99,
|
|
293
|
+
title: "Bot PR",
|
|
294
|
+
html_url: "https://github.com/test/repo/pull/99",
|
|
295
|
+
},
|
|
296
|
+
repository: BASE_REPO,
|
|
297
|
+
sender: { login: "reviewer" },
|
|
298
|
+
};
|
|
299
|
+
const commentResult = await handleComment(inlineCommentEvent, "pull_request_review_comment");
|
|
300
|
+
expect(commentResult.created).toBe(false);
|
|
301
|
+
|
|
302
|
+
// Total tasks must still be exactly 1 — no double-spawn
|
|
303
|
+
const taskCountAfter = getDb()
|
|
304
|
+
.prepare<{ n: number }, never>("SELECT COUNT(*) AS n FROM agent_tasks")
|
|
305
|
+
.get()!.n;
|
|
306
|
+
expect(taskCountAfter).toBe(1);
|
|
307
|
+
});
|
|
308
|
+
});
|