@ghcrawl/api-core 0.1.0 → 0.2.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.
- package/README.md +25 -0
- package/dist/api/server.d.ts +4 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +142 -0
- package/dist/api/server.js.map +1 -0
- package/dist/cluster/build.d.ts +16 -0
- package/dist/cluster/build.d.ts.map +1 -0
- package/dist/cluster/build.js +62 -0
- package/dist/cluster/build.js.map +1 -0
- package/dist/config.d.ts +83 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +257 -0
- package/dist/config.js.map +1 -0
- package/dist/db/migrate.d.ts +3 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/{src/db/migrate.ts → dist/db/migrate.js} +30 -36
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/sqlite.d.ts +4 -0
- package/dist/db/sqlite.d.ts.map +1 -0
- package/dist/db/sqlite.js +11 -0
- package/dist/db/sqlite.js.map +1 -0
- package/dist/documents/normalize.d.ts +23 -0
- package/dist/documents/normalize.d.ts.map +1 -0
- package/dist/documents/normalize.js +36 -0
- package/dist/documents/normalize.js.map +1 -0
- package/dist/github/client.d.ts +24 -0
- package/dist/github/client.d.ts.map +1 -0
- package/dist/github/client.js +170 -0
- package/dist/github/client.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/index.ts → dist/index.js} +1 -0
- package/dist/index.js.map +1 -0
- package/dist/openai/provider.d.ts +44 -0
- package/dist/openai/provider.d.ts.map +1 -0
- package/dist/openai/provider.js +107 -0
- package/dist/openai/provider.js.map +1 -0
- package/dist/search/exact.d.ts +14 -0
- package/dist/search/exact.d.ts.map +1 -0
- package/dist/search/exact.js +26 -0
- package/dist/search/exact.js.map +1 -0
- package/dist/service.d.ts +249 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +1801 -0
- package/dist/service.js.map +1 -0
- package/package.json +8 -6
- package/src/api/server.test.ts +0 -296
- package/src/api/server.ts +0 -171
- package/src/cluster/build.test.ts +0 -18
- package/src/cluster/build.ts +0 -74
- package/src/config.test.ts +0 -247
- package/src/config.ts +0 -421
- package/src/db/migrate.test.ts +0 -30
- package/src/db/sqlite.ts +0 -14
- package/src/documents/normalize.test.ts +0 -25
- package/src/documents/normalize.ts +0 -52
- package/src/github/client.ts +0 -241
- package/src/openai/provider.ts +0 -141
- package/src/search/exact.test.ts +0 -22
- package/src/search/exact.ts +0 -28
- package/src/service.test.ts +0 -2036
- package/src/service.ts +0 -2497
- package/src/types/better-sqlite3.d.ts +0 -1
package/src/service.test.ts
DELETED
|
@@ -1,2036 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
|
|
4
|
-
import { GHCrawlService } from './service.js';
|
|
5
|
-
|
|
6
|
-
function makeTestConfig(overrides: Partial<GHCrawlService['config']> = {}): GHCrawlService['config'] {
|
|
7
|
-
return {
|
|
8
|
-
workspaceRoot: process.cwd(),
|
|
9
|
-
configDir: '/tmp/ghcrawl-test',
|
|
10
|
-
configPath: '/tmp/ghcrawl-test/config.json',
|
|
11
|
-
configFileExists: true,
|
|
12
|
-
dbPath: ':memory:',
|
|
13
|
-
dbPathSource: 'config',
|
|
14
|
-
apiPort: 5179,
|
|
15
|
-
githubToken: 'ghp_testtoken1234567890',
|
|
16
|
-
githubTokenSource: 'config',
|
|
17
|
-
secretProvider: 'plaintext',
|
|
18
|
-
tuiPreferences: {},
|
|
19
|
-
openaiApiKeySource: 'none',
|
|
20
|
-
summaryModel: 'gpt-5-mini',
|
|
21
|
-
embedModel: 'text-embedding-3-large',
|
|
22
|
-
embedBatchSize: 2,
|
|
23
|
-
embedConcurrency: 2,
|
|
24
|
-
embedMaxUnread: 4,
|
|
25
|
-
openSearchIndex: 'ghcrawl-threads',
|
|
26
|
-
...overrides,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function makeTestService(
|
|
31
|
-
github: GHCrawlService['github'],
|
|
32
|
-
ai?: GHCrawlService['ai'],
|
|
33
|
-
): GHCrawlService {
|
|
34
|
-
return new GHCrawlService({
|
|
35
|
-
config: makeTestConfig(),
|
|
36
|
-
github,
|
|
37
|
-
ai,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
test('doctor reports config path and successful auth smoke checks', async () => {
|
|
42
|
-
let githubChecked = 0;
|
|
43
|
-
let openAiChecked = 0;
|
|
44
|
-
const service = new GHCrawlService({
|
|
45
|
-
config: makeTestConfig({
|
|
46
|
-
openaiApiKey: 'sk-proj-testkey1234567890',
|
|
47
|
-
openaiApiKeySource: 'config',
|
|
48
|
-
}),
|
|
49
|
-
github: {
|
|
50
|
-
checkAuth: async () => {
|
|
51
|
-
githubChecked += 1;
|
|
52
|
-
},
|
|
53
|
-
getRepo: async () => ({}),
|
|
54
|
-
listRepositoryIssues: async () => [],
|
|
55
|
-
getIssue: async () => ({}),
|
|
56
|
-
getPull: async () => ({}),
|
|
57
|
-
listIssueComments: async () => [],
|
|
58
|
-
listPullReviews: async () => [],
|
|
59
|
-
listPullReviewComments: async () => [],
|
|
60
|
-
},
|
|
61
|
-
ai: {
|
|
62
|
-
checkAuth: async () => {
|
|
63
|
-
openAiChecked += 1;
|
|
64
|
-
},
|
|
65
|
-
summarizeThread: async () => {
|
|
66
|
-
throw new Error('not expected');
|
|
67
|
-
},
|
|
68
|
-
embedTexts: async () => [],
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const result = await service.doctor();
|
|
74
|
-
assert.equal(result.health.configPath, '/tmp/ghcrawl-test/config.json');
|
|
75
|
-
assert.equal(result.github.formatOk, true);
|
|
76
|
-
assert.equal(result.github.authOk, true);
|
|
77
|
-
assert.equal(result.openai.formatOk, true);
|
|
78
|
-
assert.equal(result.openai.authOk, true);
|
|
79
|
-
assert.equal(githubChecked, 1);
|
|
80
|
-
assert.equal(openAiChecked, 1);
|
|
81
|
-
} finally {
|
|
82
|
-
service.close();
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('doctor reports invalid token format without attempting auth', async () => {
|
|
87
|
-
let githubChecked = 0;
|
|
88
|
-
const service = new GHCrawlService({
|
|
89
|
-
config: makeTestConfig({
|
|
90
|
-
githubToken: 'not-a-token',
|
|
91
|
-
}),
|
|
92
|
-
github: {
|
|
93
|
-
checkAuth: async () => {
|
|
94
|
-
githubChecked += 1;
|
|
95
|
-
},
|
|
96
|
-
getRepo: async () => ({}),
|
|
97
|
-
listRepositoryIssues: async () => [],
|
|
98
|
-
getIssue: async () => ({}),
|
|
99
|
-
getPull: async () => ({}),
|
|
100
|
-
listIssueComments: async () => [],
|
|
101
|
-
listPullReviews: async () => [],
|
|
102
|
-
listPullReviewComments: async () => [],
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const result = await service.doctor();
|
|
108
|
-
assert.equal(result.github.formatOk, false);
|
|
109
|
-
assert.equal(result.github.authOk, false);
|
|
110
|
-
assert.match(result.github.error ?? '', /does not look like a GitHub personal access token/);
|
|
111
|
-
assert.equal(githubChecked, 0);
|
|
112
|
-
} finally {
|
|
113
|
-
service.close();
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('doctor explains when secrets are expected from 1Password CLI env injection', async () => {
|
|
118
|
-
const service = new GHCrawlService({
|
|
119
|
-
config: makeTestConfig({
|
|
120
|
-
githubToken: undefined,
|
|
121
|
-
githubTokenSource: 'none',
|
|
122
|
-
openaiApiKey: undefined,
|
|
123
|
-
openaiApiKeySource: 'none',
|
|
124
|
-
secretProvider: 'op',
|
|
125
|
-
opVaultName: 'PwrDrvr LLC',
|
|
126
|
-
opItemName: 'ghcrawl',
|
|
127
|
-
}),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
const result = await service.doctor();
|
|
132
|
-
assert.equal(result.github.configured, false);
|
|
133
|
-
assert.match(result.github.error ?? '', /1Password CLI/);
|
|
134
|
-
assert.equal(result.openai.configured, false);
|
|
135
|
-
assert.match(result.openai.error ?? '', /OPENAI_API_KEY/);
|
|
136
|
-
} finally {
|
|
137
|
-
service.close();
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('syncRepository defaults to metadata-only mode, preserves thread kind, and tracks first/last pull timestamps', async () => {
|
|
142
|
-
const messages: string[] = [];
|
|
143
|
-
let listIssueCommentCalls = 0;
|
|
144
|
-
let listPullReviewCalls = 0;
|
|
145
|
-
let listPullReviewCommentCalls = 0;
|
|
146
|
-
const service = makeTestService({
|
|
147
|
-
checkAuth: async () => undefined,
|
|
148
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
149
|
-
listRepositoryIssues: async (_owner, _repo, _since, limit) =>
|
|
150
|
-
[
|
|
151
|
-
{
|
|
152
|
-
id: 100,
|
|
153
|
-
number: 42,
|
|
154
|
-
state: 'open',
|
|
155
|
-
title: 'Downloader hangs',
|
|
156
|
-
body: 'The transfer never finishes.',
|
|
157
|
-
html_url: 'https://github.com/openclaw/openclaw/issues/42',
|
|
158
|
-
labels: [{ name: 'bug' }],
|
|
159
|
-
assignees: [],
|
|
160
|
-
user: { login: 'alice', type: 'User' },
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
id: 101,
|
|
164
|
-
number: 43,
|
|
165
|
-
state: 'open',
|
|
166
|
-
title: 'Downloader PR',
|
|
167
|
-
body: 'Implements a fix.',
|
|
168
|
-
html_url: 'https://github.com/openclaw/openclaw/pull/43',
|
|
169
|
-
labels: [{ name: 'bug' }],
|
|
170
|
-
assignees: [],
|
|
171
|
-
pull_request: { url: 'https://api.github.com/repos/openclaw/openclaw/pulls/43' },
|
|
172
|
-
user: { login: 'alice', type: 'User' },
|
|
173
|
-
},
|
|
174
|
-
].slice(0, limit ?? 2),
|
|
175
|
-
getIssue: async (_owner, _repo, number) => ({
|
|
176
|
-
id: 100,
|
|
177
|
-
number,
|
|
178
|
-
state: 'open',
|
|
179
|
-
title: 'Downloader hangs',
|
|
180
|
-
body: 'The transfer never finishes.',
|
|
181
|
-
html_url: `https://github.com/openclaw/openclaw/issues/${number}`,
|
|
182
|
-
labels: [{ name: 'bug' }],
|
|
183
|
-
assignees: [],
|
|
184
|
-
user: { login: 'alice', type: 'User' },
|
|
185
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
186
|
-
}),
|
|
187
|
-
getPull: async (_owner, _repo, number) => ({
|
|
188
|
-
id: 101,
|
|
189
|
-
number,
|
|
190
|
-
state: 'open',
|
|
191
|
-
title: 'Downloader PR',
|
|
192
|
-
body: 'Implements a fix.',
|
|
193
|
-
html_url: `https://github.com/openclaw/openclaw/pull/${number}`,
|
|
194
|
-
labels: [{ name: 'bug' }],
|
|
195
|
-
assignees: [],
|
|
196
|
-
user: { login: 'alice', type: 'User' },
|
|
197
|
-
draft: false,
|
|
198
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
199
|
-
}),
|
|
200
|
-
listIssueComments: async () => {
|
|
201
|
-
listIssueCommentCalls += 1;
|
|
202
|
-
return [
|
|
203
|
-
{
|
|
204
|
-
id: 200,
|
|
205
|
-
body: 'same here',
|
|
206
|
-
created_at: '2026-03-09T00:00:00Z',
|
|
207
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
208
|
-
user: { login: 'bob', type: 'User' },
|
|
209
|
-
},
|
|
210
|
-
];
|
|
211
|
-
},
|
|
212
|
-
listPullReviews: async () => {
|
|
213
|
-
listPullReviewCalls += 1;
|
|
214
|
-
return [];
|
|
215
|
-
},
|
|
216
|
-
listPullReviewComments: async () => {
|
|
217
|
-
listPullReviewCommentCalls += 1;
|
|
218
|
-
return [];
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const result = await service.syncRepository({
|
|
224
|
-
owner: 'openclaw',
|
|
225
|
-
repo: 'openclaw',
|
|
226
|
-
limit: 2,
|
|
227
|
-
onProgress: (message) => messages.push(message),
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
assert.equal(result.threadsSynced, 2);
|
|
231
|
-
assert.equal(result.commentsSynced, 0);
|
|
232
|
-
assert.equal(result.threadsClosed, 0);
|
|
233
|
-
assert.match(messages.join('\n'), /discovered 2 threads/);
|
|
234
|
-
assert.match(messages.join('\n'), /1\/2 issue #42/);
|
|
235
|
-
assert.match(messages.join('\n'), /2\/2 pull_request #43/);
|
|
236
|
-
assert.match(messages.join('\n'), /metadata-only mode; skipping comment, review, and review-comment fetches/);
|
|
237
|
-
assert.equal(service.listRepositories().repositories.length, 1);
|
|
238
|
-
assert.equal(service.listThreads({ owner: 'openclaw', repo: 'openclaw' }).threads.length, 2);
|
|
239
|
-
assert.equal(listIssueCommentCalls, 0);
|
|
240
|
-
assert.equal(listPullReviewCalls, 0);
|
|
241
|
-
assert.equal(listPullReviewCommentCalls, 0);
|
|
242
|
-
|
|
243
|
-
const rows = service.db
|
|
244
|
-
.prepare('select number, kind, first_pulled_at, last_pulled_at from threads order by number asc')
|
|
245
|
-
.all() as Array<{
|
|
246
|
-
number: number;
|
|
247
|
-
kind: 'issue' | 'pull_request';
|
|
248
|
-
first_pulled_at: string | null;
|
|
249
|
-
last_pulled_at: string | null;
|
|
250
|
-
}>;
|
|
251
|
-
|
|
252
|
-
assert.deepEqual(
|
|
253
|
-
rows.map((row) => ({ number: row.number, kind: row.kind })),
|
|
254
|
-
[
|
|
255
|
-
{ number: 42, kind: 'issue' },
|
|
256
|
-
{ number: 43, kind: 'pull_request' },
|
|
257
|
-
],
|
|
258
|
-
);
|
|
259
|
-
for (const row of rows) {
|
|
260
|
-
assert.ok(row.first_pulled_at);
|
|
261
|
-
assert.ok(row.last_pulled_at);
|
|
262
|
-
assert.equal(row.first_pulled_at, row.last_pulled_at);
|
|
263
|
-
}
|
|
264
|
-
} finally {
|
|
265
|
-
service.close();
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test('syncRepository fetches comments, reviews, and review comments when includeComments is enabled', async () => {
|
|
270
|
-
let listIssueCommentCalls = 0;
|
|
271
|
-
let listPullReviewCalls = 0;
|
|
272
|
-
let listPullReviewCommentCalls = 0;
|
|
273
|
-
|
|
274
|
-
const service = makeTestService({
|
|
275
|
-
checkAuth: async () => undefined,
|
|
276
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
277
|
-
listRepositoryIssues: async () => [
|
|
278
|
-
{
|
|
279
|
-
id: 101,
|
|
280
|
-
number: 43,
|
|
281
|
-
state: 'open',
|
|
282
|
-
title: 'Downloader PR',
|
|
283
|
-
body: 'Implements a fix.',
|
|
284
|
-
html_url: 'https://github.com/openclaw/openclaw/pull/43',
|
|
285
|
-
labels: [{ name: 'bug' }],
|
|
286
|
-
assignees: [],
|
|
287
|
-
pull_request: { url: 'https://api.github.com/repos/openclaw/openclaw/pulls/43' },
|
|
288
|
-
user: { login: 'alice', type: 'User' },
|
|
289
|
-
},
|
|
290
|
-
],
|
|
291
|
-
getIssue: async () => {
|
|
292
|
-
throw new Error('not expected');
|
|
293
|
-
},
|
|
294
|
-
getPull: async (_owner, _repo, number) => ({
|
|
295
|
-
id: 101,
|
|
296
|
-
number,
|
|
297
|
-
state: 'open',
|
|
298
|
-
title: 'Downloader PR',
|
|
299
|
-
body: 'Implements a fix.',
|
|
300
|
-
html_url: `https://github.com/openclaw/openclaw/pull/${number}`,
|
|
301
|
-
labels: [{ name: 'bug' }],
|
|
302
|
-
assignees: [],
|
|
303
|
-
user: { login: 'alice', type: 'User' },
|
|
304
|
-
draft: false,
|
|
305
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
306
|
-
}),
|
|
307
|
-
listIssueComments: async () => {
|
|
308
|
-
listIssueCommentCalls += 1;
|
|
309
|
-
return [
|
|
310
|
-
{
|
|
311
|
-
id: 200,
|
|
312
|
-
body: 'same here',
|
|
313
|
-
created_at: '2026-03-09T00:00:00Z',
|
|
314
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
315
|
-
user: { login: 'bob', type: 'User' },
|
|
316
|
-
},
|
|
317
|
-
];
|
|
318
|
-
},
|
|
319
|
-
listPullReviews: async () => {
|
|
320
|
-
listPullReviewCalls += 1;
|
|
321
|
-
return [
|
|
322
|
-
{
|
|
323
|
-
id: 300,
|
|
324
|
-
body: 'Looks good',
|
|
325
|
-
state: 'APPROVED',
|
|
326
|
-
submitted_at: '2026-03-09T00:00:00Z',
|
|
327
|
-
user: { login: 'carol', type: 'User' },
|
|
328
|
-
},
|
|
329
|
-
];
|
|
330
|
-
},
|
|
331
|
-
listPullReviewComments: async () => {
|
|
332
|
-
listPullReviewCommentCalls += 1;
|
|
333
|
-
return [
|
|
334
|
-
{
|
|
335
|
-
id: 400,
|
|
336
|
-
body: 'Please rename this variable',
|
|
337
|
-
created_at: '2026-03-09T00:00:00Z',
|
|
338
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
339
|
-
user: { login: 'dave', type: 'User' },
|
|
340
|
-
},
|
|
341
|
-
];
|
|
342
|
-
},
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
try {
|
|
346
|
-
const result = await service.syncRepository({
|
|
347
|
-
owner: 'openclaw',
|
|
348
|
-
repo: 'openclaw',
|
|
349
|
-
includeComments: true,
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
assert.equal(result.threadsSynced, 1);
|
|
353
|
-
assert.equal(result.commentsSynced, 3);
|
|
354
|
-
assert.equal(listIssueCommentCalls, 1);
|
|
355
|
-
assert.equal(listPullReviewCalls, 1);
|
|
356
|
-
assert.equal(listPullReviewCommentCalls, 1);
|
|
357
|
-
|
|
358
|
-
const commentCount = service.db.prepare('select count(*) as count from comments').get() as { count: number };
|
|
359
|
-
assert.equal(commentCount.count, 3);
|
|
360
|
-
} finally {
|
|
361
|
-
service.close();
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
test('summarizeRepository excludes hydrated comments by default and reports token usage', async () => {
|
|
366
|
-
const summaryInputs: string[] = [];
|
|
367
|
-
const service = makeTestService(
|
|
368
|
-
{
|
|
369
|
-
checkAuth: async () => undefined,
|
|
370
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
371
|
-
listRepositoryIssues: async () => [],
|
|
372
|
-
getIssue: async () => {
|
|
373
|
-
throw new Error('not expected');
|
|
374
|
-
},
|
|
375
|
-
getPull: async () => {
|
|
376
|
-
throw new Error('not expected');
|
|
377
|
-
},
|
|
378
|
-
listIssueComments: async () => [],
|
|
379
|
-
listPullReviews: async () => [],
|
|
380
|
-
listPullReviewComments: async () => [],
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
checkAuth: async () => undefined,
|
|
384
|
-
summarizeThread: async ({ text }) => {
|
|
385
|
-
summaryInputs.push(text);
|
|
386
|
-
return {
|
|
387
|
-
summary: {
|
|
388
|
-
problemSummary: 'Problem',
|
|
389
|
-
solutionSummary: 'Solution',
|
|
390
|
-
maintainerSignalSummary: 'Signal',
|
|
391
|
-
dedupeSummary: 'Dedupe',
|
|
392
|
-
},
|
|
393
|
-
usage: {
|
|
394
|
-
inputTokens: 123,
|
|
395
|
-
outputTokens: 45,
|
|
396
|
-
totalTokens: 168,
|
|
397
|
-
cachedInputTokens: 0,
|
|
398
|
-
reasoningTokens: 0,
|
|
399
|
-
},
|
|
400
|
-
};
|
|
401
|
-
},
|
|
402
|
-
embedTexts: async () => [],
|
|
403
|
-
},
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
try {
|
|
407
|
-
const now = '2026-03-09T00:00:00Z';
|
|
408
|
-
service.db
|
|
409
|
-
.prepare(
|
|
410
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
411
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
412
|
-
)
|
|
413
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
414
|
-
service.db
|
|
415
|
-
.prepare(
|
|
416
|
-
`insert into threads (
|
|
417
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
418
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
419
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
420
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
421
|
-
)
|
|
422
|
-
.run(
|
|
423
|
-
10,
|
|
424
|
-
1,
|
|
425
|
-
'100',
|
|
426
|
-
42,
|
|
427
|
-
'issue',
|
|
428
|
-
'open',
|
|
429
|
-
'Downloader hangs',
|
|
430
|
-
'The transfer never finishes.',
|
|
431
|
-
'alice',
|
|
432
|
-
'User',
|
|
433
|
-
'https://github.com/openclaw/openclaw/issues/42',
|
|
434
|
-
'["bug"]',
|
|
435
|
-
'[]',
|
|
436
|
-
'{}',
|
|
437
|
-
'hash-42',
|
|
438
|
-
0,
|
|
439
|
-
now,
|
|
440
|
-
now,
|
|
441
|
-
null,
|
|
442
|
-
null,
|
|
443
|
-
now,
|
|
444
|
-
now,
|
|
445
|
-
now,
|
|
446
|
-
);
|
|
447
|
-
service.db
|
|
448
|
-
.prepare(
|
|
449
|
-
`insert into comments (
|
|
450
|
-
thread_id, github_id, comment_type, author_login, author_type, body, is_bot, raw_json, created_at_gh, updated_at_gh
|
|
451
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
452
|
-
)
|
|
453
|
-
.run(10, '200', 'issue_comment', 'human', 'User', 'This extra comment should stay out.', 0, '{}', now, now);
|
|
454
|
-
const result = await service.summarizeRepository({
|
|
455
|
-
owner: 'openclaw',
|
|
456
|
-
repo: 'openclaw',
|
|
457
|
-
threadNumber: 42,
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
assert.equal(result.summarized, 1);
|
|
461
|
-
assert.equal(result.inputTokens, 123);
|
|
462
|
-
assert.equal(result.outputTokens, 45);
|
|
463
|
-
assert.equal(result.totalTokens, 168);
|
|
464
|
-
assert.equal(summaryInputs.length, 1);
|
|
465
|
-
assert.match(summaryInputs[0], /title: Downloader hangs/);
|
|
466
|
-
assert.match(summaryInputs[0], /body: The transfer never finishes\./);
|
|
467
|
-
assert.doesNotMatch(summaryInputs[0], /This extra comment should stay out/);
|
|
468
|
-
} finally {
|
|
469
|
-
service.close();
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
test('summarizeRepository includes hydrated human comments when includeComments is enabled', async () => {
|
|
474
|
-
const summaryInputs: string[] = [];
|
|
475
|
-
const service = makeTestService(
|
|
476
|
-
{
|
|
477
|
-
checkAuth: async () => undefined,
|
|
478
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
479
|
-
listRepositoryIssues: async () => [],
|
|
480
|
-
getIssue: async () => {
|
|
481
|
-
throw new Error('not expected');
|
|
482
|
-
},
|
|
483
|
-
getPull: async () => {
|
|
484
|
-
throw new Error('not expected');
|
|
485
|
-
},
|
|
486
|
-
listIssueComments: async () => [],
|
|
487
|
-
listPullReviews: async () => [],
|
|
488
|
-
listPullReviewComments: async () => [],
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
checkAuth: async () => undefined,
|
|
492
|
-
summarizeThread: async ({ text }) => {
|
|
493
|
-
summaryInputs.push(text);
|
|
494
|
-
return {
|
|
495
|
-
summary: {
|
|
496
|
-
problemSummary: 'Problem',
|
|
497
|
-
solutionSummary: 'Solution',
|
|
498
|
-
maintainerSignalSummary: 'Signal',
|
|
499
|
-
dedupeSummary: 'Dedupe',
|
|
500
|
-
},
|
|
501
|
-
};
|
|
502
|
-
},
|
|
503
|
-
embedTexts: async () => [],
|
|
504
|
-
},
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
try {
|
|
508
|
-
const now = '2026-03-09T00:00:00Z';
|
|
509
|
-
service.db
|
|
510
|
-
.prepare(
|
|
511
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
512
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
513
|
-
)
|
|
514
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
515
|
-
service.db
|
|
516
|
-
.prepare(
|
|
517
|
-
`insert into threads (
|
|
518
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
519
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
520
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
521
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
522
|
-
)
|
|
523
|
-
.run(
|
|
524
|
-
10,
|
|
525
|
-
1,
|
|
526
|
-
'100',
|
|
527
|
-
42,
|
|
528
|
-
'issue',
|
|
529
|
-
'open',
|
|
530
|
-
'Downloader hangs',
|
|
531
|
-
'The transfer never finishes.',
|
|
532
|
-
'alice',
|
|
533
|
-
'User',
|
|
534
|
-
'https://github.com/openclaw/openclaw/issues/42',
|
|
535
|
-
'["bug"]',
|
|
536
|
-
'[]',
|
|
537
|
-
'{}',
|
|
538
|
-
'hash-42',
|
|
539
|
-
0,
|
|
540
|
-
now,
|
|
541
|
-
now,
|
|
542
|
-
null,
|
|
543
|
-
null,
|
|
544
|
-
now,
|
|
545
|
-
now,
|
|
546
|
-
now,
|
|
547
|
-
);
|
|
548
|
-
service.db
|
|
549
|
-
.prepare(
|
|
550
|
-
`insert into comments (
|
|
551
|
-
thread_id, github_id, comment_type, author_login, author_type, body, is_bot, raw_json, created_at_gh, updated_at_gh
|
|
552
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
553
|
-
)
|
|
554
|
-
.run(10, '200', 'issue_comment', 'human', 'User', 'Same here on macOS.', 0, '{}', now, now);
|
|
555
|
-
service.db
|
|
556
|
-
.prepare(
|
|
557
|
-
`insert into comments (
|
|
558
|
-
thread_id, github_id, comment_type, author_login, author_type, body, is_bot, raw_json, created_at_gh, updated_at_gh
|
|
559
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
560
|
-
)
|
|
561
|
-
.run(10, '201', 'issue_comment', 'dependabot[bot]', 'Bot', 'Noise', 1, '{}', now, now);
|
|
562
|
-
|
|
563
|
-
const result = await service.summarizeRepository({
|
|
564
|
-
owner: 'openclaw',
|
|
565
|
-
repo: 'openclaw',
|
|
566
|
-
threadNumber: 42,
|
|
567
|
-
includeComments: true,
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
assert.equal(result.summarized, 1);
|
|
571
|
-
assert.equal(summaryInputs.length, 1);
|
|
572
|
-
assert.match(summaryInputs[0], /discussion:/);
|
|
573
|
-
assert.match(summaryInputs[0], /@human: Same here on macOS\./);
|
|
574
|
-
assert.doesNotMatch(summaryInputs[0], /dependabot/);
|
|
575
|
-
} finally {
|
|
576
|
-
service.close();
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
test('purgeComments removes hydrated comments and refreshes canonical documents', () => {
|
|
581
|
-
const service = makeTestService({
|
|
582
|
-
checkAuth: async () => undefined,
|
|
583
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
584
|
-
listRepositoryIssues: async () => [],
|
|
585
|
-
getIssue: async () => {
|
|
586
|
-
throw new Error('not expected');
|
|
587
|
-
},
|
|
588
|
-
getPull: async () => {
|
|
589
|
-
throw new Error('not expected');
|
|
590
|
-
},
|
|
591
|
-
listIssueComments: async () => [],
|
|
592
|
-
listPullReviews: async () => [],
|
|
593
|
-
listPullReviewComments: async () => [],
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
try {
|
|
597
|
-
const now = '2026-03-09T00:00:00Z';
|
|
598
|
-
service.db
|
|
599
|
-
.prepare(
|
|
600
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
601
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
602
|
-
)
|
|
603
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
604
|
-
service.db
|
|
605
|
-
.prepare(
|
|
606
|
-
`insert into threads (
|
|
607
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
608
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
609
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
610
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
611
|
-
)
|
|
612
|
-
.run(
|
|
613
|
-
10,
|
|
614
|
-
1,
|
|
615
|
-
'100',
|
|
616
|
-
42,
|
|
617
|
-
'issue',
|
|
618
|
-
'open',
|
|
619
|
-
'Downloader hangs',
|
|
620
|
-
'The transfer never finishes.',
|
|
621
|
-
'alice',
|
|
622
|
-
'User',
|
|
623
|
-
'https://github.com/openclaw/openclaw/issues/42',
|
|
624
|
-
'["bug"]',
|
|
625
|
-
'[]',
|
|
626
|
-
'{}',
|
|
627
|
-
'hash-42',
|
|
628
|
-
0,
|
|
629
|
-
now,
|
|
630
|
-
now,
|
|
631
|
-
null,
|
|
632
|
-
null,
|
|
633
|
-
now,
|
|
634
|
-
now,
|
|
635
|
-
now,
|
|
636
|
-
);
|
|
637
|
-
service.db
|
|
638
|
-
.prepare(
|
|
639
|
-
`insert into comments (
|
|
640
|
-
thread_id, github_id, comment_type, author_login, author_type, body, is_bot, raw_json, created_at_gh, updated_at_gh
|
|
641
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
642
|
-
)
|
|
643
|
-
.run(10, '200', 'issue_comment', 'human', 'User', 'Same here on macOS.', 0, '{}', now, now);
|
|
644
|
-
service.db
|
|
645
|
-
.prepare(
|
|
646
|
-
`insert into documents (thread_id, title, body, raw_text, dedupe_text, updated_at)
|
|
647
|
-
values (?, ?, ?, ?, ?, ?)`,
|
|
648
|
-
)
|
|
649
|
-
.run(
|
|
650
|
-
10,
|
|
651
|
-
'Downloader hangs',
|
|
652
|
-
'The transfer never finishes.',
|
|
653
|
-
'Downloader hangs\n\nThe transfer never finishes.\n\nSame here on macOS.',
|
|
654
|
-
'title: Downloader hangs\n\nbody: The transfer never finishes.\n\ndiscussion: @human: Same here on macOS.',
|
|
655
|
-
now,
|
|
656
|
-
);
|
|
657
|
-
|
|
658
|
-
const before = service.db.prepare('select dedupe_text from documents where thread_id = ?').get(10) as { dedupe_text: string };
|
|
659
|
-
assert.match(before.dedupe_text, /discussion:/);
|
|
660
|
-
|
|
661
|
-
const result = service.purgeComments({ owner: 'openclaw', repo: 'openclaw' });
|
|
662
|
-
|
|
663
|
-
const count = service.db.prepare('select count(*) as count from comments').get() as { count: number };
|
|
664
|
-
const after = service.db.prepare('select dedupe_text from documents where thread_id = ?').get(10) as { dedupe_text: string };
|
|
665
|
-
|
|
666
|
-
assert.equal(result.purgedComments, 1);
|
|
667
|
-
assert.equal(result.refreshedThreads, 1);
|
|
668
|
-
assert.equal(count.count, 0);
|
|
669
|
-
assert.doesNotMatch(after.dedupe_text, /discussion:/);
|
|
670
|
-
} finally {
|
|
671
|
-
service.close();
|
|
672
|
-
}
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
test('embedRepository batches multi-source embeddings and skips unchanged inputs by hash', async () => {
|
|
676
|
-
const embedCalls: string[][] = [];
|
|
677
|
-
const service = makeTestService(
|
|
678
|
-
{
|
|
679
|
-
checkAuth: async () => undefined,
|
|
680
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
681
|
-
listRepositoryIssues: async () => [],
|
|
682
|
-
getIssue: async () => {
|
|
683
|
-
throw new Error('not expected');
|
|
684
|
-
},
|
|
685
|
-
getPull: async () => {
|
|
686
|
-
throw new Error('not expected');
|
|
687
|
-
},
|
|
688
|
-
listIssueComments: async () => [],
|
|
689
|
-
listPullReviews: async () => [],
|
|
690
|
-
listPullReviewComments: async () => [],
|
|
691
|
-
},
|
|
692
|
-
{
|
|
693
|
-
checkAuth: async () => undefined,
|
|
694
|
-
summarizeThread: async () => {
|
|
695
|
-
throw new Error('not expected');
|
|
696
|
-
},
|
|
697
|
-
embedTexts: async ({ texts }) => {
|
|
698
|
-
embedCalls.push(texts);
|
|
699
|
-
return texts.map((text, index) => [text.length, index]);
|
|
700
|
-
},
|
|
701
|
-
},
|
|
702
|
-
);
|
|
703
|
-
|
|
704
|
-
try {
|
|
705
|
-
const now = '2026-03-09T00:00:00Z';
|
|
706
|
-
service.db
|
|
707
|
-
.prepare(
|
|
708
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
709
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
710
|
-
)
|
|
711
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
712
|
-
service.db
|
|
713
|
-
.prepare(
|
|
714
|
-
`insert into threads (
|
|
715
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
716
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
717
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
718
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
719
|
-
)
|
|
720
|
-
.run(
|
|
721
|
-
10,
|
|
722
|
-
1,
|
|
723
|
-
'100',
|
|
724
|
-
42,
|
|
725
|
-
'issue',
|
|
726
|
-
'open',
|
|
727
|
-
'Downloader hangs',
|
|
728
|
-
'The transfer never finishes.',
|
|
729
|
-
'alice',
|
|
730
|
-
'User',
|
|
731
|
-
'https://github.com/openclaw/openclaw/issues/42',
|
|
732
|
-
'["bug"]',
|
|
733
|
-
'[]',
|
|
734
|
-
'{}',
|
|
735
|
-
'hash-42',
|
|
736
|
-
0,
|
|
737
|
-
now,
|
|
738
|
-
now,
|
|
739
|
-
null,
|
|
740
|
-
null,
|
|
741
|
-
now,
|
|
742
|
-
now,
|
|
743
|
-
now,
|
|
744
|
-
);
|
|
745
|
-
service.db
|
|
746
|
-
.prepare(
|
|
747
|
-
`insert into document_summaries (thread_id, summary_kind, model, content_hash, summary_text, created_at, updated_at)
|
|
748
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
749
|
-
)
|
|
750
|
-
.run(10, 'dedupe_summary', 'gpt-5-mini', 'summary-hash', 'Transfer hangs near completion.', now, now);
|
|
751
|
-
|
|
752
|
-
const first = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' });
|
|
753
|
-
assert.equal(first.embedded, 3);
|
|
754
|
-
assert.equal(embedCalls.length, 2);
|
|
755
|
-
assert.deepEqual(
|
|
756
|
-
service.db
|
|
757
|
-
.prepare('select source_kind from document_embeddings order by source_kind asc')
|
|
758
|
-
.all()
|
|
759
|
-
.map((row: unknown) => (row as { source_kind: string }).source_kind),
|
|
760
|
-
['body', 'dedupe_summary', 'title'],
|
|
761
|
-
);
|
|
762
|
-
|
|
763
|
-
const second = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' });
|
|
764
|
-
assert.equal(second.embedded, 0);
|
|
765
|
-
assert.equal(embedCalls.length, 2);
|
|
766
|
-
|
|
767
|
-
service.db
|
|
768
|
-
.prepare('update threads set body = ?, updated_at = ? where id = ?')
|
|
769
|
-
.run('The transfer now stalls at 99%.', now, 10);
|
|
770
|
-
const third = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' });
|
|
771
|
-
assert.equal(third.embedded, 1);
|
|
772
|
-
assert.equal(embedCalls.length, 3);
|
|
773
|
-
assert.deepEqual(embedCalls[2], ['The transfer now stalls at 99%.']);
|
|
774
|
-
} finally {
|
|
775
|
-
service.close();
|
|
776
|
-
}
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
test('embedRepository truncates oversized inputs before submission', async () => {
|
|
780
|
-
const embedCalls: string[][] = [];
|
|
781
|
-
const service = new GHCrawlService({
|
|
782
|
-
config: makeTestConfig({
|
|
783
|
-
embedBatchSize: 8,
|
|
784
|
-
embedConcurrency: 1,
|
|
785
|
-
embedMaxUnread: 2,
|
|
786
|
-
}),
|
|
787
|
-
github: {
|
|
788
|
-
checkAuth: async () => undefined,
|
|
789
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
790
|
-
listRepositoryIssues: async () => [],
|
|
791
|
-
getIssue: async () => {
|
|
792
|
-
throw new Error('not expected');
|
|
793
|
-
},
|
|
794
|
-
getPull: async () => {
|
|
795
|
-
throw new Error('not expected');
|
|
796
|
-
},
|
|
797
|
-
listIssueComments: async () => [],
|
|
798
|
-
listPullReviews: async () => [],
|
|
799
|
-
listPullReviewComments: async () => [],
|
|
800
|
-
},
|
|
801
|
-
ai: {
|
|
802
|
-
checkAuth: async () => undefined,
|
|
803
|
-
summarizeThread: async () => {
|
|
804
|
-
throw new Error('not expected');
|
|
805
|
-
},
|
|
806
|
-
embedTexts: async ({ texts }) => {
|
|
807
|
-
embedCalls.push(texts);
|
|
808
|
-
return texts.map((text, index) => [text.length, index]);
|
|
809
|
-
},
|
|
810
|
-
},
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
try {
|
|
814
|
-
const now = '2026-03-09T00:00:00Z';
|
|
815
|
-
const hugeBody = 'a'.repeat(30000);
|
|
816
|
-
service.db
|
|
817
|
-
.prepare(
|
|
818
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
819
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
820
|
-
)
|
|
821
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
822
|
-
service.db
|
|
823
|
-
.prepare(
|
|
824
|
-
`insert into threads (
|
|
825
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
826
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
827
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
828
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
829
|
-
)
|
|
830
|
-
.run(
|
|
831
|
-
10,
|
|
832
|
-
1,
|
|
833
|
-
'100',
|
|
834
|
-
42,
|
|
835
|
-
'issue',
|
|
836
|
-
'open',
|
|
837
|
-
'Huge body one',
|
|
838
|
-
hugeBody,
|
|
839
|
-
'alice',
|
|
840
|
-
'User',
|
|
841
|
-
'https://github.com/openclaw/openclaw/issues/42',
|
|
842
|
-
'[]',
|
|
843
|
-
'[]',
|
|
844
|
-
'{}',
|
|
845
|
-
'hash-42',
|
|
846
|
-
0,
|
|
847
|
-
now,
|
|
848
|
-
now,
|
|
849
|
-
null,
|
|
850
|
-
null,
|
|
851
|
-
now,
|
|
852
|
-
now,
|
|
853
|
-
now,
|
|
854
|
-
);
|
|
855
|
-
service.db
|
|
856
|
-
.prepare(
|
|
857
|
-
`insert into threads (
|
|
858
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
859
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
860
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
861
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
862
|
-
)
|
|
863
|
-
.run(
|
|
864
|
-
11,
|
|
865
|
-
1,
|
|
866
|
-
'101',
|
|
867
|
-
43,
|
|
868
|
-
'issue',
|
|
869
|
-
'open',
|
|
870
|
-
'Huge body two',
|
|
871
|
-
hugeBody,
|
|
872
|
-
'bob',
|
|
873
|
-
'User',
|
|
874
|
-
'https://github.com/openclaw/openclaw/issues/43',
|
|
875
|
-
'[]',
|
|
876
|
-
'[]',
|
|
877
|
-
'{}',
|
|
878
|
-
'hash-43',
|
|
879
|
-
0,
|
|
880
|
-
now,
|
|
881
|
-
now,
|
|
882
|
-
null,
|
|
883
|
-
null,
|
|
884
|
-
now,
|
|
885
|
-
now,
|
|
886
|
-
now,
|
|
887
|
-
);
|
|
888
|
-
|
|
889
|
-
const result = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' });
|
|
890
|
-
|
|
891
|
-
assert.equal(result.embedded, 4);
|
|
892
|
-
assert.ok(embedCalls.length >= 1);
|
|
893
|
-
const truncatedBodies = embedCalls.flat().filter((text) => text.includes('[truncated for embedding]'));
|
|
894
|
-
assert.equal(truncatedBodies.length, 2);
|
|
895
|
-
for (const text of truncatedBodies) {
|
|
896
|
-
assert.ok(text.length < hugeBody.length);
|
|
897
|
-
}
|
|
898
|
-
} finally {
|
|
899
|
-
service.close();
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
test('embedRepository isolates a failing oversized item from a mixed batch and retries it shortened', async () => {
|
|
904
|
-
const embedCalls: string[][] = [];
|
|
905
|
-
const service = new GHCrawlService({
|
|
906
|
-
config: makeTestConfig({
|
|
907
|
-
embedBatchSize: 8,
|
|
908
|
-
embedConcurrency: 1,
|
|
909
|
-
embedMaxUnread: 2,
|
|
910
|
-
}),
|
|
911
|
-
github: {
|
|
912
|
-
checkAuth: async () => undefined,
|
|
913
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
914
|
-
listRepositoryIssues: async () => [],
|
|
915
|
-
getIssue: async () => {
|
|
916
|
-
throw new Error('not expected');
|
|
917
|
-
},
|
|
918
|
-
getPull: async () => {
|
|
919
|
-
throw new Error('not expected');
|
|
920
|
-
},
|
|
921
|
-
listIssueComments: async () => [],
|
|
922
|
-
listPullReviews: async () => [],
|
|
923
|
-
listPullReviewComments: async () => [],
|
|
924
|
-
},
|
|
925
|
-
ai: {
|
|
926
|
-
checkAuth: async () => undefined,
|
|
927
|
-
summarizeThread: async () => {
|
|
928
|
-
throw new Error('not expected');
|
|
929
|
-
},
|
|
930
|
-
embedTexts: async ({ texts }) => {
|
|
931
|
-
embedCalls.push(texts);
|
|
932
|
-
for (const text of texts) {
|
|
933
|
-
if (text.length > 9000) {
|
|
934
|
-
throw new Error(
|
|
935
|
-
"400 This model's maximum context length is 8192 tokens, however you requested 18227 tokens (18227 in your prompt; 0 for the completion).",
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
return texts.map((text, index) => [text.length, index]);
|
|
940
|
-
},
|
|
941
|
-
},
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
try {
|
|
945
|
-
const now = '2026-03-09T00:00:00Z';
|
|
946
|
-
service.db
|
|
947
|
-
.prepare(
|
|
948
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
949
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
950
|
-
)
|
|
951
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
952
|
-
service.db
|
|
953
|
-
.prepare(
|
|
954
|
-
`insert into threads (
|
|
955
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
956
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
957
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
958
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
959
|
-
)
|
|
960
|
-
.run(
|
|
961
|
-
10,
|
|
962
|
-
1,
|
|
963
|
-
'100',
|
|
964
|
-
42,
|
|
965
|
-
'issue',
|
|
966
|
-
'open',
|
|
967
|
-
'Short title',
|
|
968
|
-
'short body',
|
|
969
|
-
'alice',
|
|
970
|
-
'User',
|
|
971
|
-
'https://github.com/openclaw/openclaw/issues/42',
|
|
972
|
-
'[]',
|
|
973
|
-
'[]',
|
|
974
|
-
'{}',
|
|
975
|
-
'hash-42',
|
|
976
|
-
0,
|
|
977
|
-
now,
|
|
978
|
-
now,
|
|
979
|
-
null,
|
|
980
|
-
null,
|
|
981
|
-
now,
|
|
982
|
-
now,
|
|
983
|
-
now,
|
|
984
|
-
);
|
|
985
|
-
service.db
|
|
986
|
-
.prepare(
|
|
987
|
-
`insert into threads (
|
|
988
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
989
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
990
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
991
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
992
|
-
)
|
|
993
|
-
.run(
|
|
994
|
-
11,
|
|
995
|
-
1,
|
|
996
|
-
'101',
|
|
997
|
-
43,
|
|
998
|
-
'issue',
|
|
999
|
-
'open',
|
|
1000
|
-
'Large body',
|
|
1001
|
-
'x'.repeat(20000),
|
|
1002
|
-
'bob',
|
|
1003
|
-
'User',
|
|
1004
|
-
'https://github.com/openclaw/openclaw/issues/43',
|
|
1005
|
-
'[]',
|
|
1006
|
-
'[]',
|
|
1007
|
-
'{}',
|
|
1008
|
-
'hash-43',
|
|
1009
|
-
0,
|
|
1010
|
-
now,
|
|
1011
|
-
now,
|
|
1012
|
-
null,
|
|
1013
|
-
null,
|
|
1014
|
-
now,
|
|
1015
|
-
now,
|
|
1016
|
-
now,
|
|
1017
|
-
);
|
|
1018
|
-
|
|
1019
|
-
const result = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' });
|
|
1020
|
-
|
|
1021
|
-
assert.equal(result.embedded, 4);
|
|
1022
|
-
assert.ok(embedCalls.length >= 3);
|
|
1023
|
-
assert.equal(embedCalls[0].length, 4);
|
|
1024
|
-
assert.ok(embedCalls.flat().some((text) => text.includes('[truncated for embedding]')));
|
|
1025
|
-
} finally {
|
|
1026
|
-
service.close();
|
|
1027
|
-
}
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
test('listNeighbors returns exact nearest neighbors for an embedded thread', () => {
|
|
1031
|
-
const service = makeTestService({
|
|
1032
|
-
checkAuth: async () => undefined,
|
|
1033
|
-
getRepo: async () => ({}),
|
|
1034
|
-
listRepositoryIssues: async () => [],
|
|
1035
|
-
getIssue: async () => ({}),
|
|
1036
|
-
getPull: async () => ({}),
|
|
1037
|
-
listIssueComments: async () => [],
|
|
1038
|
-
listPullReviews: async () => [],
|
|
1039
|
-
listPullReviewComments: async () => [],
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
try {
|
|
1043
|
-
const now = '2026-03-09T00:00:00Z';
|
|
1044
|
-
service.db
|
|
1045
|
-
.prepare(
|
|
1046
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
1047
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
1048
|
-
)
|
|
1049
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
1050
|
-
service.db
|
|
1051
|
-
.prepare(
|
|
1052
|
-
`insert into threads (
|
|
1053
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
1054
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
1055
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
1056
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1057
|
-
)
|
|
1058
|
-
.run(10, 1, '100', 42, 'issue', 'open', 'Downloader hangs', 'The transfer never finishes.', 'alice', 'User', 'https://github.com/openclaw/openclaw/issues/42', '[]', '[]', '{}', 'hash-42', 0, now, now, null, null, now, now, now);
|
|
1059
|
-
service.db
|
|
1060
|
-
.prepare(
|
|
1061
|
-
`insert into threads (
|
|
1062
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
1063
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
1064
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
1065
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1066
|
-
)
|
|
1067
|
-
.run(11, 1, '101', 43, 'pull_request', 'open', 'Fix downloader hang', 'Implements a fix.', 'bob', 'User', 'https://github.com/openclaw/openclaw/pull/43', '[]', '[]', '{}', 'hash-43', 0, now, now, null, null, now, now, now);
|
|
1068
|
-
service.db
|
|
1069
|
-
.prepare(
|
|
1070
|
-
`insert into threads (
|
|
1071
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
1072
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
1073
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
1074
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1075
|
-
)
|
|
1076
|
-
.run(12, 1, '102', 44, 'issue', 'open', 'Unrelated auth issue', 'Login is broken.', 'carol', 'User', 'https://github.com/openclaw/openclaw/issues/44', '[]', '[]', '{}', 'hash-44', 0, now, now, null, null, now, now, now);
|
|
1077
|
-
service.db
|
|
1078
|
-
.prepare(
|
|
1079
|
-
`insert into document_embeddings (thread_id, source_kind, model, dimensions, content_hash, embedding_json, created_at, updated_at)
|
|
1080
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1081
|
-
)
|
|
1082
|
-
.run(10, 'dedupe_summary', 'text-embedding-3-large', 2, 'hash-42', '[1,0]', now, now);
|
|
1083
|
-
service.db
|
|
1084
|
-
.prepare(
|
|
1085
|
-
`insert into document_embeddings (thread_id, source_kind, model, dimensions, content_hash, embedding_json, created_at, updated_at)
|
|
1086
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1087
|
-
)
|
|
1088
|
-
.run(11, 'dedupe_summary', 'text-embedding-3-large', 2, 'hash-43', '[0.99,0.01]', now, now);
|
|
1089
|
-
service.db
|
|
1090
|
-
.prepare(
|
|
1091
|
-
`insert into document_embeddings (thread_id, source_kind, model, dimensions, content_hash, embedding_json, created_at, updated_at)
|
|
1092
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1093
|
-
)
|
|
1094
|
-
.run(12, 'dedupe_summary', 'text-embedding-3-large', 2, 'hash-44', '[0,1]', now, now);
|
|
1095
|
-
|
|
1096
|
-
const result = service.listNeighbors({
|
|
1097
|
-
owner: 'openclaw',
|
|
1098
|
-
repo: 'openclaw',
|
|
1099
|
-
threadNumber: 42,
|
|
1100
|
-
limit: 2,
|
|
1101
|
-
minScore: 0.1,
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
assert.equal(result.thread.number, 42);
|
|
1105
|
-
assert.equal(result.neighbors.length, 1);
|
|
1106
|
-
assert.equal(result.neighbors[0].number, 43);
|
|
1107
|
-
assert.ok(result.neighbors[0].score > 0.9);
|
|
1108
|
-
} finally {
|
|
1109
|
-
service.close();
|
|
1110
|
-
}
|
|
1111
|
-
});
|
|
1112
|
-
|
|
1113
|
-
test('tui snapshot returns mixed issue and pull request counts with default recent sort and filters', () => {
|
|
1114
|
-
const service = makeTestService({
|
|
1115
|
-
checkAuth: async () => undefined,
|
|
1116
|
-
getRepo: async () => ({}),
|
|
1117
|
-
listRepositoryIssues: async () => [],
|
|
1118
|
-
getIssue: async () => ({}),
|
|
1119
|
-
getPull: async () => ({}),
|
|
1120
|
-
listIssueComments: async () => [],
|
|
1121
|
-
listPullReviews: async () => [],
|
|
1122
|
-
listPullReviewComments: async () => [],
|
|
1123
|
-
});
|
|
1124
|
-
|
|
1125
|
-
try {
|
|
1126
|
-
const now = '2026-03-09T12:00:00Z';
|
|
1127
|
-
service.db
|
|
1128
|
-
.prepare(
|
|
1129
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
1130
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
1131
|
-
)
|
|
1132
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
1133
|
-
|
|
1134
|
-
const insertThread = service.db.prepare(
|
|
1135
|
-
`insert into threads (
|
|
1136
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
1137
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
1138
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
1139
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1140
|
-
);
|
|
1141
|
-
insertThread.run(10, 1, '100', 42, 'issue', 'open', 'Old issue cluster', 'body', 'alice', 'User', 'https://github.com/openclaw/openclaw/issues/42', '[]', '[]', '{}', 'hash-42', 0, now, '2026-03-07T00:00:00Z', null, null, now, now, now);
|
|
1142
|
-
insertThread.run(11, 1, '101', 43, 'pull_request', 'open', 'Old PR cluster', 'body', 'bob', 'User', 'https://github.com/openclaw/openclaw/pull/43', '[]', '[]', '{}', 'hash-43', 0, now, '2026-03-07T00:00:00Z', null, null, now, now, now);
|
|
1143
|
-
insertThread.run(12, 1, '102', 44, 'issue', 'open', 'Recent issue cluster', 'body', 'carol', 'User', 'https://github.com/openclaw/openclaw/issues/44', '[]', '[]', '{}', 'hash-44', 0, now, '2026-03-09T14:00:00Z', null, null, now, now, now);
|
|
1144
|
-
insertThread.run(13, 1, '103', 45, 'issue', 'open', 'Recent issue followup', 'body', 'dave', 'User', 'https://github.com/openclaw/openclaw/issues/45', '[]', '[]', '{}', 'hash-45', 0, now, '2026-03-09T13:00:00Z', null, null, now, now, now);
|
|
1145
|
-
insertThread.run(14, 1, '104', 46, 'pull_request', 'open', 'Recent PR followup', 'body', 'erin', 'User', 'https://github.com/openclaw/openclaw/pull/46', '[]', '[]', '{}', 'hash-46', 0, now, '2026-03-09T12:30:00Z', null, null, now, now, now);
|
|
1146
|
-
|
|
1147
|
-
service.db
|
|
1148
|
-
.prepare(`insert into cluster_runs (id, repo_id, scope, status, started_at, finished_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1149
|
-
.run(1, 1, 'openclaw/openclaw', 'completed', now, '2026-03-09T14:30:00Z');
|
|
1150
|
-
service.db
|
|
1151
|
-
.prepare(`insert into sync_runs (id, repo_id, scope, status, started_at, finished_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1152
|
-
.run(1, 1, 'openclaw/openclaw', 'completed', now, '2026-03-09T12:00:00Z');
|
|
1153
|
-
service.db
|
|
1154
|
-
.prepare(`insert into embedding_runs (id, repo_id, scope, status, started_at, finished_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1155
|
-
.run(1, 1, 'openclaw/openclaw', 'completed', now, '2026-03-09T13:00:00Z');
|
|
1156
|
-
service.db
|
|
1157
|
-
.prepare(
|
|
1158
|
-
`insert into clusters (id, repo_id, cluster_run_id, representative_thread_id, member_count, created_at)
|
|
1159
|
-
values (?, ?, ?, ?, ?, ?)`,
|
|
1160
|
-
)
|
|
1161
|
-
.run(100, 1, 1, 10, 2, now);
|
|
1162
|
-
service.db
|
|
1163
|
-
.prepare(
|
|
1164
|
-
`insert into clusters (id, repo_id, cluster_run_id, representative_thread_id, member_count, created_at)
|
|
1165
|
-
values (?, ?, ?, ?, ?, ?)`,
|
|
1166
|
-
)
|
|
1167
|
-
.run(101, 1, 1, 12, 3, now);
|
|
1168
|
-
const insertMember = service.db.prepare(
|
|
1169
|
-
`insert into cluster_members (cluster_id, thread_id, score_to_representative, created_at)
|
|
1170
|
-
values (?, ?, ?, ?)`,
|
|
1171
|
-
);
|
|
1172
|
-
insertMember.run(100, 10, null, now);
|
|
1173
|
-
insertMember.run(100, 11, 0.9, now);
|
|
1174
|
-
insertMember.run(101, 12, null, now);
|
|
1175
|
-
insertMember.run(101, 13, 0.95, now);
|
|
1176
|
-
insertMember.run(101, 14, 0.88, now);
|
|
1177
|
-
|
|
1178
|
-
const snapshot = service.getTuiSnapshot({ owner: 'openclaw', repo: 'openclaw' });
|
|
1179
|
-
assert.equal(snapshot.stats.openIssueCount, 3);
|
|
1180
|
-
assert.equal(snapshot.stats.openPullRequestCount, 2);
|
|
1181
|
-
assert.equal(snapshot.stats.lastGithubReconciliationAt, '2026-03-09T12:00:00Z');
|
|
1182
|
-
assert.equal(snapshot.stats.lastEmbedRefreshAt, '2026-03-09T13:00:00Z');
|
|
1183
|
-
assert.equal(snapshot.stats.staleEmbedThreadCount, 5);
|
|
1184
|
-
assert.equal(snapshot.stats.staleEmbedSourceCount, 10);
|
|
1185
|
-
assert.equal(snapshot.stats.latestClusterRunId, 1);
|
|
1186
|
-
assert.equal(snapshot.clusters.length, 0);
|
|
1187
|
-
|
|
1188
|
-
const allSnapshot = service.getTuiSnapshot({ owner: 'openclaw', repo: 'openclaw', minSize: 0 });
|
|
1189
|
-
assert.deepEqual(
|
|
1190
|
-
allSnapshot.clusters.map((cluster) => cluster.clusterId),
|
|
1191
|
-
[101, 100],
|
|
1192
|
-
);
|
|
1193
|
-
assert.equal(allSnapshot.clusters[0].issueCount, 2);
|
|
1194
|
-
assert.equal(allSnapshot.clusters[0].pullRequestCount, 1);
|
|
1195
|
-
assert.equal(allSnapshot.clusters[0].displayTitle, 'Recent issue cluster');
|
|
1196
|
-
|
|
1197
|
-
const sizeSorted = service.getTuiSnapshot({ owner: 'openclaw', repo: 'openclaw', minSize: 0, sort: 'size' });
|
|
1198
|
-
assert.deepEqual(
|
|
1199
|
-
sizeSorted.clusters.map((cluster) => cluster.clusterId),
|
|
1200
|
-
[101, 100],
|
|
1201
|
-
);
|
|
1202
|
-
|
|
1203
|
-
const filtered = service.getTuiSnapshot({ owner: 'openclaw', repo: 'openclaw', minSize: 0, search: 'old pr cluster' });
|
|
1204
|
-
assert.deepEqual(
|
|
1205
|
-
filtered.clusters.map((cluster) => cluster.clusterId),
|
|
1206
|
-
[100],
|
|
1207
|
-
);
|
|
1208
|
-
} finally {
|
|
1209
|
-
service.close();
|
|
1210
|
-
}
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
test('tui cluster detail and thread detail expose members, summaries, and neighbors', () => {
|
|
1214
|
-
const service = makeTestService({
|
|
1215
|
-
checkAuth: async () => undefined,
|
|
1216
|
-
getRepo: async () => ({}),
|
|
1217
|
-
listRepositoryIssues: async () => [],
|
|
1218
|
-
getIssue: async () => ({}),
|
|
1219
|
-
getPull: async () => ({}),
|
|
1220
|
-
listIssueComments: async () => [],
|
|
1221
|
-
listPullReviews: async () => [],
|
|
1222
|
-
listPullReviewComments: async () => [],
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
try {
|
|
1226
|
-
const now = '2026-03-09T12:00:00Z';
|
|
1227
|
-
service.db
|
|
1228
|
-
.prepare(
|
|
1229
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
1230
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
1231
|
-
)
|
|
1232
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
1233
|
-
|
|
1234
|
-
const insertThread = service.db.prepare(
|
|
1235
|
-
`insert into threads (
|
|
1236
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
1237
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
1238
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
1239
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1240
|
-
);
|
|
1241
|
-
insertThread.run(10, 1, '100', 42, 'issue', 'open', 'Downloader hangs', 'The transfer never finishes.', 'alice', 'User', 'https://github.com/openclaw/openclaw/issues/42', '["bug"]', '[]', '{}', 'hash-42', 0, now, now, null, null, now, now, now);
|
|
1242
|
-
insertThread.run(11, 1, '101', 43, 'pull_request', 'open', 'Fix downloader hang', 'Implements a fix.', 'bob', 'User', 'https://github.com/openclaw/openclaw/pull/43', '["bug"]', '[]', '{}', 'hash-43', 0, now, now, null, null, now, now, now);
|
|
1243
|
-
|
|
1244
|
-
service.db
|
|
1245
|
-
.prepare(`insert into cluster_runs (id, repo_id, scope, status, started_at, finished_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1246
|
-
.run(1, 1, 'openclaw/openclaw', 'completed', now, now);
|
|
1247
|
-
service.db
|
|
1248
|
-
.prepare(
|
|
1249
|
-
`insert into clusters (id, repo_id, cluster_run_id, representative_thread_id, member_count, created_at)
|
|
1250
|
-
values (?, ?, ?, ?, ?, ?)`,
|
|
1251
|
-
)
|
|
1252
|
-
.run(100, 1, 1, 10, 2, now);
|
|
1253
|
-
service.db
|
|
1254
|
-
.prepare(
|
|
1255
|
-
`insert into cluster_members (cluster_id, thread_id, score_to_representative, created_at)
|
|
1256
|
-
values (?, ?, ?, ?)`,
|
|
1257
|
-
)
|
|
1258
|
-
.run(100, 10, null, now);
|
|
1259
|
-
service.db
|
|
1260
|
-
.prepare(
|
|
1261
|
-
`insert into cluster_members (cluster_id, thread_id, score_to_representative, created_at)
|
|
1262
|
-
values (?, ?, ?, ?)`,
|
|
1263
|
-
)
|
|
1264
|
-
.run(100, 11, 0.93, now);
|
|
1265
|
-
service.db
|
|
1266
|
-
.prepare(
|
|
1267
|
-
`insert into document_summaries (thread_id, summary_kind, model, content_hash, summary_text, created_at, updated_at)
|
|
1268
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
1269
|
-
)
|
|
1270
|
-
.run(10, 'problem_summary', 'gpt-5-mini', 'hash-problem', 'Downloads hang before completion.', now, now);
|
|
1271
|
-
service.db
|
|
1272
|
-
.prepare(
|
|
1273
|
-
`insert into document_summaries (thread_id, summary_kind, model, content_hash, summary_text, created_at, updated_at)
|
|
1274
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
1275
|
-
)
|
|
1276
|
-
.run(10, 'dedupe_summary', 'gpt-5-mini', 'hash-dedupe', 'Transfer stalls near completion.', now, now);
|
|
1277
|
-
service.db
|
|
1278
|
-
.prepare(
|
|
1279
|
-
`insert into document_embeddings (thread_id, source_kind, model, dimensions, content_hash, embedding_json, created_at, updated_at)
|
|
1280
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1281
|
-
)
|
|
1282
|
-
.run(10, 'title', 'text-embedding-3-large', 2, 'hash-title-42', '[1,0]', now, now);
|
|
1283
|
-
service.db
|
|
1284
|
-
.prepare(
|
|
1285
|
-
`insert into document_embeddings (thread_id, source_kind, model, dimensions, content_hash, embedding_json, created_at, updated_at)
|
|
1286
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1287
|
-
)
|
|
1288
|
-
.run(11, 'title', 'text-embedding-3-large', 2, 'hash-title-43', '[0.95,0.05]', now, now);
|
|
1289
|
-
|
|
1290
|
-
const detail = service.getTuiClusterDetail({ owner: 'openclaw', repo: 'openclaw', clusterId: 100 });
|
|
1291
|
-
assert.equal(detail.totalCount, 2);
|
|
1292
|
-
assert.equal(detail.issueCount, 1);
|
|
1293
|
-
assert.equal(detail.pullRequestCount, 1);
|
|
1294
|
-
assert.equal(detail.members[0].kind, 'issue');
|
|
1295
|
-
assert.equal(detail.members[1].kind, 'pull_request');
|
|
1296
|
-
assert.equal(detail.members[1].clusterScore, 0.93);
|
|
1297
|
-
|
|
1298
|
-
const threadDetail = service.getTuiThreadDetail({ owner: 'openclaw', repo: 'openclaw', threadNumber: 42 });
|
|
1299
|
-
assert.equal(threadDetail.thread.number, 42);
|
|
1300
|
-
assert.equal(threadDetail.thread.labels[0], 'bug');
|
|
1301
|
-
assert.equal(threadDetail.thread.htmlUrl, 'https://github.com/openclaw/openclaw/issues/42');
|
|
1302
|
-
assert.equal(threadDetail.summaries.problem_summary, 'Downloads hang before completion.');
|
|
1303
|
-
assert.equal(threadDetail.summaries.dedupe_summary, 'Transfer stalls near completion.');
|
|
1304
|
-
assert.equal(threadDetail.neighbors[0]?.number, 43);
|
|
1305
|
-
} finally {
|
|
1306
|
-
service.close();
|
|
1307
|
-
}
|
|
1308
|
-
});
|
|
1309
|
-
|
|
1310
|
-
test('refreshRepository runs sync, embed, and cluster in order and returns the combined result', async () => {
|
|
1311
|
-
const messages: string[] = [];
|
|
1312
|
-
const service = makeTestService(
|
|
1313
|
-
{
|
|
1314
|
-
checkAuth: async () => undefined,
|
|
1315
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
1316
|
-
listRepositoryIssues: async () => [
|
|
1317
|
-
{
|
|
1318
|
-
id: 100,
|
|
1319
|
-
number: 42,
|
|
1320
|
-
state: 'open',
|
|
1321
|
-
title: 'Downloader hangs',
|
|
1322
|
-
body: 'The transfer never finishes.',
|
|
1323
|
-
html_url: 'https://github.com/openclaw/openclaw/issues/42',
|
|
1324
|
-
labels: [{ name: 'bug' }],
|
|
1325
|
-
assignees: [],
|
|
1326
|
-
user: { login: 'alice', type: 'User' },
|
|
1327
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
1328
|
-
},
|
|
1329
|
-
],
|
|
1330
|
-
getIssue: async (_owner, _repo, number) => ({
|
|
1331
|
-
id: 100,
|
|
1332
|
-
number,
|
|
1333
|
-
state: 'open',
|
|
1334
|
-
title: 'Downloader hangs',
|
|
1335
|
-
body: 'The transfer never finishes.',
|
|
1336
|
-
html_url: `https://github.com/openclaw/openclaw/issues/${number}`,
|
|
1337
|
-
labels: [{ name: 'bug' }],
|
|
1338
|
-
assignees: [],
|
|
1339
|
-
user: { login: 'alice', type: 'User' },
|
|
1340
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
1341
|
-
}),
|
|
1342
|
-
getPull: async () => {
|
|
1343
|
-
throw new Error('not expected');
|
|
1344
|
-
},
|
|
1345
|
-
listIssueComments: async () => [],
|
|
1346
|
-
listPullReviews: async () => [],
|
|
1347
|
-
listPullReviewComments: async () => [],
|
|
1348
|
-
},
|
|
1349
|
-
{
|
|
1350
|
-
checkAuth: async () => undefined,
|
|
1351
|
-
summarizeThread: async () => {
|
|
1352
|
-
throw new Error('not expected');
|
|
1353
|
-
},
|
|
1354
|
-
embedTexts: async ({ texts }) => texts.map(() => [1, 0]),
|
|
1355
|
-
},
|
|
1356
|
-
);
|
|
1357
|
-
|
|
1358
|
-
try {
|
|
1359
|
-
const result = await service.refreshRepository({
|
|
1360
|
-
owner: 'openclaw',
|
|
1361
|
-
repo: 'openclaw',
|
|
1362
|
-
onProgress: (message) => messages.push(message),
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
assert.equal(result.selected.sync, true);
|
|
1366
|
-
assert.equal(result.selected.embed, true);
|
|
1367
|
-
assert.equal(result.selected.cluster, true);
|
|
1368
|
-
assert.equal(result.sync?.threadsSynced, 1);
|
|
1369
|
-
assert.equal(result.embed?.embedded, 2);
|
|
1370
|
-
assert.equal(result.cluster?.clusters, 1);
|
|
1371
|
-
|
|
1372
|
-
const syncIndex = messages.findIndex((message) => message.includes('[sync]'));
|
|
1373
|
-
const embedIndex = messages.findIndex((message) => message.includes('[embed]'));
|
|
1374
|
-
const clusterIndex = messages.findIndex((message) => message.includes('[cluster]'));
|
|
1375
|
-
assert.ok(syncIndex >= 0);
|
|
1376
|
-
assert.ok(embedIndex > syncIndex);
|
|
1377
|
-
assert.ok(clusterIndex > embedIndex);
|
|
1378
|
-
} finally {
|
|
1379
|
-
service.close();
|
|
1380
|
-
}
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
|
-
test('agent cluster summary and detail dumps expose repo stats, snippets, and summaries', () => {
|
|
1384
|
-
const service = makeTestService({
|
|
1385
|
-
checkAuth: async () => undefined,
|
|
1386
|
-
getRepo: async () => ({}),
|
|
1387
|
-
listRepositoryIssues: async () => [],
|
|
1388
|
-
getIssue: async () => ({}),
|
|
1389
|
-
getPull: async () => ({}),
|
|
1390
|
-
listIssueComments: async () => [],
|
|
1391
|
-
listPullReviews: async () => [],
|
|
1392
|
-
listPullReviewComments: async () => [],
|
|
1393
|
-
});
|
|
1394
|
-
|
|
1395
|
-
try {
|
|
1396
|
-
const now = '2026-03-09T12:00:00Z';
|
|
1397
|
-
service.db
|
|
1398
|
-
.prepare(
|
|
1399
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
1400
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
1401
|
-
)
|
|
1402
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
1403
|
-
|
|
1404
|
-
const insertThread = service.db.prepare(
|
|
1405
|
-
`insert into threads (
|
|
1406
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
1407
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh,
|
|
1408
|
-
merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
1409
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1410
|
-
);
|
|
1411
|
-
insertThread.run(
|
|
1412
|
-
10,
|
|
1413
|
-
1,
|
|
1414
|
-
'100',
|
|
1415
|
-
42,
|
|
1416
|
-
'issue',
|
|
1417
|
-
'open',
|
|
1418
|
-
'Downloader hangs',
|
|
1419
|
-
'The transfer never finishes after a large file download and needs to be retried.',
|
|
1420
|
-
'alice',
|
|
1421
|
-
'User',
|
|
1422
|
-
'https://github.com/openclaw/openclaw/issues/42',
|
|
1423
|
-
'["bug"]',
|
|
1424
|
-
'[]',
|
|
1425
|
-
'{}',
|
|
1426
|
-
'hash-42',
|
|
1427
|
-
0,
|
|
1428
|
-
now,
|
|
1429
|
-
'2026-03-09T10:00:00Z',
|
|
1430
|
-
null,
|
|
1431
|
-
null,
|
|
1432
|
-
now,
|
|
1433
|
-
now,
|
|
1434
|
-
now,
|
|
1435
|
-
);
|
|
1436
|
-
insertThread.run(
|
|
1437
|
-
11,
|
|
1438
|
-
1,
|
|
1439
|
-
'101',
|
|
1440
|
-
43,
|
|
1441
|
-
'pull_request',
|
|
1442
|
-
'open',
|
|
1443
|
-
'Fix downloader hang',
|
|
1444
|
-
'This updates the retry logic and timeout handling.',
|
|
1445
|
-
'bob',
|
|
1446
|
-
'User',
|
|
1447
|
-
'https://github.com/openclaw/openclaw/pull/43',
|
|
1448
|
-
'["bug"]',
|
|
1449
|
-
'[]',
|
|
1450
|
-
'{}',
|
|
1451
|
-
'hash-43',
|
|
1452
|
-
0,
|
|
1453
|
-
now,
|
|
1454
|
-
'2026-03-09T11:00:00Z',
|
|
1455
|
-
null,
|
|
1456
|
-
null,
|
|
1457
|
-
now,
|
|
1458
|
-
now,
|
|
1459
|
-
now,
|
|
1460
|
-
);
|
|
1461
|
-
|
|
1462
|
-
service.db
|
|
1463
|
-
.prepare(`insert into sync_runs (id, repo_id, scope, status, started_at, finished_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1464
|
-
.run(1, 1, 'openclaw/openclaw', 'completed', now, '2026-03-09T12:30:00Z');
|
|
1465
|
-
service.db
|
|
1466
|
-
.prepare(`insert into embedding_runs (id, repo_id, scope, status, started_at, finished_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1467
|
-
.run(1, 1, 'openclaw/openclaw', 'completed', now, '2026-03-09T12:45:00Z');
|
|
1468
|
-
service.db
|
|
1469
|
-
.prepare(`insert into cluster_runs (id, repo_id, scope, status, started_at, finished_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1470
|
-
.run(1, 1, 'openclaw/openclaw', 'completed', now, '2026-03-09T13:00:00Z');
|
|
1471
|
-
service.db
|
|
1472
|
-
.prepare(
|
|
1473
|
-
`insert into clusters (id, repo_id, cluster_run_id, representative_thread_id, member_count, created_at)
|
|
1474
|
-
values (?, ?, ?, ?, ?, ?)`,
|
|
1475
|
-
)
|
|
1476
|
-
.run(100, 1, 1, 10, 2, now);
|
|
1477
|
-
service.db
|
|
1478
|
-
.prepare(
|
|
1479
|
-
`insert into cluster_members (cluster_id, thread_id, score_to_representative, created_at)
|
|
1480
|
-
values (?, ?, ?, ?)`,
|
|
1481
|
-
)
|
|
1482
|
-
.run(100, 10, null, now);
|
|
1483
|
-
service.db
|
|
1484
|
-
.prepare(
|
|
1485
|
-
`insert into cluster_members (cluster_id, thread_id, score_to_representative, created_at)
|
|
1486
|
-
values (?, ?, ?, ?)`,
|
|
1487
|
-
)
|
|
1488
|
-
.run(100, 11, 0.93, now);
|
|
1489
|
-
service.db
|
|
1490
|
-
.prepare(
|
|
1491
|
-
`insert into document_summaries (thread_id, summary_kind, model, content_hash, summary_text, created_at, updated_at)
|
|
1492
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
1493
|
-
)
|
|
1494
|
-
.run(10, 'dedupe_summary', 'gpt-5-mini', 'hash-dedupe', 'Transfer stalls near completion.', now, now);
|
|
1495
|
-
service.db
|
|
1496
|
-
.prepare(
|
|
1497
|
-
`insert into document_embeddings (thread_id, source_kind, model, dimensions, content_hash, embedding_json, created_at, updated_at)
|
|
1498
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1499
|
-
)
|
|
1500
|
-
.run(10, 'title', 'text-embedding-3-large', 2, 'hash-title-42', '[1,0]', now, now);
|
|
1501
|
-
service.db
|
|
1502
|
-
.prepare(
|
|
1503
|
-
`insert into document_embeddings (thread_id, source_kind, model, dimensions, content_hash, embedding_json, created_at, updated_at)
|
|
1504
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1505
|
-
)
|
|
1506
|
-
.run(11, 'title', 'text-embedding-3-large', 2, 'hash-title-43', '[0.95,0.05]', now, now);
|
|
1507
|
-
|
|
1508
|
-
const summaries = service.listClusterSummaries({ owner: 'openclaw', repo: 'openclaw', minSize: 0 });
|
|
1509
|
-
assert.equal(summaries.stats.openIssueCount, 1);
|
|
1510
|
-
assert.equal(summaries.clusters.length, 1);
|
|
1511
|
-
assert.equal(summaries.clusters[0]?.displayTitle, 'Downloader hangs');
|
|
1512
|
-
|
|
1513
|
-
const detail = service.getClusterDetailDump({
|
|
1514
|
-
owner: 'openclaw',
|
|
1515
|
-
repo: 'openclaw',
|
|
1516
|
-
clusterId: 100,
|
|
1517
|
-
memberLimit: 1,
|
|
1518
|
-
bodyChars: 30,
|
|
1519
|
-
});
|
|
1520
|
-
assert.equal(detail.members.length, 1);
|
|
1521
|
-
assert.equal(detail.members[0]?.thread.number, 42);
|
|
1522
|
-
assert.equal(detail.members[0]?.bodySnippet, 'The transfer never finishes a…');
|
|
1523
|
-
assert.equal(detail.members[0]?.summaries.dedupe_summary, 'Transfer stalls near completion.');
|
|
1524
|
-
} finally {
|
|
1525
|
-
service.close();
|
|
1526
|
-
}
|
|
1527
|
-
});
|
|
1528
|
-
|
|
1529
|
-
test('getTuiThreadDetail can skip neighbor loading for fast browse paths', () => {
|
|
1530
|
-
const service = makeTestService({
|
|
1531
|
-
checkAuth: async () => undefined,
|
|
1532
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
1533
|
-
listRepositoryIssues: async () => [],
|
|
1534
|
-
getIssue: async () => {
|
|
1535
|
-
throw new Error('not expected');
|
|
1536
|
-
},
|
|
1537
|
-
getPull: async () => {
|
|
1538
|
-
throw new Error('not expected');
|
|
1539
|
-
},
|
|
1540
|
-
listIssueComments: async () => [],
|
|
1541
|
-
listPullReviews: async () => [],
|
|
1542
|
-
listPullReviewComments: async () => [],
|
|
1543
|
-
});
|
|
1544
|
-
|
|
1545
|
-
try {
|
|
1546
|
-
const now = '2026-03-09T00:00:00Z';
|
|
1547
|
-
service.db
|
|
1548
|
-
.prepare(
|
|
1549
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
1550
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
1551
|
-
)
|
|
1552
|
-
.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now);
|
|
1553
|
-
service.db
|
|
1554
|
-
.prepare(
|
|
1555
|
-
`insert into threads (
|
|
1556
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
1557
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh,
|
|
1558
|
-
closed_at_gh, merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
1559
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1560
|
-
)
|
|
1561
|
-
.run(
|
|
1562
|
-
10,
|
|
1563
|
-
1,
|
|
1564
|
-
'100',
|
|
1565
|
-
42,
|
|
1566
|
-
'issue',
|
|
1567
|
-
'open',
|
|
1568
|
-
'Fast browse thread',
|
|
1569
|
-
'body',
|
|
1570
|
-
'alice',
|
|
1571
|
-
'User',
|
|
1572
|
-
'https://github.com/openclaw/openclaw/issues/42',
|
|
1573
|
-
'[]',
|
|
1574
|
-
'[]',
|
|
1575
|
-
'{}',
|
|
1576
|
-
'hash-42',
|
|
1577
|
-
0,
|
|
1578
|
-
now,
|
|
1579
|
-
now,
|
|
1580
|
-
null,
|
|
1581
|
-
null,
|
|
1582
|
-
now,
|
|
1583
|
-
now,
|
|
1584
|
-
now,
|
|
1585
|
-
);
|
|
1586
|
-
|
|
1587
|
-
const originalListNeighbors = service.listNeighbors.bind(service);
|
|
1588
|
-
let neighborCalls = 0;
|
|
1589
|
-
service.listNeighbors = ((...args: Parameters<typeof originalListNeighbors>) => {
|
|
1590
|
-
neighborCalls += 1;
|
|
1591
|
-
return originalListNeighbors(...args);
|
|
1592
|
-
}) as typeof service.listNeighbors;
|
|
1593
|
-
|
|
1594
|
-
const detail = service.getTuiThreadDetail({
|
|
1595
|
-
owner: 'openclaw',
|
|
1596
|
-
repo: 'openclaw',
|
|
1597
|
-
threadId: 10,
|
|
1598
|
-
includeNeighbors: false,
|
|
1599
|
-
});
|
|
1600
|
-
|
|
1601
|
-
assert.equal(detail.thread.number, 42);
|
|
1602
|
-
assert.deepEqual(detail.neighbors, []);
|
|
1603
|
-
assert.equal(neighborCalls, 0);
|
|
1604
|
-
} finally {
|
|
1605
|
-
service.close();
|
|
1606
|
-
}
|
|
1607
|
-
});
|
|
1608
|
-
|
|
1609
|
-
test('syncRepository reconciles stale open threads and marks confirmed closures without re-fetching comments', async () => {
|
|
1610
|
-
let listIssueCommentCalls = 0;
|
|
1611
|
-
let getIssueCalls = 0;
|
|
1612
|
-
let listRepositoryIssuesCalls = 0;
|
|
1613
|
-
|
|
1614
|
-
const service = makeTestService({
|
|
1615
|
-
checkAuth: async () => undefined,
|
|
1616
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
1617
|
-
listRepositoryIssues: async () => {
|
|
1618
|
-
listRepositoryIssuesCalls += 1;
|
|
1619
|
-
return listRepositoryIssuesCalls === 1
|
|
1620
|
-
? [
|
|
1621
|
-
{
|
|
1622
|
-
id: 100,
|
|
1623
|
-
number: 42,
|
|
1624
|
-
state: 'open',
|
|
1625
|
-
title: 'Downloader hangs',
|
|
1626
|
-
body: 'The transfer never finishes.',
|
|
1627
|
-
html_url: 'https://github.com/openclaw/openclaw/issues/42',
|
|
1628
|
-
labels: [{ name: 'bug' }],
|
|
1629
|
-
assignees: [],
|
|
1630
|
-
user: { login: 'alice', type: 'User' },
|
|
1631
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
1632
|
-
},
|
|
1633
|
-
]
|
|
1634
|
-
: [];
|
|
1635
|
-
},
|
|
1636
|
-
getIssue: async (_owner, _repo, number) => {
|
|
1637
|
-
getIssueCalls += 1;
|
|
1638
|
-
return {
|
|
1639
|
-
id: 100,
|
|
1640
|
-
number,
|
|
1641
|
-
state: 'closed',
|
|
1642
|
-
title: 'Downloader hangs',
|
|
1643
|
-
body: 'The transfer never finishes.',
|
|
1644
|
-
html_url: `https://github.com/openclaw/openclaw/issues/${number}`,
|
|
1645
|
-
labels: [{ name: 'bug' }],
|
|
1646
|
-
assignees: [],
|
|
1647
|
-
user: { login: 'alice', type: 'User' },
|
|
1648
|
-
updated_at: '2026-03-10T00:00:00Z',
|
|
1649
|
-
closed_at: '2026-03-10T00:00:00Z',
|
|
1650
|
-
};
|
|
1651
|
-
},
|
|
1652
|
-
getPull: async () => {
|
|
1653
|
-
throw new Error('not expected');
|
|
1654
|
-
},
|
|
1655
|
-
listIssueComments: async () => {
|
|
1656
|
-
listIssueCommentCalls += 1;
|
|
1657
|
-
return [];
|
|
1658
|
-
},
|
|
1659
|
-
listPullReviews: async () => [],
|
|
1660
|
-
listPullReviewComments: async () => [],
|
|
1661
|
-
});
|
|
1662
|
-
|
|
1663
|
-
try {
|
|
1664
|
-
await service.syncRepository({ owner: 'openclaw', repo: 'openclaw' });
|
|
1665
|
-
const before = service.db
|
|
1666
|
-
.prepare("select state, first_pulled_at, last_pulled_at from threads where number = 42 and kind = 'issue'")
|
|
1667
|
-
.get() as { state: string; first_pulled_at: string; last_pulled_at: string };
|
|
1668
|
-
assert.equal(before.state, 'open');
|
|
1669
|
-
assert.equal(listIssueCommentCalls, 0);
|
|
1670
|
-
|
|
1671
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1672
|
-
|
|
1673
|
-
const result = await service.syncRepository({ owner: 'openclaw', repo: 'openclaw' });
|
|
1674
|
-
const after = service.db
|
|
1675
|
-
.prepare("select state, closed_at_gh, first_pulled_at, last_pulled_at from threads where number = 42 and kind = 'issue'")
|
|
1676
|
-
.get() as {
|
|
1677
|
-
state: string;
|
|
1678
|
-
closed_at_gh: string | null;
|
|
1679
|
-
first_pulled_at: string;
|
|
1680
|
-
last_pulled_at: string;
|
|
1681
|
-
};
|
|
1682
|
-
|
|
1683
|
-
assert.equal(result.threadsSynced, 0);
|
|
1684
|
-
assert.equal(result.threadsClosed, 1);
|
|
1685
|
-
assert.equal(after.state, 'closed');
|
|
1686
|
-
assert.equal(after.closed_at_gh, '2026-03-10T00:00:00Z');
|
|
1687
|
-
assert.equal(after.first_pulled_at, before.first_pulled_at);
|
|
1688
|
-
assert.notEqual(after.last_pulled_at, before.last_pulled_at);
|
|
1689
|
-
assert.equal(getIssueCalls, 1);
|
|
1690
|
-
assert.equal(listIssueCommentCalls, 0);
|
|
1691
|
-
assert.equal(service.listThreads({ owner: 'openclaw', repo: 'openclaw' }).threads.length, 0);
|
|
1692
|
-
} finally {
|
|
1693
|
-
service.close();
|
|
1694
|
-
}
|
|
1695
|
-
});
|
|
1696
|
-
|
|
1697
|
-
test('syncRepository treats missing stale pull requests as closed and continues', async () => {
|
|
1698
|
-
let listRepositoryIssuesCalls = 0;
|
|
1699
|
-
let getPullCalls = 0;
|
|
1700
|
-
const messages: string[] = [];
|
|
1701
|
-
|
|
1702
|
-
const service = makeTestService({
|
|
1703
|
-
checkAuth: async () => undefined,
|
|
1704
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
1705
|
-
listRepositoryIssues: async () => {
|
|
1706
|
-
listRepositoryIssuesCalls += 1;
|
|
1707
|
-
return listRepositoryIssuesCalls === 1
|
|
1708
|
-
? [
|
|
1709
|
-
{
|
|
1710
|
-
id: 101,
|
|
1711
|
-
number: 43,
|
|
1712
|
-
state: 'open',
|
|
1713
|
-
title: 'Fix downloader hang',
|
|
1714
|
-
body: 'Implements a fix.',
|
|
1715
|
-
html_url: 'https://github.com/openclaw/openclaw/pull/43',
|
|
1716
|
-
labels: [{ name: 'bug' }],
|
|
1717
|
-
assignees: [],
|
|
1718
|
-
pull_request: { url: 'https://api.github.com/repos/openclaw/openclaw/pulls/43' },
|
|
1719
|
-
user: { login: 'bob', type: 'User' },
|
|
1720
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
1721
|
-
},
|
|
1722
|
-
]
|
|
1723
|
-
: [];
|
|
1724
|
-
},
|
|
1725
|
-
getIssue: async () => {
|
|
1726
|
-
throw new Error('not expected');
|
|
1727
|
-
},
|
|
1728
|
-
getPull: async (_owner, _repo, number) => {
|
|
1729
|
-
getPullCalls += 1;
|
|
1730
|
-
if (getPullCalls === 1) {
|
|
1731
|
-
return {
|
|
1732
|
-
id: 101,
|
|
1733
|
-
number,
|
|
1734
|
-
state: 'open',
|
|
1735
|
-
title: 'Fix downloader hang',
|
|
1736
|
-
body: 'Implements a fix.',
|
|
1737
|
-
html_url: `https://github.com/openclaw/openclaw/pull/${number}`,
|
|
1738
|
-
labels: [{ name: 'bug' }],
|
|
1739
|
-
assignees: [],
|
|
1740
|
-
user: { login: 'bob', type: 'User' },
|
|
1741
|
-
draft: false,
|
|
1742
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
1743
|
-
};
|
|
1744
|
-
}
|
|
1745
|
-
throw Object.assign(new Error('GitHub request failed for GET /repos/openclaw/openclaw/pulls/43: Not Found'), {
|
|
1746
|
-
status: 404,
|
|
1747
|
-
});
|
|
1748
|
-
},
|
|
1749
|
-
listIssueComments: async () => [],
|
|
1750
|
-
listPullReviews: async () => [],
|
|
1751
|
-
listPullReviewComments: async () => [],
|
|
1752
|
-
});
|
|
1753
|
-
|
|
1754
|
-
try {
|
|
1755
|
-
await service.syncRepository({ owner: 'openclaw', repo: 'openclaw' });
|
|
1756
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1757
|
-
|
|
1758
|
-
const result = await service.syncRepository({
|
|
1759
|
-
owner: 'openclaw',
|
|
1760
|
-
repo: 'openclaw',
|
|
1761
|
-
onProgress: (message) => messages.push(message),
|
|
1762
|
-
});
|
|
1763
|
-
const after = service.db
|
|
1764
|
-
.prepare("select state, closed_at_gh, last_pulled_at from threads where number = 43 and kind = 'pull_request'")
|
|
1765
|
-
.get() as {
|
|
1766
|
-
state: string;
|
|
1767
|
-
closed_at_gh: string | null;
|
|
1768
|
-
last_pulled_at: string | null;
|
|
1769
|
-
};
|
|
1770
|
-
|
|
1771
|
-
assert.equal(result.threadsSynced, 0);
|
|
1772
|
-
assert.equal(result.threadsClosed, 1);
|
|
1773
|
-
assert.equal(after.state, 'closed');
|
|
1774
|
-
assert.ok(after.closed_at_gh);
|
|
1775
|
-
assert.ok(after.last_pulled_at);
|
|
1776
|
-
assert.equal(getPullCalls, 2);
|
|
1777
|
-
assert.match(messages.join('\n'), /missing on GitHub; marking it closed locally and continuing/);
|
|
1778
|
-
} finally {
|
|
1779
|
-
service.close();
|
|
1780
|
-
}
|
|
1781
|
-
});
|
|
1782
|
-
|
|
1783
|
-
test('syncRepository skips stale-open reconciliation for filtered crawls', async () => {
|
|
1784
|
-
let listRepositoryIssuesCalls = 0;
|
|
1785
|
-
let getIssueCalls = 0;
|
|
1786
|
-
|
|
1787
|
-
const service = makeTestService({
|
|
1788
|
-
checkAuth: async () => undefined,
|
|
1789
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
1790
|
-
listRepositoryIssues: async (_owner, _repo, _since, limit) => {
|
|
1791
|
-
listRepositoryIssuesCalls += 1;
|
|
1792
|
-
return listRepositoryIssuesCalls === 1
|
|
1793
|
-
? [
|
|
1794
|
-
{
|
|
1795
|
-
id: 100,
|
|
1796
|
-
number: 42,
|
|
1797
|
-
state: 'open',
|
|
1798
|
-
title: 'Downloader hangs',
|
|
1799
|
-
body: 'The transfer never finishes.',
|
|
1800
|
-
html_url: 'https://github.com/openclaw/openclaw/issues/42',
|
|
1801
|
-
labels: [{ name: 'bug' }],
|
|
1802
|
-
assignees: [],
|
|
1803
|
-
user: { login: 'alice', type: 'User' },
|
|
1804
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
1805
|
-
},
|
|
1806
|
-
].slice(0, limit ?? 1)
|
|
1807
|
-
: [];
|
|
1808
|
-
},
|
|
1809
|
-
getIssue: async (_owner, _repo, number) => {
|
|
1810
|
-
getIssueCalls += 1;
|
|
1811
|
-
return {
|
|
1812
|
-
id: 100,
|
|
1813
|
-
number,
|
|
1814
|
-
state: 'closed',
|
|
1815
|
-
title: 'Downloader hangs',
|
|
1816
|
-
body: 'The transfer never finishes.',
|
|
1817
|
-
html_url: `https://github.com/openclaw/openclaw/issues/${number}`,
|
|
1818
|
-
labels: [{ name: 'bug' }],
|
|
1819
|
-
assignees: [],
|
|
1820
|
-
user: { login: 'alice', type: 'User' },
|
|
1821
|
-
updated_at: '2026-03-10T00:00:00Z',
|
|
1822
|
-
closed_at: '2026-03-10T00:00:00Z',
|
|
1823
|
-
};
|
|
1824
|
-
},
|
|
1825
|
-
getPull: async () => {
|
|
1826
|
-
throw new Error('not expected');
|
|
1827
|
-
},
|
|
1828
|
-
listIssueComments: async () => [],
|
|
1829
|
-
listPullReviews: async () => [],
|
|
1830
|
-
listPullReviewComments: async () => [],
|
|
1831
|
-
});
|
|
1832
|
-
|
|
1833
|
-
try {
|
|
1834
|
-
await service.syncRepository({ owner: 'openclaw', repo: 'openclaw' });
|
|
1835
|
-
const result = await service.syncRepository({ owner: 'openclaw', repo: 'openclaw', limit: 1 });
|
|
1836
|
-
const after = service.db
|
|
1837
|
-
.prepare("select state from threads where number = 42 and kind = 'issue'")
|
|
1838
|
-
.get() as { state: string };
|
|
1839
|
-
|
|
1840
|
-
assert.equal(result.threadsClosed, 0);
|
|
1841
|
-
assert.equal(getIssueCalls, 0);
|
|
1842
|
-
assert.equal(after.state, 'open');
|
|
1843
|
-
} finally {
|
|
1844
|
-
service.close();
|
|
1845
|
-
}
|
|
1846
|
-
});
|
|
1847
|
-
|
|
1848
|
-
test('syncRepository derives the default overlapping since window from the last completed full scan', async () => {
|
|
1849
|
-
const sinceValues: Array<string | undefined> = [];
|
|
1850
|
-
|
|
1851
|
-
const service = makeTestService({
|
|
1852
|
-
checkAuth: async () => undefined,
|
|
1853
|
-
getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }),
|
|
1854
|
-
listRepositoryIssues: async (_owner, _repo, since) => {
|
|
1855
|
-
sinceValues.push(since);
|
|
1856
|
-
return [
|
|
1857
|
-
{
|
|
1858
|
-
id: 100,
|
|
1859
|
-
number: 42,
|
|
1860
|
-
state: 'open',
|
|
1861
|
-
title: 'Downloader hangs',
|
|
1862
|
-
body: 'The transfer never finishes.',
|
|
1863
|
-
html_url: 'https://github.com/openclaw/openclaw/issues/42',
|
|
1864
|
-
labels: [{ name: 'bug' }],
|
|
1865
|
-
assignees: [],
|
|
1866
|
-
user: { login: 'alice', type: 'User' },
|
|
1867
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
1868
|
-
},
|
|
1869
|
-
];
|
|
1870
|
-
},
|
|
1871
|
-
getIssue: async (_owner, _repo, number) => ({
|
|
1872
|
-
id: 100,
|
|
1873
|
-
number,
|
|
1874
|
-
state: 'open',
|
|
1875
|
-
title: 'Downloader hangs',
|
|
1876
|
-
body: 'The transfer never finishes.',
|
|
1877
|
-
html_url: `https://github.com/openclaw/openclaw/issues/${number}`,
|
|
1878
|
-
labels: [{ name: 'bug' }],
|
|
1879
|
-
assignees: [],
|
|
1880
|
-
user: { login: 'alice', type: 'User' },
|
|
1881
|
-
updated_at: '2026-03-09T00:00:00Z',
|
|
1882
|
-
}),
|
|
1883
|
-
getPull: async () => {
|
|
1884
|
-
throw new Error('not expected');
|
|
1885
|
-
},
|
|
1886
|
-
listIssueComments: async () => [],
|
|
1887
|
-
listPullReviews: async () => [],
|
|
1888
|
-
listPullReviewComments: async () => [],
|
|
1889
|
-
});
|
|
1890
|
-
|
|
1891
|
-
try {
|
|
1892
|
-
await service.syncRepository({
|
|
1893
|
-
owner: 'openclaw',
|
|
1894
|
-
repo: 'openclaw',
|
|
1895
|
-
startedAt: '2026-03-09T13:13:00.000Z',
|
|
1896
|
-
});
|
|
1897
|
-
await service.syncRepository({
|
|
1898
|
-
owner: 'openclaw',
|
|
1899
|
-
repo: 'openclaw',
|
|
1900
|
-
startedAt: '2026-03-09T14:13:01.000Z',
|
|
1901
|
-
});
|
|
1902
|
-
|
|
1903
|
-
assert.equal(sinceValues[0], undefined);
|
|
1904
|
-
assert.equal(sinceValues[1], '2026-03-09T12:13:01.000Z');
|
|
1905
|
-
|
|
1906
|
-
const syncState = service.db
|
|
1907
|
-
.prepare(
|
|
1908
|
-
`select
|
|
1909
|
-
last_full_open_scan_started_at,
|
|
1910
|
-
last_overlapping_open_scan_completed_at,
|
|
1911
|
-
last_non_overlapping_scan_completed_at,
|
|
1912
|
-
last_open_close_reconciled_at
|
|
1913
|
-
from repo_sync_state`,
|
|
1914
|
-
)
|
|
1915
|
-
.get() as {
|
|
1916
|
-
last_full_open_scan_started_at: string | null;
|
|
1917
|
-
last_overlapping_open_scan_completed_at: string | null;
|
|
1918
|
-
last_non_overlapping_scan_completed_at: string | null;
|
|
1919
|
-
last_open_close_reconciled_at: string | null;
|
|
1920
|
-
};
|
|
1921
|
-
|
|
1922
|
-
const rows = service.db.prepare("select stats_json from sync_runs where status = 'completed' order by id asc").all() as Array<{
|
|
1923
|
-
stats_json: string | null;
|
|
1924
|
-
}>;
|
|
1925
|
-
const firstStats = JSON.parse(rows[0]?.stats_json ?? '{}') as Record<string, unknown>;
|
|
1926
|
-
const secondStats = JSON.parse(rows[1]?.stats_json ?? '{}') as Record<string, unknown>;
|
|
1927
|
-
|
|
1928
|
-
assert.equal(firstStats.isFullOpenScan, true);
|
|
1929
|
-
assert.equal(firstStats.effectiveSince, null);
|
|
1930
|
-
assert.equal(secondStats.isOverlappingOpenScan, true);
|
|
1931
|
-
assert.equal(secondStats.effectiveSince, '2026-03-09T12:13:01.000Z');
|
|
1932
|
-
assert.equal(syncState.last_full_open_scan_started_at, '2026-03-09T13:13:00.000Z');
|
|
1933
|
-
assert.ok(syncState.last_overlapping_open_scan_completed_at);
|
|
1934
|
-
assert.ok(Date.parse(syncState.last_overlapping_open_scan_completed_at) >= Date.parse('2026-03-09T14:13:01.000Z'));
|
|
1935
|
-
assert.equal(syncState.last_non_overlapping_scan_completed_at, null);
|
|
1936
|
-
assert.equal(syncState.last_open_close_reconciled_at, syncState.last_overlapping_open_scan_completed_at);
|
|
1937
|
-
} finally {
|
|
1938
|
-
service.close();
|
|
1939
|
-
}
|
|
1940
|
-
});
|
|
1941
|
-
|
|
1942
|
-
test('repository-scoped reads and neighbors do not leak across repos in the same database', () => {
|
|
1943
|
-
const service = makeTestService({
|
|
1944
|
-
checkAuth: async () => undefined,
|
|
1945
|
-
getRepo: async () => ({ id: 1, full_name: 'owner-one/repo-one' }),
|
|
1946
|
-
listRepositoryIssues: async () => [],
|
|
1947
|
-
getIssue: async () => {
|
|
1948
|
-
throw new Error('not expected');
|
|
1949
|
-
},
|
|
1950
|
-
getPull: async () => {
|
|
1951
|
-
throw new Error('not expected');
|
|
1952
|
-
},
|
|
1953
|
-
listIssueComments: async () => [],
|
|
1954
|
-
listPullReviews: async () => [],
|
|
1955
|
-
listPullReviewComments: async () => [],
|
|
1956
|
-
});
|
|
1957
|
-
|
|
1958
|
-
try {
|
|
1959
|
-
const now = '2026-03-09T00:00:00Z';
|
|
1960
|
-
const insertRepo = service.db.prepare(
|
|
1961
|
-
`insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at)
|
|
1962
|
-
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
1963
|
-
);
|
|
1964
|
-
insertRepo.run(1, 'owner-one', 'repo-one', 'owner-one/repo-one', '1', '{}', now);
|
|
1965
|
-
insertRepo.run(2, 'owner-two', 'repo-two', 'owner-two/repo-two', '2', '{}', now);
|
|
1966
|
-
|
|
1967
|
-
const insertThread = service.db.prepare(
|
|
1968
|
-
`insert into threads (
|
|
1969
|
-
id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url,
|
|
1970
|
-
labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh,
|
|
1971
|
-
closed_at_gh, merged_at_gh, first_pulled_at, last_pulled_at, updated_at
|
|
1972
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1973
|
-
);
|
|
1974
|
-
insertThread.run(10, 1, '100', 42, 'issue', 'open', 'Repo one issue', 'body', 'alice', 'User', 'https://github.com/owner-one/repo-one/issues/42', '[]', '[]', '{}', 'hash-10', 0, now, now, null, null, now, now, now);
|
|
1975
|
-
insertThread.run(11, 1, '101', 43, 'pull_request', 'open', 'Repo one pr', 'body', 'bob', 'User', 'https://github.com/owner-one/repo-one/pull/43', '[]', '[]', '{}', 'hash-11', 0, now, now, null, null, now, now, now);
|
|
1976
|
-
insertThread.run(20, 2, '200', 42, 'issue', 'open', 'Repo two issue', 'body', 'carol', 'User', 'https://github.com/owner-two/repo-two/issues/42', '[]', '[]', '{}', 'hash-20', 0, now, now, null, null, now, now, now);
|
|
1977
|
-
|
|
1978
|
-
service.db
|
|
1979
|
-
.prepare(`insert into cluster_runs (id, repo_id, scope, status, started_at, finished_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1980
|
-
.run(1, 1, 'owner-one/repo-one', 'completed', now, now);
|
|
1981
|
-
service.db
|
|
1982
|
-
.prepare(`insert into cluster_runs (id, repo_id, scope, status, started_at, finished_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1983
|
-
.run(2, 2, 'owner-two/repo-two', 'completed', now, now);
|
|
1984
|
-
service.db
|
|
1985
|
-
.prepare(`insert into clusters (id, repo_id, cluster_run_id, representative_thread_id, member_count, created_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1986
|
-
.run(100, 1, 1, 10, 2, now);
|
|
1987
|
-
service.db
|
|
1988
|
-
.prepare(`insert into clusters (id, repo_id, cluster_run_id, representative_thread_id, member_count, created_at) values (?, ?, ?, ?, ?, ?)`)
|
|
1989
|
-
.run(200, 2, 2, 20, 1, now);
|
|
1990
|
-
service.db
|
|
1991
|
-
.prepare(`insert into cluster_members (cluster_id, thread_id, score_to_representative, created_at) values (?, ?, ?, ?)`)
|
|
1992
|
-
.run(100, 10, null, now);
|
|
1993
|
-
service.db
|
|
1994
|
-
.prepare(`insert into cluster_members (cluster_id, thread_id, score_to_representative, created_at) values (?, ?, ?, ?)`)
|
|
1995
|
-
.run(100, 11, 0.91, now);
|
|
1996
|
-
service.db
|
|
1997
|
-
.prepare(`insert into cluster_members (cluster_id, thread_id, score_to_representative, created_at) values (?, ?, ?, ?)`)
|
|
1998
|
-
.run(200, 20, null, now);
|
|
1999
|
-
|
|
2000
|
-
const insertEmbedding = service.db.prepare(
|
|
2001
|
-
`insert into document_embeddings (thread_id, source_kind, model, dimensions, content_hash, embedding_json, created_at, updated_at)
|
|
2002
|
-
values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2003
|
-
);
|
|
2004
|
-
insertEmbedding.run(10, 'title', 'text-embedding-3-large', 2, 'hash-a', '[1,0]', now, now);
|
|
2005
|
-
insertEmbedding.run(11, 'title', 'text-embedding-3-large', 2, 'hash-b', '[0.9,0.1]', now, now);
|
|
2006
|
-
insertEmbedding.run(20, 'title', 'text-embedding-3-large', 2, 'hash-c', '[1,0]', now, now);
|
|
2007
|
-
|
|
2008
|
-
const repoOneThreads = service.listThreads({ owner: 'owner-one', repo: 'repo-one' });
|
|
2009
|
-
assert.equal(repoOneThreads.threads.length, 2);
|
|
2010
|
-
assert.deepEqual(
|
|
2011
|
-
repoOneThreads.threads.map((thread) => thread.number),
|
|
2012
|
-
[43, 42],
|
|
2013
|
-
);
|
|
2014
|
-
|
|
2015
|
-
const repoOneSnapshot = service.getTuiSnapshot({ owner: 'owner-one', repo: 'repo-one', minSize: 0 });
|
|
2016
|
-
assert.equal(repoOneSnapshot.repository.fullName, 'owner-one/repo-one');
|
|
2017
|
-
assert.deepEqual(
|
|
2018
|
-
repoOneSnapshot.clusters.map((cluster) => cluster.clusterId),
|
|
2019
|
-
[100],
|
|
2020
|
-
);
|
|
2021
|
-
|
|
2022
|
-
const repoOneNeighbors = service.listNeighbors({
|
|
2023
|
-
owner: 'owner-one',
|
|
2024
|
-
repo: 'repo-one',
|
|
2025
|
-
threadNumber: 42,
|
|
2026
|
-
limit: 5,
|
|
2027
|
-
minScore: 0.1,
|
|
2028
|
-
});
|
|
2029
|
-
assert.deepEqual(
|
|
2030
|
-
repoOneNeighbors.neighbors.map((neighbor) => neighbor.number),
|
|
2031
|
-
[43],
|
|
2032
|
-
);
|
|
2033
|
-
} finally {
|
|
2034
|
-
service.close();
|
|
2035
|
-
}
|
|
2036
|
-
});
|