@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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/skills/index.json +2 -2
  3. package/skills/magpie/SKILL.md +21 -20
  4. package/skills/magpie/bin/magpie.ts +63 -2
  5. package/skills/magpie/evals/quality/README.md +74 -1
  6. package/skills/magpie/evals/quality/__tests__/probe-specialist.test.ts +498 -0
  7. package/skills/magpie/evals/quality/__tests__/replay-full.test.ts +9 -0
  8. package/skills/magpie/evals/quality/probe-specialist.ts +672 -0
  9. package/skills/magpie/evals/quality/replay-full.ts +6 -2
  10. package/skills/magpie/fixtures/fake-gh-with-lockfile.sh +37 -2
  11. package/skills/magpie/fixtures/fake-gh.sh +49 -2
  12. package/skills/magpie/references/critic.md +20 -4
  13. package/skills/magpie/references/proof.md +102 -0
  14. package/skills/magpie/references/scout.md +6 -1
  15. package/skills/magpie/references/specialists.md +15 -9
  16. package/skills/magpie/scripts/__tests__/cleanup-cmd.test.ts +103 -7
  17. package/skills/magpie/scripts/__tests__/cli.test.ts +69 -0
  18. package/skills/magpie/scripts/__tests__/critic-cmd.test.ts +46 -0
  19. package/skills/magpie/scripts/__tests__/critic.test.ts +11 -1
  20. package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +20 -1
  21. package/skills/magpie/scripts/__tests__/git-diff.test.ts +23 -4
  22. package/skills/magpie/scripts/__tests__/handoff-cmd.test.ts +68 -0
  23. package/skills/magpie/scripts/__tests__/history.test.ts +232 -0
  24. package/skills/magpie/scripts/__tests__/impact-cmd.test.ts +477 -0
  25. package/skills/magpie/scripts/__tests__/impact-render.test.ts +365 -0
  26. package/skills/magpie/scripts/__tests__/impact.test.ts +915 -0
  27. package/skills/magpie/scripts/__tests__/incremental.test.ts +59 -8
  28. package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +1 -1
  29. package/skills/magpie/scripts/__tests__/post-cmd.test.ts +58 -0
  30. package/skills/magpie/scripts/__tests__/pr-activity.test.ts +281 -0
  31. package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +4 -4
  32. package/skills/magpie/scripts/__tests__/previous-findings.test.ts +267 -0
  33. package/skills/magpie/scripts/__tests__/proof-cmd.test.ts +260 -0
  34. package/skills/magpie/scripts/__tests__/proof.test.ts +226 -0
  35. package/skills/magpie/scripts/__tests__/refresh.test.ts +71 -0
  36. package/skills/magpie/scripts/__tests__/render-annotation.test.ts +34 -0
  37. package/skills/magpie/scripts/__tests__/render-findings.test.ts +75 -0
  38. package/skills/magpie/scripts/__tests__/render-progress.test.ts +9 -0
  39. package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +376 -1
  40. package/skills/magpie/scripts/__tests__/shard-coverage.test.ts +79 -0
  41. package/skills/magpie/scripts/__tests__/skill-lint.test.ts +48 -2
  42. package/skills/magpie/scripts/__tests__/status-cmd.test.ts +20 -8
  43. package/skills/magpie/scripts/__tests__/types.test.ts +46 -0
  44. package/skills/magpie/scripts/cleanup-cmd.ts +88 -0
  45. package/skills/magpie/scripts/critic-cmd.ts +2 -0
  46. package/skills/magpie/scripts/critic.ts +4 -0
  47. package/skills/magpie/scripts/dedupe-cmd.ts +4 -0
  48. package/skills/magpie/scripts/evidence-filter.ts +10 -0
  49. package/skills/magpie/scripts/git-diff.ts +50 -19
  50. package/skills/magpie/scripts/handoff-cmd.ts +42 -3
  51. package/skills/magpie/scripts/history.ts +143 -0
  52. package/skills/magpie/scripts/impact-cmd.ts +202 -0
  53. package/skills/magpie/scripts/impact-render.ts +158 -0
  54. package/skills/magpie/scripts/impact.ts +622 -0
  55. package/skills/magpie/scripts/incremental.ts +44 -5
  56. package/skills/magpie/scripts/labels-cmd.ts +7 -3
  57. package/skills/magpie/scripts/post-cmd.ts +46 -0
  58. package/skills/magpie/scripts/pr-activity.ts +243 -0
  59. package/skills/magpie/scripts/preview-cmd.ts +3 -0
  60. package/skills/magpie/scripts/previous-findings.ts +181 -0
  61. package/skills/magpie/scripts/proof-cmd.ts +226 -0
  62. package/skills/magpie/scripts/proof.ts +150 -0
  63. package/skills/magpie/scripts/refresh.ts +11 -0
  64. package/skills/magpie/scripts/render-annotation.ts +17 -0
  65. package/skills/magpie/scripts/render-cmd.ts +12 -0
  66. package/skills/magpie/scripts/render-findings.ts +37 -1
  67. package/skills/magpie/scripts/render-progress.ts +3 -0
  68. package/skills/magpie/scripts/setup-cmd.ts +142 -11
  69. package/skills/magpie/scripts/shard-coverage.ts +53 -0
  70. package/skills/magpie/scripts/status-cmd.ts +2 -0
  71. package/skills/magpie/scripts/types.ts +39 -0
  72. package/skills/magpie/skill.json +1 -1
  73. package/skills/magpie/templates/styles.css +30 -0
  74. package/skills/sluice/scripts/status.sh +32 -13
  75. 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('<<MERGE_CANDIDATES>>, <<REVIEW_RULES>>, <<WORKTREE>>, <<DIFF_PATH>>')
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 errors when a stale refs/magpie/pr-<n> ref points at an older commit', async () => {
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).toContain('stale')
81
- expect(result.error).toContain(staleSha)
82
- expect(result.error).toContain(head)
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
+ })