@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
@@ -0,0 +1,477 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { appendFile, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+ import { runImpact } from '../impact-cmd.ts'
6
+ import { runRender } from '../render-cmd.ts'
7
+ import { runStatus, type StatusResult } from '../status-cmd.ts'
8
+ import { git } from './helpers/git-fixture.ts'
9
+
10
+ const CLI = new URL('../../bin/magpie.ts', import.meta.url).pathname
11
+
12
+ const BASE_FILES: Record<string, string> = {
13
+ 'src/report.ts': `export function sealReport(id: string) {
14
+ const label = 'sealed'
15
+ const prefix = 'report'
16
+ const suffix = 'done'
17
+ return \`\${prefix}:\${id}:\${label}:\${suffix}\`
18
+ }
19
+ `,
20
+ 'src/close.ts': `export function closeReport(id: string) {
21
+ return sealReport(id)
22
+ }
23
+ `,
24
+ 'src/archive.ts': `export function archiveReport(id: string) {
25
+ const sealed = sealReport(id)
26
+ return sealed
27
+ }
28
+ `,
29
+ 'src/lock-writer.ts': `export function lockReport(id: string) {
30
+ return db.insert(reportLocks).values({ id })
31
+ }
32
+ `,
33
+ 'src/routes.ts': `export const routes = {
34
+ video: '/api/locks/video',
35
+ }
36
+ `,
37
+ 'docs/design.md': `# Design
38
+
39
+ Sealing goes through \`sealReport\` before archive.
40
+ `,
41
+ // A build output calling sealReport: the default path filter drops dist/.
42
+ 'dist/bundle.js': `sealReport(x)
43
+ `,
44
+ 'notes.txt': `draft one
45
+ `,
46
+ }
47
+
48
+ const HEAD_FILES: Record<string, string> = {
49
+ 'src/report.ts': `export function sealReport(id: string) {
50
+ const label = 'sealed'
51
+ const prefix = 'report'
52
+ const suffix = 'done'
53
+ const locks = db.select().from(reportLocks)
54
+ return \`\${prefix}:\${id}:\${label}:\${suffix}:\${locks.length}\`
55
+ }
56
+ `,
57
+ 'src/routes.ts': `export const routes = {
58
+ video: '/api/locks/video',
59
+ report: '/api/locks/report',
60
+ }
61
+ `,
62
+ 'notes.txt': `draft two
63
+ `,
64
+ }
65
+
66
+ let repo: string
67
+ let diff: string
68
+ let runDir: string
69
+ const tempDirs: string[] = []
70
+
71
+ async function writeFiles(root: string, files: Record<string, string>): Promise<void> {
72
+ for (const [path, text] of Object.entries(files)) {
73
+ await mkdir(dirname(join(root, path)), { recursive: true })
74
+ await writeFile(join(root, path), text)
75
+ }
76
+ }
77
+
78
+ async function tempDir(prefix: string): Promise<string> {
79
+ const dir = await mkdtemp(join(tmpdir(), prefix))
80
+ tempDirs.push(dir)
81
+ return dir
82
+ }
83
+
84
+ async function buildRepo(): Promise<string> {
85
+ const root = await tempDir('magpie-impact-repo-')
86
+ await git(root, 'init', '-b', 'main')
87
+ await git(root, 'config', 'user.email', 'test@example.com')
88
+ await git(root, 'config', 'user.name', 'Test')
89
+ await writeFiles(root, BASE_FILES)
90
+ await git(root, 'add', '.')
91
+ await git(root, 'commit', '-m', 'base')
92
+ await writeFiles(root, HEAD_FILES)
93
+ await git(root, 'commit', '-am', 'change')
94
+ return root
95
+ }
96
+
97
+ beforeAll(async () => {
98
+ repo = await buildRepo()
99
+ diff = `${await git(repo, 'diff', 'HEAD~1', 'HEAD')}\n`
100
+ })
101
+
102
+ afterAll(async () => {
103
+ for (const dir of tempDirs) await rm(dir, { recursive: true, force: true })
104
+ })
105
+
106
+ const TWO_SHARDS = {
107
+ budget: 6000,
108
+ maxFiles: 80,
109
+ totalFiles: 3,
110
+ totalLines: 30,
111
+ shards: [
112
+ { id: 1, path: 'shards/shard-1.patch', files: ['src/report.ts'], lines: 20 },
113
+ { id: 2, path: 'shards/shard-2.patch', files: ['notes.txt'], lines: 5 },
114
+ ],
115
+ }
116
+
117
+ async function makeRunDir(root: string, mode: 'pr' | 'local' = 'pr'): Promise<string> {
118
+ const dir = await tempDir('magpie-impact-run-')
119
+ await writeFile(join(dir, 'diff.patch'), diff)
120
+ await writeFile(join(dir, 'target.json'), JSON.stringify({ mode, root }))
121
+ await mkdir(join(dir, 'shards'))
122
+ await writeFile(join(dir, 'shards', 'manifest.json'), JSON.stringify(TWO_SHARDS))
123
+ await writeFile(join(dir, 'log.jsonl'), `${JSON.stringify({ stage: 'setup', status: 'done' })}\n`)
124
+ return dir
125
+ }
126
+
127
+ async function logEntries(dir: string): Promise<Array<Record<string, unknown>>> {
128
+ const text = await readFile(join(dir, 'log.jsonl'), 'utf8')
129
+ return text
130
+ .split('\n')
131
+ .filter(Boolean)
132
+ .map((line) => JSON.parse(line) as Record<string, unknown>)
133
+ }
134
+
135
+ async function impactFiles(dir: string): Promise<string[]> {
136
+ return (await readdir(dir)).filter((name) => name.startsWith('impact')).sort()
137
+ }
138
+
139
+ const REPORT_SHARD_MD = `### Other writers of the same data
140
+ - \`reportLocks\` (diff: src/report.ts:5)
141
+ - src/lock-writer.ts:2 \`return db.insert(reportLocks).values({ id })\`
142
+
143
+ ### References to changed symbols
144
+ - \`sealReport\` (diff: src/report.ts:1)
145
+ - docs/design.md:3 \`\` Sealing goes through \`sealReport\` before archive. \`\`
146
+ - src/archive.ts:2 \`const sealed = sealReport(id)\`
147
+ - src/close.ts:2 \`return sealReport(id)\`
148
+ - src/report.ts:1 \`export function sealReport(id: string) {\`
149
+
150
+ ### Docs naming what changed
151
+ - docs/design.md:3 \`\` Sealing goes through \`sealReport\` before archive. \`\` (names: sealReport)
152
+ `
153
+
154
+ const ROUTES_MD = `### Sibling routes
155
+ - \`/api/locks/report\` (diff: src/routes.ts:3)
156
+ - src/routes.ts:2 \`video: '/api/locks/video',\`
157
+ `
158
+
159
+ describe('runImpact on a sharded PR run', () => {
160
+ beforeEach(async () => {
161
+ runDir = await makeRunDir(repo)
162
+ })
163
+
164
+ test('impact.json lists the callers, the writer, the sibling route and the doc line', async () => {
165
+ expect(await runImpact(runDir)).toBe(0)
166
+ const map = JSON.parse(await readFile(join(runDir, 'impact.json'), 'utf8'))
167
+ expect(map).toEqual({
168
+ version: 1,
169
+ symbols: [
170
+ {
171
+ name: 'sealReport',
172
+ kind: 'changed',
173
+ definedAt: [{ file: 'src/report.ts', line: 1 }],
174
+ shard: 1,
175
+ hits: 4,
176
+ references: [
177
+ {
178
+ file: 'docs/design.md',
179
+ line: 3,
180
+ text: 'Sealing goes through `sealReport` before archive.',
181
+ },
182
+ { file: 'src/archive.ts', line: 2, text: 'const sealed = sealReport(id)' },
183
+ { file: 'src/close.ts', line: 2, text: 'return sealReport(id)' },
184
+ { file: 'src/report.ts', line: 1, text: 'export function sealReport(id: string) {' },
185
+ ],
186
+ },
187
+ ],
188
+ uses: [],
189
+ stores: [
190
+ {
191
+ name: 'reportLocks',
192
+ kind: 'table',
193
+ usedIn: [{ file: 'src/report.ts', line: 5 }],
194
+ shard: 1,
195
+ writers: [
196
+ {
197
+ file: 'src/lock-writer.ts',
198
+ line: 2,
199
+ text: 'return db.insert(reportLocks).values({ id })',
200
+ },
201
+ ],
202
+ },
203
+ ],
204
+ routes: [
205
+ {
206
+ path: '/api/locks/report',
207
+ usedIn: { file: 'src/routes.ts', line: 3 },
208
+ shard: null,
209
+ siblings: [{ file: 'src/routes.ts', line: 2, text: "video: '/api/locks/video'," }],
210
+ },
211
+ ],
212
+ docs: [
213
+ {
214
+ file: 'docs/design.md',
215
+ line: 3,
216
+ text: 'Sealing goes through `sealReport` before archive.',
217
+ mentions: ['sealReport'],
218
+ shards: [1],
219
+ },
220
+ ],
221
+ })
222
+ })
223
+
224
+ test('impact.md renders every entry, the unsharded route included', async () => {
225
+ await runImpact(runDir)
226
+ const [writers, symbols, docs] = REPORT_SHARD_MD.split('\n\n')
227
+ expect(await readFile(join(runDir, 'impact.md'), 'utf8')).toBe(
228
+ `${writers}\n\n${symbols}\n\n${ROUTES_MD}\n${docs}`,
229
+ )
230
+ })
231
+
232
+ test('impact.brief.md caps each entry at three children', async () => {
233
+ await runImpact(runDir)
234
+ expect(await readFile(join(runDir, 'impact.brief.md'), 'utf8')).toBe(
235
+ `### Other writers of the same data
236
+ - \`reportLocks\` (diff: src/report.ts:5)
237
+ - src/lock-writer.ts:2 \`return db.insert(reportLocks).values({ id })\`
238
+
239
+ ### References to changed symbols
240
+ - \`sealReport\` (diff: src/report.ts:1)
241
+ - docs/design.md:3 \`\` Sealing goes through \`sealReport\` before archive. \`\`
242
+ - src/archive.ts:2 \`const sealed = sealReport(id)\`
243
+ - src/close.ts:2 \`return sealReport(id)\`
244
+ - 3 of 4 shown
245
+
246
+ ### Sibling routes
247
+ - \`/api/locks/report\` (diff: src/routes.ts:3)
248
+ - src/routes.ts:2 \`video: '/api/locks/video',\`
249
+
250
+ ### Docs naming what changed
251
+ - docs/design.md:3 \`\` Sealing goes through \`sealReport\` before archive. \`\` (names: sealReport)
252
+ `,
253
+ )
254
+ })
255
+
256
+ test('impact.shard-1.md holds only shard 1 entries', async () => {
257
+ await runImpact(runDir)
258
+ expect(await readFile(join(runDir, 'impact.shard-1.md'), 'utf8')).toBe(REPORT_SHARD_MD)
259
+ })
260
+
261
+ test('a shard with no entries gets no impact files', async () => {
262
+ await runImpact(runDir)
263
+ expect(await impactFiles(runDir)).toEqual([
264
+ 'impact.brief.md',
265
+ 'impact.json',
266
+ 'impact.md',
267
+ 'impact.shard-1.brief.md',
268
+ 'impact.shard-1.md',
269
+ ])
270
+ })
271
+
272
+ test('the done log line carries the counts', async () => {
273
+ await runImpact(runDir)
274
+ const last = (await logEntries(runDir)).at(-1)
275
+ expect(last).toEqual({
276
+ stage: 'impact',
277
+ status: 'done',
278
+ symbols: 1,
279
+ uses: 0,
280
+ stores: 1,
281
+ routes: 1,
282
+ docs: 1,
283
+ ts: expect.any(Number),
284
+ })
285
+ })
286
+
287
+ test('a run leaves no temp-named files, including one an interrupted run left behind', async () => {
288
+ await writeFile(join(runDir, 'impact.json.tmp'), '{"version": 1, "sym')
289
+ expect(await runImpact(runDir)).toBe(0)
290
+ expect(await impactFiles(runDir)).toEqual([
291
+ 'impact.brief.md',
292
+ 'impact.json',
293
+ 'impact.md',
294
+ 'impact.shard-1.brief.md',
295
+ 'impact.shard-1.md',
296
+ ])
297
+ })
298
+
299
+ test('a truncated impact.json from an earlier run is replaced by a readable map', async () => {
300
+ await writeFile(join(runDir, 'impact.json'), '{"version": 1, "sym')
301
+ expect(await runImpact(runDir)).toBe(0)
302
+ const map = JSON.parse(await readFile(join(runDir, 'impact.json'), 'utf8'))
303
+ expect(map.stores.map((s: { name: string }) => s.name)).toEqual(['reportLocks'])
304
+ })
305
+
306
+ test('a second run removes shard files the first left behind', async () => {
307
+ await runImpact(runDir)
308
+ await writeFile(join(runDir, 'impact.shard-3.md'), 'stale\n')
309
+ await writeFile(join(runDir, 'impact.shard-3.brief.md'), 'stale\n')
310
+ await runImpact(runDir)
311
+ expect(await impactFiles(runDir)).toEqual([
312
+ 'impact.brief.md',
313
+ 'impact.json',
314
+ 'impact.md',
315
+ 'impact.shard-1.brief.md',
316
+ 'impact.shard-1.md',
317
+ ])
318
+ })
319
+
320
+ test('magpie shard rewrites the per-shard files for the new split', async () => {
321
+ await runImpact(runDir)
322
+ const proc = Bun.spawn(['bun', CLI, 'shard', runDir, '--budget', '1'], {
323
+ stdout: 'pipe',
324
+ stderr: 'pipe',
325
+ })
326
+ expect(await proc.exited).toBe(0)
327
+ // Budget 1 puts each file in its own shard, in diff order: notes.txt,
328
+ // src/report.ts, src/routes.ts.
329
+ expect(await impactFiles(runDir)).toEqual([
330
+ 'impact.brief.md',
331
+ 'impact.json',
332
+ 'impact.md',
333
+ 'impact.shard-2.brief.md',
334
+ 'impact.shard-2.md',
335
+ 'impact.shard-3.brief.md',
336
+ 'impact.shard-3.md',
337
+ ])
338
+ expect(await readFile(join(runDir, 'impact.shard-2.md'), 'utf8')).toBe(REPORT_SHARD_MD)
339
+ expect(await readFile(join(runDir, 'impact.shard-3.md'), 'utf8')).toBe(ROUTES_MD)
340
+ })
341
+ })
342
+
343
+ describe('magpie shard', () => {
344
+ test('a successful split exits 0 even when the impact re-run cannot start', async () => {
345
+ // No target.json and no log.jsonl: runImpact rejects the dir, the split does not care.
346
+ const dir = await tempDir('magpie-impact-shard-')
347
+ await writeFile(join(dir, 'diff.patch'), diff)
348
+ await writeFile(join(dir, 'impact.json'), '{}')
349
+ const proc = Bun.spawn(['bun', CLI, 'shard', dir], { stdout: 'pipe', stderr: 'pipe' })
350
+ expect(await proc.exited).toBe(0)
351
+ })
352
+ })
353
+
354
+ describe('runImpact path filter config', () => {
355
+ // Only dist/ is admitted, so the one listed reference is the default-excluded build output.
356
+ const DIST_ONLY = JSON.stringify({ include: ['dist/**'] })
357
+
358
+ async function referencesIn(dir: string): Promise<unknown> {
359
+ expect(await runImpact(dir)).toBe(0)
360
+ const map = JSON.parse(await readFile(join(dir, 'impact.json'), 'utf8'))
361
+ return map.symbols[0].references
362
+ }
363
+
364
+ test('an include pattern re-admits a default-excluded path', async () => {
365
+ const root = await buildRepo()
366
+ await writeFile(join(root, '.magpie.json'), DIST_ONLY)
367
+ expect(await referencesIn(await makeRunDir(root, 'local'))).toEqual([
368
+ { file: 'dist/bundle.js', line: 1, text: 'sealReport(x)' },
369
+ ])
370
+ })
371
+
372
+ test('a PR run reads .magpie.json from the main checkout, not the PR worktree', async () => {
373
+ const root = await buildRepo()
374
+ await writeFile(join(root, '.magpie.json'), DIST_ONLY)
375
+ const worktree = join(await tempDir('magpie-impact-wt-'), 'head')
376
+ await git(root, 'worktree', 'add', '--detach', worktree, 'HEAD')
377
+ expect(await referencesIn(await makeRunDir(worktree))).toEqual([
378
+ { file: 'dist/bundle.js', line: 1, text: 'sealReport(x)' },
379
+ ])
380
+ })
381
+ })
382
+
383
+ describe('runImpact symbol lines', () => {
384
+ test('a symbol found only in a hunk header is placed at its definition line', async () => {
385
+ const root = await tempDir('magpie-impact-header-')
386
+ await git(root, 'init', '-b', 'main')
387
+ await git(root, 'config', 'user.email', 'test@example.com')
388
+ await git(root, 'config', 'user.name', 'Test')
389
+ const body = (extra: string) =>
390
+ `export function sealReport(id: string) {\n const a = 1\n const b = 2\n const c = 3\n${extra} return id\n}\n`
391
+ await writeFiles(root, { 'src/report.ts': body('') })
392
+ await git(root, 'add', '.')
393
+ await git(root, 'commit', '-m', 'base')
394
+ await writeFiles(root, { 'src/report.ts': body(' const d = 4\n') })
395
+ await git(root, 'commit', '-am', 'change')
396
+ const dir = await tempDir('magpie-impact-run-')
397
+ await writeFile(join(dir, 'diff.patch'), `${await git(root, 'diff', 'HEAD~1', 'HEAD')}\n`)
398
+ await writeFile(join(dir, 'target.json'), JSON.stringify({ mode: 'pr', root }))
399
+ await writeFile(join(dir, 'log.jsonl'), '')
400
+
401
+ expect(await runImpact(dir)).toBe(0)
402
+ const map = JSON.parse(await readFile(join(dir, 'impact.json'), 'utf8'))
403
+ // Line 1 is `export function sealReport(...)`; line 2 is `const a = 1`.
404
+ expect(map.symbols[0].definedAt).toEqual([{ file: 'src/report.ts', line: 1 }])
405
+ })
406
+ })
407
+
408
+ describe('runImpact failures', () => {
409
+ test('a review root that is not a git repo logs skipped and exits 0', async () => {
410
+ const notRepo = await tempDir('magpie-impact-norepo-')
411
+ const dir = await makeRunDir(notRepo)
412
+ expect(await runImpact(dir)).toBe(0)
413
+ const last = (await logEntries(dir)).at(-1)
414
+ expect(last?.stage).toBe('impact')
415
+ expect(last?.status).toBe('skipped')
416
+ expect(String(last?.error)).toContain('not a git repository')
417
+ expect(await impactFiles(dir)).toEqual([])
418
+ })
419
+
420
+ test('a missing diff.patch logs skipped naming the file', async () => {
421
+ const dir = await makeRunDir(repo)
422
+ await rm(join(dir, 'diff.patch'))
423
+ expect(await runImpact(dir)).toBe(0)
424
+ const last = (await logEntries(dir)).at(-1)
425
+ expect(last?.status).toBe('skipped')
426
+ expect(String(last?.error)).toContain(join(dir, 'diff.patch'))
427
+ })
428
+
429
+ test('a missing run dir is a usage error', async () => {
430
+ const parent = await tempDir('magpie-impact-missing-')
431
+ expect(await runImpact(join(parent, 'no-such-run'))).toBe(2)
432
+ })
433
+
434
+ test('a directory that is not a magpie run is a usage error', async () => {
435
+ expect(await runImpact(await tempDir('magpie-impact-empty-'))).toBe(2)
436
+ })
437
+ })
438
+
439
+ const PROGRESS_PANE = `<section class="progress-pane" data-role="progress-pane">
440
+ <p class="now-doing">Five reviewers reading the diff in parallel.</p>
441
+ <ol class="pipeline" style="--done-count: 2"><li class="step done" data-stage="setup"><span class="dot"></span><span class="name">setup</span><span class="hint">fetch PR and diff</span></li><li class="step done" data-stage="context"><span class="dot"></span><span class="name">context</span><span class="hint">index symbols</span></li><li class="step running" data-stage="specialists"><span class="dot"></span><span class="name">specialists</span><span class="hint">five reviewers in parallel</span></li><li class="step pending" data-stage="dedupe"><span class="dot"></span><span class="name">dedupe</span><span class="hint">merge overlaps</span></li><li class="step pending" data-stage="critic"><span class="dot"></span><span class="name">critic</span><span class="hint">keep the high-signal ones</span></li><li class="step pending" data-stage="peer-review"><span class="dot"></span><span class="name">peer-review</span><span class="hint">independent second opinion</span></li><li class="step pending" data-stage="proof"><span class="dot"></span><span class="name">proof</span><span class="hint">reproduce the serious ones</span></li><li class="step pending" data-stage="report"><span class="dot"></span><span class="name">report</span><span class="hint">render this page</span></li><li class="step pending" data-stage="post"><span class="dot"></span><span class="name">post</span><span class="hint">comment on the PR</span></li></ol>
442
+ <p class="specialists-line"><span class="count">0</span> findings <span class="sep">·</span> <span class="part">security <span class="count">0</span></span><span class="sep">·</span><span class="part">bugs <span class="count">0</span></span><span class="sep">·</span><span class="part">performance <span class="count">0</span></span><span class="sep">·</span><span class="part">code-smells <span class="count">0</span></span><span class="sep">·</span><span class="part">architecture <span class="count">0</span></span></p>
443
+ </section>`
444
+
445
+ test('impact log entries change neither status nor the progress page', async () => {
446
+ const dir = await tempDir('magpie-impact-status-')
447
+ await writeFile(join(dir, 'target.json'), JSON.stringify({ mode: 'pr', root: dir }))
448
+ await writeFile(
449
+ join(dir, 'log.jsonl'),
450
+ [
451
+ { stage: 'setup', status: 'done' },
452
+ { stage: 'context', status: 'done' },
453
+ { stage: 'specialists', status: 'running' },
454
+ ]
455
+ .map((e) => `${JSON.stringify(e)}\n`)
456
+ .join(''),
457
+ )
458
+ const expectedStatus: StatusResult = {
459
+ lastCompleted: 'context',
460
+ next: 'specialists',
461
+ error: null,
462
+ }
463
+
464
+ expect(await runStatus(dir)).toEqual(expectedStatus)
465
+ await runRender(dir, 'progress')
466
+ const before = await readFile(join(dir, 'screen', 'progress.html'), 'utf8')
467
+ expect(before).toContain(PROGRESS_PANE)
468
+
469
+ await appendFile(
470
+ join(dir, 'log.jsonl'),
471
+ `${JSON.stringify({ stage: 'impact', status: 'done', symbols: 1, uses: 0, stores: 1, routes: 1, docs: 1, ts: 1 })}\n${JSON.stringify({ stage: 'impact', status: 'skipped', error: 'git grep exited 128', ts: 2 })}\n`,
472
+ )
473
+
474
+ expect(await runStatus(dir)).toEqual(expectedStatus)
475
+ await runRender(dir, 'progress')
476
+ expect(await readFile(join(dir, 'screen', 'progress-v2.html'), 'utf8')).toBe(before)
477
+ })