@iceinvein/agent-skills 0.23.9 → 0.23.13
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/package.json +1 -1
- package/skills/index.json +2 -2
- package/skills/magpie/SKILL.md +21 -20
- package/skills/magpie/bin/magpie.ts +63 -2
- package/skills/magpie/evals/quality/README.md +74 -1
- package/skills/magpie/evals/quality/__tests__/probe-specialist.test.ts +498 -0
- package/skills/magpie/evals/quality/__tests__/replay-full.test.ts +9 -0
- package/skills/magpie/evals/quality/probe-specialist.ts +672 -0
- package/skills/magpie/evals/quality/replay-full.ts +6 -2
- package/skills/magpie/fixtures/fake-gh-with-lockfile.sh +37 -2
- package/skills/magpie/fixtures/fake-gh.sh +49 -2
- package/skills/magpie/references/critic.md +20 -4
- package/skills/magpie/references/proof.md +102 -0
- package/skills/magpie/references/scout.md +6 -1
- package/skills/magpie/references/specialists.md +15 -9
- package/skills/magpie/scripts/__tests__/cleanup-cmd.test.ts +103 -7
- package/skills/magpie/scripts/__tests__/cli.test.ts +69 -0
- package/skills/magpie/scripts/__tests__/critic-cmd.test.ts +46 -0
- package/skills/magpie/scripts/__tests__/critic.test.ts +11 -1
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +20 -1
- package/skills/magpie/scripts/__tests__/git-diff.test.ts +23 -4
- package/skills/magpie/scripts/__tests__/handoff-cmd.test.ts +68 -0
- package/skills/magpie/scripts/__tests__/history.test.ts +232 -0
- package/skills/magpie/scripts/__tests__/impact-cmd.test.ts +477 -0
- package/skills/magpie/scripts/__tests__/impact-render.test.ts +365 -0
- package/skills/magpie/scripts/__tests__/impact.test.ts +915 -0
- package/skills/magpie/scripts/__tests__/incremental.test.ts +59 -8
- package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +1 -1
- package/skills/magpie/scripts/__tests__/post-cmd.test.ts +58 -0
- package/skills/magpie/scripts/__tests__/pr-activity.test.ts +281 -0
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +4 -4
- package/skills/magpie/scripts/__tests__/previous-findings.test.ts +267 -0
- package/skills/magpie/scripts/__tests__/proof-cmd.test.ts +260 -0
- package/skills/magpie/scripts/__tests__/proof.test.ts +226 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +71 -0
- package/skills/magpie/scripts/__tests__/render-annotation.test.ts +34 -0
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +75 -0
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +9 -0
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +376 -1
- package/skills/magpie/scripts/__tests__/shard-coverage.test.ts +79 -0
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +48 -2
- package/skills/magpie/scripts/__tests__/status-cmd.test.ts +20 -8
- package/skills/magpie/scripts/__tests__/types.test.ts +46 -0
- package/skills/magpie/scripts/cleanup-cmd.ts +88 -0
- package/skills/magpie/scripts/critic-cmd.ts +2 -0
- package/skills/magpie/scripts/critic.ts +4 -0
- package/skills/magpie/scripts/dedupe-cmd.ts +4 -0
- package/skills/magpie/scripts/evidence-filter.ts +10 -0
- package/skills/magpie/scripts/git-diff.ts +50 -19
- package/skills/magpie/scripts/handoff-cmd.ts +42 -3
- package/skills/magpie/scripts/history.ts +143 -0
- package/skills/magpie/scripts/impact-cmd.ts +202 -0
- package/skills/magpie/scripts/impact-render.ts +158 -0
- package/skills/magpie/scripts/impact.ts +622 -0
- package/skills/magpie/scripts/incremental.ts +44 -5
- package/skills/magpie/scripts/labels-cmd.ts +7 -3
- package/skills/magpie/scripts/post-cmd.ts +46 -0
- package/skills/magpie/scripts/pr-activity.ts +243 -0
- package/skills/magpie/scripts/preview-cmd.ts +3 -0
- package/skills/magpie/scripts/previous-findings.ts +181 -0
- package/skills/magpie/scripts/proof-cmd.ts +226 -0
- package/skills/magpie/scripts/proof.ts +150 -0
- package/skills/magpie/scripts/refresh.ts +11 -0
- package/skills/magpie/scripts/render-annotation.ts +17 -0
- package/skills/magpie/scripts/render-cmd.ts +12 -0
- package/skills/magpie/scripts/render-findings.ts +37 -1
- package/skills/magpie/scripts/render-progress.ts +3 -0
- package/skills/magpie/scripts/setup-cmd.ts +142 -11
- package/skills/magpie/scripts/shard-coverage.ts +53 -0
- package/skills/magpie/scripts/status-cmd.ts +2 -0
- package/skills/magpie/scripts/types.ts +39 -0
- package/skills/magpie/skill.json +1 -1
- package/skills/magpie/templates/styles.css +30 -0
- package/skills/sluice/scripts/status.sh +32 -13
- package/skills/sluice/skill.json +1 -1
|
@@ -55,6 +55,7 @@ const TEMPLATE_DOC = [
|
|
|
55
55
|
'Candidates: <<CANDIDATES>>',
|
|
56
56
|
'Groups: <<MERGE_CANDIDATES>>',
|
|
57
57
|
'Rules: <<REVIEW_RULES>>',
|
|
58
|
+
'Raised: <<ALREADY_RAISED>>',
|
|
58
59
|
'Worktree: <<WORKTREE>>',
|
|
59
60
|
'Diff: <<DIFF_PATH>>',
|
|
60
61
|
'Write to: <<OUTPUT_PATH>>',
|
|
@@ -90,6 +91,51 @@ test('critic-prompt writes the prompt file from the magpie-critic block and prin
|
|
|
90
91
|
expect(prompt).toContain(`Write to: ${outputPath}`)
|
|
91
92
|
})
|
|
92
93
|
|
|
94
|
+
test('critic-prompt fills <<ALREADY_RAISED>> from the PR activity and previous dismissals', async () => {
|
|
95
|
+
const templatePath = join(runDir, 'critic.md')
|
|
96
|
+
await writeFile(templatePath, TEMPLATE_DOC)
|
|
97
|
+
await writeDeduped([finding('a', 'bugs')])
|
|
98
|
+
await writeFile(
|
|
99
|
+
join(runDir, 'pr-activity.json'),
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
checks: [{ name: 'build', conclusion: 'FAILURE', url: null }],
|
|
102
|
+
threads: [
|
|
103
|
+
{
|
|
104
|
+
file: 'src/a.ts',
|
|
105
|
+
line: 10,
|
|
106
|
+
resolved: false,
|
|
107
|
+
outdated: false,
|
|
108
|
+
author: 'dave',
|
|
109
|
+
body: 'Off by one here.',
|
|
110
|
+
replies: 0,
|
|
111
|
+
magpie: false,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
comments: [],
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const exit = await runCriticPrompt(runDir, { templatePath })
|
|
119
|
+
|
|
120
|
+
expect(exit).toBe(0)
|
|
121
|
+
const prompt = await readFile(join(runDir, 'critic-prompt.md'), 'utf8')
|
|
122
|
+
expect(prompt).toContain(
|
|
123
|
+
'Raised: Open review threads:\n- src/a.ts:10 (dave, 0 replies): Off by one here.\nWorktree',
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('critic-prompt fills <<ALREADY_RAISED>> with (none) on a run with no history', async () => {
|
|
128
|
+
const templatePath = join(runDir, 'critic.md')
|
|
129
|
+
await writeFile(templatePath, TEMPLATE_DOC)
|
|
130
|
+
await writeDeduped([finding('a', 'bugs')])
|
|
131
|
+
|
|
132
|
+
const exit = await runCriticPrompt(runDir, { templatePath })
|
|
133
|
+
|
|
134
|
+
expect(exit).toBe(0)
|
|
135
|
+
const prompt = await readFile(join(runDir, 'critic-prompt.md'), 'utf8')
|
|
136
|
+
expect(prompt).toContain('Raised: (none)\n')
|
|
137
|
+
})
|
|
138
|
+
|
|
93
139
|
test('critic-prompt points <<WORKTREE>> at the review root named in target.json', async () => {
|
|
94
140
|
const templatePath = join(runDir, 'critic.md')
|
|
95
141
|
await writeFile(templatePath, TEMPLATE_DOC)
|
|
@@ -214,6 +214,7 @@ const ALL_PLACEHOLDERS = [
|
|
|
214
214
|
'C=<<CANDIDATES>>',
|
|
215
215
|
'M=<<MERGE_CANDIDATES>>',
|
|
216
216
|
'R=<<REVIEW_RULES>>',
|
|
217
|
+
'A=<<ALREADY_RAISED>>',
|
|
217
218
|
'W=<<WORKTREE>>',
|
|
218
219
|
'D=<<DIFF_PATH>>',
|
|
219
220
|
'O=<<OUTPUT_PATH>>',
|
|
@@ -240,6 +241,7 @@ test('three candidates yield one prompt with every placeholder replaced and crit
|
|
|
240
241
|
],
|
|
241
242
|
mergeCandidates: [['c1', 'c2']],
|
|
242
243
|
reviewRules: [{ rule: 'no default exports', source: 'CLAUDE.md' }],
|
|
244
|
+
alreadyRaised: 'Open review threads:\n- src/a.ts:10 (dave, 0 replies): off by one',
|
|
243
245
|
worktree: '/run/worktree',
|
|
244
246
|
diffPath: '/run/diff.patch',
|
|
245
247
|
runDir: '/run',
|
|
@@ -265,6 +267,9 @@ test('three candidates yield one prompt with every placeholder replaced and crit
|
|
|
265
267
|
})
|
|
266
268
|
expect(section(only.prompt, 'M', 'R')).toEqual([['c1', 'c2']])
|
|
267
269
|
expect(only.prompt).toContain('R=- no default exports (CLAUDE.md)\n')
|
|
270
|
+
expect(only.prompt).toContain(
|
|
271
|
+
'A=Open review threads:\n- src/a.ts:10 (dave, 0 replies): off by one\n',
|
|
272
|
+
)
|
|
268
273
|
expect(only.prompt).toContain('W=/run/worktree\n')
|
|
269
274
|
expect(only.prompt).toContain('D=/run/diff.patch\n')
|
|
270
275
|
expect(only.prompt.endsWith('O=/run/critic.json')).toBe(true)
|
|
@@ -276,6 +281,7 @@ test('sixty-five candidates at batch size thirty yield three numbered prompts',
|
|
|
276
281
|
candidates: candidates(65),
|
|
277
282
|
mergeCandidates: [],
|
|
278
283
|
reviewRules: [],
|
|
284
|
+
alreadyRaised: '(none)',
|
|
279
285
|
worktree: '/run/worktree',
|
|
280
286
|
diffPath: '/run/diff.patch',
|
|
281
287
|
runDir: '/run',
|
|
@@ -298,6 +304,7 @@ test('a merge group straddling a batch boundary is placed whole in the batch of
|
|
|
298
304
|
candidates: candidates(35),
|
|
299
305
|
mergeCandidates: [['c30', 'c31']],
|
|
300
306
|
reviewRules: [],
|
|
307
|
+
alreadyRaised: '(none)',
|
|
301
308
|
worktree: '/run/worktree',
|
|
302
309
|
diffPath: '/run/diff.patch',
|
|
303
310
|
runDir: '/run',
|
|
@@ -319,10 +326,13 @@ test('a template missing placeholders throws naming each missing one', () => {
|
|
|
319
326
|
candidates: candidates(1),
|
|
320
327
|
mergeCandidates: [],
|
|
321
328
|
reviewRules: [],
|
|
329
|
+
alreadyRaised: '(none)',
|
|
322
330
|
worktree: '/run/worktree',
|
|
323
331
|
diffPath: '/run/diff.patch',
|
|
324
332
|
runDir: '/run',
|
|
325
333
|
batchSize: 30,
|
|
326
334
|
}),
|
|
327
|
-
).toThrow(
|
|
335
|
+
).toThrow(
|
|
336
|
+
'<<MERGE_CANDIDATES>>, <<REVIEW_RULES>>, <<ALREADY_RAISED>>, <<WORKTREE>>, <<DIFF_PATH>>',
|
|
337
|
+
)
|
|
328
338
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { runDedupe } from '../dedupe-cmd.ts'
|
|
@@ -337,6 +337,25 @@ test('runDedupe names the (focus, shard) pairs that have no findings file', asyn
|
|
|
337
337
|
})
|
|
338
338
|
})
|
|
339
339
|
|
|
340
|
+
test('runDedupe does not count a shard skipped at the gate as missing', async () => {
|
|
341
|
+
await writeManifest(2)
|
|
342
|
+
for (const focus of LLM_FOCUSES) {
|
|
343
|
+
await writeFile(
|
|
344
|
+
join(runDir, 'findings', `${focus}.shard-1.json`),
|
|
345
|
+
JSON.stringify([f(`${focus}-1`, 'a.ts', 1, `${focus} on shard 1`, focus)]),
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
await appendFile(
|
|
349
|
+
join(runDir, 'log.jsonl'),
|
|
350
|
+
`${JSON.stringify({ stage: 'shard-coverage', status: 'partial', reviewed: [1], skipped: [2] })}\n`,
|
|
351
|
+
)
|
|
352
|
+
const { exit, stdout } = await dedupeCapturingStdout()
|
|
353
|
+
expect(exit).toBe(0)
|
|
354
|
+
expect(stdout).not.toContain('missing')
|
|
355
|
+
const done = await dedupeLogEntry('done')
|
|
356
|
+
expect(done?.coverage).toEqual({ expected: 5, missing: [] })
|
|
357
|
+
})
|
|
358
|
+
|
|
340
359
|
test('a single-shard manifest expects the unsharded findings filenames', async () => {
|
|
341
360
|
await writeManifest(1)
|
|
342
361
|
for (const focus of ['security', 'bugs', 'performance', 'code-smells']) {
|
|
@@ -60,7 +60,7 @@ test('localPrDiff errors when the base branch is not present locally', async ()
|
|
|
60
60
|
expect(result.error).toContain('cannot resolve base branch')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
test('localPrDiff
|
|
63
|
+
test('localPrDiff uses the PR head SHA when it is local, whatever a stale refs/magpie/pr-<n> says', async () => {
|
|
64
64
|
const { repoPath, head } = await createGitFixtureRepo()
|
|
65
65
|
repo = repoPath
|
|
66
66
|
// Simulate a previous run's fetch having landed an older commit under the
|
|
@@ -75,9 +75,28 @@ test('localPrDiff errors when a stale refs/magpie/pr-<n> ref points at an older
|
|
|
75
75
|
baseRefName: 'main',
|
|
76
76
|
headRefOid: head,
|
|
77
77
|
})
|
|
78
|
+
expect(result.ok).toBe(true)
|
|
79
|
+
if (!result.ok) return
|
|
80
|
+
expect(result.mergeBase).toBe(staleSha)
|
|
81
|
+
expect(result.diff).toContain('+export const x = 2')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('localPrDiff errors when the PR head SHA is missing and refs/magpie/pr-<n> points elsewhere', async () => {
|
|
85
|
+
const { repoPath, head } = await createGitFixtureRepo()
|
|
86
|
+
repo = repoPath
|
|
87
|
+
const staleSha = await git(repoPath, 'rev-parse', `${head}~1`)
|
|
88
|
+
await git(repoPath, 'update-ref', 'refs/magpie/pr-7', staleSha)
|
|
89
|
+
const missing = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
|
|
90
|
+
const result = await localPrDiff({
|
|
91
|
+
gitBin: 'git',
|
|
92
|
+
repoPath,
|
|
93
|
+
prNumber: 7,
|
|
94
|
+
baseRefName: 'main',
|
|
95
|
+
headRefOid: missing,
|
|
96
|
+
})
|
|
78
97
|
expect(result.ok).toBe(false)
|
|
79
98
|
if (result.ok) return
|
|
80
|
-
expect(result.error).
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
expect(result.error).toBe(
|
|
100
|
+
`local PR head ${staleSha} does not match the PR head ${missing}; the local ref is stale`,
|
|
101
|
+
)
|
|
83
102
|
})
|
|
@@ -317,3 +317,71 @@ test('runHandoff still prints, stale, when the review root is gone', async () =>
|
|
|
317
317
|
expect(stdout).toContain(WARNING)
|
|
318
318
|
expect(stderr).toContain('magpie: handoff: could not recompute the diff: ')
|
|
319
319
|
})
|
|
320
|
+
|
|
321
|
+
test('formatHandoff shows a reproduced finding with its command and repro source', () => {
|
|
322
|
+
const out = formatHandoff({
|
|
323
|
+
target: 't',
|
|
324
|
+
findings: [
|
|
325
|
+
{
|
|
326
|
+
...mustFix,
|
|
327
|
+
suggestion: undefined,
|
|
328
|
+
proof: {
|
|
329
|
+
status: 'reproduced',
|
|
330
|
+
reason: 'fails as described',
|
|
331
|
+
repro: {
|
|
332
|
+
path: 'a.test.ts',
|
|
333
|
+
command: 'bun test a.test.ts',
|
|
334
|
+
source: "expect(log).not.toContain('tok')",
|
|
335
|
+
output: 'received tok',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
stale: false,
|
|
341
|
+
})
|
|
342
|
+
expect(out).toContain(
|
|
343
|
+
"Reproduced with `bun test a.test.ts`:\n```\nexpect(log).not.toContain('tok')\n```",
|
|
344
|
+
)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test('formatHandoff says why a finding is not proven', () => {
|
|
348
|
+
const out = formatHandoff({
|
|
349
|
+
target: 't',
|
|
350
|
+
findings: [{ ...shouldFix, proof: { status: 'not-proven', reason: 'needs a live database' } }],
|
|
351
|
+
stale: false,
|
|
352
|
+
})
|
|
353
|
+
expect(out).toContain('Not proven: needs a live database')
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('formatHandoff warns which files went unreviewed', () => {
|
|
357
|
+
const out = formatHandoff({
|
|
358
|
+
target: 't',
|
|
359
|
+
findings: [],
|
|
360
|
+
stale: false,
|
|
361
|
+
unreviewed: { skipped: [{ id: 3, files: ['docs/d.md', 'src/e.ts'] }] },
|
|
362
|
+
})
|
|
363
|
+
expect(out).toBe(
|
|
364
|
+
'# magpie local review: t\nWarning: Not reviewed: shard 3 (2 files):\n- docs/d.md\n- src/e.ts\n\nNo findings.\n',
|
|
365
|
+
)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
test('formatHandoff lists findings a passing test disproved after the rest', () => {
|
|
369
|
+
const out = formatHandoff({
|
|
370
|
+
target: 't',
|
|
371
|
+
findings: [],
|
|
372
|
+
stale: false,
|
|
373
|
+
disproved: [
|
|
374
|
+
{
|
|
375
|
+
id: 'bugs-2',
|
|
376
|
+
file: 'src/cache.ts',
|
|
377
|
+
line: null,
|
|
378
|
+
title: 'Cache never evicts',
|
|
379
|
+
reason: 'evict() runs on every put',
|
|
380
|
+
repro: { path: 'p', command: 'bun test p', source: 's', output: 'o' },
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
})
|
|
384
|
+
expect(out).toBe(
|
|
385
|
+
'# magpie local review: t\n\nNo findings.\n\nDropped because a passing test disproved them:\n- src/cache.ts: Cache never evicts (evict() runs on every put; `bun test p`)\n',
|
|
386
|
+
)
|
|
387
|
+
})
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { readHistory, renderAlreadyRaised, renderHistory } from '../history.ts'
|
|
6
|
+
import type { PrActivity } from '../pr-activity.ts'
|
|
7
|
+
import type { PreviousFindings } from '../previous-findings.ts'
|
|
8
|
+
|
|
9
|
+
const ACTIVITY: PrActivity = {
|
|
10
|
+
checks: [
|
|
11
|
+
{ name: 'build', conclusion: 'FAILURE', url: 'https://ci.example/1' },
|
|
12
|
+
{ name: 'lint', conclusion: 'SUCCESS', url: null },
|
|
13
|
+
{ name: 'deploy', conclusion: 'TIMED_OUT', url: null },
|
|
14
|
+
],
|
|
15
|
+
threads: [
|
|
16
|
+
{
|
|
17
|
+
file: 'src/upload.ts',
|
|
18
|
+
line: 42,
|
|
19
|
+
resolved: false,
|
|
20
|
+
outdated: false,
|
|
21
|
+
author: 'dave',
|
|
22
|
+
body: 'This swallows\n\nthe timeout error.',
|
|
23
|
+
replies: 2,
|
|
24
|
+
magpie: false,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
file: 'src/cache.ts',
|
|
28
|
+
line: 9,
|
|
29
|
+
resolved: false,
|
|
30
|
+
outdated: true,
|
|
31
|
+
author: 'octocat',
|
|
32
|
+
body: '### High\nCache never evicts\n<!-- magpie:finding id=perf-1 hash=abc -->',
|
|
33
|
+
replies: 0,
|
|
34
|
+
magpie: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
file: 'src/done.ts',
|
|
38
|
+
line: 1,
|
|
39
|
+
resolved: true,
|
|
40
|
+
outdated: false,
|
|
41
|
+
author: 'erin',
|
|
42
|
+
body: 'Fixed already.',
|
|
43
|
+
replies: 1,
|
|
44
|
+
magpie: false,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
comments: [{ author: 'alice', body: 'Does this retry past three attempts?', magpie: false }],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PREVIOUS: PreviousFindings = {
|
|
51
|
+
previousRunId: 'pr-7-100',
|
|
52
|
+
findings: [
|
|
53
|
+
{
|
|
54
|
+
id: 'perf-3',
|
|
55
|
+
fromRunId: 'pr-7-50',
|
|
56
|
+
file: 'src/cache.ts',
|
|
57
|
+
line: 9,
|
|
58
|
+
title: 'Cache never evicts',
|
|
59
|
+
domain: 'performance',
|
|
60
|
+
label: 'dismissed',
|
|
61
|
+
reason: 'not-worth-it',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'bugs-1',
|
|
65
|
+
fromRunId: 'pr-7-100',
|
|
66
|
+
file: 'src/upload.ts',
|
|
67
|
+
line: 4,
|
|
68
|
+
title: 'Swallowed error',
|
|
69
|
+
domain: 'bugs',
|
|
70
|
+
label: 'posted',
|
|
71
|
+
anchor: 'unchanged',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test('already-raised lists open threads, comments and dismissals, skipping resolved threads', () => {
|
|
77
|
+
expect(renderAlreadyRaised({ activity: ACTIVITY, previous: PREVIOUS })).toBe(
|
|
78
|
+
[
|
|
79
|
+
'Open review threads:',
|
|
80
|
+
'- src/upload.ts:42 (dave, 2 replies): This swallows the timeout error.',
|
|
81
|
+
'- src/cache.ts:9 (magpie, earlier run) [outdated]: ### High Cache never evicts',
|
|
82
|
+
'',
|
|
83
|
+
'PR comments:',
|
|
84
|
+
'- alice: Does this retry past three attempts?',
|
|
85
|
+
'',
|
|
86
|
+
'Dismissed by the reviewer in an earlier magpie run:',
|
|
87
|
+
'- src/cache.ts:9 [not-worth-it]: Cache never evicts',
|
|
88
|
+
].join('\n'),
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('already-raised is (none) when nothing is on the PR and nothing was dismissed', () => {
|
|
93
|
+
expect(renderAlreadyRaised({ activity: null, previous: null })).toBe('(none)')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('a long comment is cut to 280 characters with an ellipsis', () => {
|
|
97
|
+
const long = 'a'.repeat(400)
|
|
98
|
+
const out = renderAlreadyRaised({
|
|
99
|
+
activity: { checks: [], threads: [], comments: [{ author: 'bot', body: long, magpie: false }] },
|
|
100
|
+
previous: null,
|
|
101
|
+
})
|
|
102
|
+
expect(out).toBe(`PR comments:\n- bot: ${'a'.repeat(280)}…`)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('history adds failing checks and the files changed since the last review', () => {
|
|
106
|
+
const out = renderHistory({
|
|
107
|
+
activity: ACTIVITY,
|
|
108
|
+
previous: PREVIOUS,
|
|
109
|
+
incremental: { previousSha: 'abc1234def', changedSincePrevious: ['src/upload.ts'] },
|
|
110
|
+
})
|
|
111
|
+
expect(out).toBe(
|
|
112
|
+
[
|
|
113
|
+
'## Already on this PR',
|
|
114
|
+
'',
|
|
115
|
+
'The threads, comments and dismissals below were raised before this review. Do not report a finding that repeats one of them: the critic drops repeats. Report it again only when you have something material it lacks, and say what that is.',
|
|
116
|
+
'',
|
|
117
|
+
'Open review threads:',
|
|
118
|
+
'- src/upload.ts:42 (dave, 2 replies): This swallows the timeout error.',
|
|
119
|
+
'- src/cache.ts:9 (magpie, earlier run) [outdated]: ### High Cache never evicts',
|
|
120
|
+
'',
|
|
121
|
+
'PR comments:',
|
|
122
|
+
'- alice: Does this retry past three attempts?',
|
|
123
|
+
'',
|
|
124
|
+
'Dismissed by the reviewer in an earlier magpie run:',
|
|
125
|
+
'- src/cache.ts:9 [not-worth-it]: Cache never evicts',
|
|
126
|
+
'',
|
|
127
|
+
'Leads, not repeats: a finding that explains one of these is wanted.',
|
|
128
|
+
'',
|
|
129
|
+
'Failing CI checks (a lead to investigate, not a finding in itself):',
|
|
130
|
+
'- build: FAILURE (https://ci.example/1)',
|
|
131
|
+
'- deploy: TIMED_OUT',
|
|
132
|
+
'',
|
|
133
|
+
'Files changed since the last review at abc1234 (the whole diff is still your scope):',
|
|
134
|
+
'- src/upload.ts',
|
|
135
|
+
].join('\n'),
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('history is empty when the run has no activity, no previous run and no incremental', () => {
|
|
140
|
+
expect(renderHistory({ activity: null, previous: null, incremental: null })).toBe('')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
let dir: string
|
|
144
|
+
|
|
145
|
+
beforeEach(async () => {
|
|
146
|
+
dir = await mkdtemp(join(tmpdir(), 'magpie-history-'))
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
afterEach(async () => {
|
|
150
|
+
await rm(dir, { recursive: true, force: true })
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('readHistory reads the three sidecars from a run dir', async () => {
|
|
154
|
+
await writeFile(join(dir, 'pr-activity.json'), JSON.stringify(ACTIVITY))
|
|
155
|
+
await writeFile(join(dir, 'previous-findings.json'), JSON.stringify(PREVIOUS))
|
|
156
|
+
await writeFile(
|
|
157
|
+
join(dir, 'incremental.json'),
|
|
158
|
+
JSON.stringify({
|
|
159
|
+
previousRunId: 'pr-7-100',
|
|
160
|
+
previousSha: 'abc1234def',
|
|
161
|
+
currentSha: 'fff',
|
|
162
|
+
sameSha: false,
|
|
163
|
+
changedSincePrevious: ['src/upload.ts'],
|
|
164
|
+
}),
|
|
165
|
+
)
|
|
166
|
+
const history = await readHistory(dir)
|
|
167
|
+
expect(history.activity?.threads).toHaveLength(3)
|
|
168
|
+
expect(history.previous?.previousRunId).toBe('pr-7-100')
|
|
169
|
+
expect(history.incremental).toEqual({
|
|
170
|
+
previousSha: 'abc1234def',
|
|
171
|
+
changedSincePrevious: ['src/upload.ts'],
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('readHistory reads a run with none of the sidecars as empty', async () => {
|
|
176
|
+
expect(await readHistory(dir)).toEqual({ activity: null, previous: null, incremental: null })
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('readHistory fails on a sidecar that does not parse, naming it', async () => {
|
|
180
|
+
await writeFile(join(dir, 'pr-activity.json'), '{not json')
|
|
181
|
+
await expect(readHistory(dir)).rejects.toThrow('pr-activity.json')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('history with only leads carries the leads heading and no repeats preamble', () => {
|
|
185
|
+
const out = renderHistory({
|
|
186
|
+
activity: {
|
|
187
|
+
checks: [{ name: 'build', conclusion: 'FAILURE', url: null }],
|
|
188
|
+
threads: [],
|
|
189
|
+
comments: [],
|
|
190
|
+
},
|
|
191
|
+
previous: null,
|
|
192
|
+
incremental: null,
|
|
193
|
+
})
|
|
194
|
+
expect(out).toBe(
|
|
195
|
+
[
|
|
196
|
+
'## Already on this PR',
|
|
197
|
+
'',
|
|
198
|
+
'Leads, not repeats: a finding that explains one of these is wanted.',
|
|
199
|
+
'',
|
|
200
|
+
'Failing CI checks (a lead to investigate, not a finding in itself):',
|
|
201
|
+
'- build: FAILURE',
|
|
202
|
+
].join('\n'),
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('already-raised leaves out comments from github-actions, which posts CI output', () => {
|
|
207
|
+
const out = renderAlreadyRaised({
|
|
208
|
+
activity: {
|
|
209
|
+
checks: [],
|
|
210
|
+
threads: [],
|
|
211
|
+
comments: [
|
|
212
|
+
{ author: 'github-actions', body: '<h2>Coverage Report</h2>', magpie: false },
|
|
213
|
+
{ author: 'coderabbitai', body: 'This retry never backs off.', magpie: false },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
previous: null,
|
|
217
|
+
})
|
|
218
|
+
expect(out).toBe('PR comments:\n- coderabbitai: This retry never backs off.')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('already-raised is (none) when the only comments are CI output', () => {
|
|
222
|
+
expect(
|
|
223
|
+
renderAlreadyRaised({
|
|
224
|
+
activity: {
|
|
225
|
+
checks: [],
|
|
226
|
+
threads: [],
|
|
227
|
+
comments: [{ author: 'github-actions', body: '<h2>Coverage Report</h2>', magpie: false }],
|
|
228
|
+
},
|
|
229
|
+
previous: null,
|
|
230
|
+
}),
|
|
231
|
+
).toBe('(none)')
|
|
232
|
+
})
|