@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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
describePreviousPosted,
|
|
7
|
+
type PreviousFinding,
|
|
8
|
+
readPreviousFindings,
|
|
9
|
+
} from '../previous-findings.ts'
|
|
10
|
+
|
|
11
|
+
let root: string
|
|
12
|
+
let prevRun: string
|
|
13
|
+
let worktree: string
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
root = await mkdtemp(join(tmpdir(), 'magpie-previous-'))
|
|
17
|
+
prevRun = join(root, 'pr-7-100')
|
|
18
|
+
worktree = join(root, 'worktree')
|
|
19
|
+
await mkdir(join(prevRun, 'state'), { recursive: true })
|
|
20
|
+
await mkdir(join(worktree, 'src'), { recursive: true })
|
|
21
|
+
await writeFile(
|
|
22
|
+
join(worktree, 'src', 'upload.ts'),
|
|
23
|
+
'export function upload() {\n try {\n send()\n } catch {}\n}\n',
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await rm(root, { recursive: true, force: true })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function finding(
|
|
32
|
+
id: string,
|
|
33
|
+
file: string,
|
|
34
|
+
line: number | null,
|
|
35
|
+
evidence: string | undefined,
|
|
36
|
+
): Record<string, unknown> {
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
file,
|
|
40
|
+
line,
|
|
41
|
+
...(evidence !== undefined ? { evidence } : {}),
|
|
42
|
+
severity: 'high',
|
|
43
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'should-fix' },
|
|
44
|
+
title: `title of ${id}`,
|
|
45
|
+
description: 'Observation: x',
|
|
46
|
+
domain: 'bugs',
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function seedFinal(findings: Record<string, unknown>[]): Promise<void> {
|
|
51
|
+
await writeFile(join(prevRun, 'findings.final.json'), JSON.stringify(findings))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('a previous run with no findings.final.json yields null', async () => {
|
|
55
|
+
expect(await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('an aborted previous run still passes on the dismissals it carried', async () => {
|
|
59
|
+
const carried: PreviousFinding = {
|
|
60
|
+
id: 'perf-3',
|
|
61
|
+
fromRunId: 'pr-7-50',
|
|
62
|
+
file: 'src/cache.ts',
|
|
63
|
+
line: 9,
|
|
64
|
+
title: 'Cache never evicts',
|
|
65
|
+
domain: 'performance',
|
|
66
|
+
label: 'dismissed',
|
|
67
|
+
reason: 'not-worth-it',
|
|
68
|
+
}
|
|
69
|
+
await writeFile(
|
|
70
|
+
join(prevRun, 'previous-findings.json'),
|
|
71
|
+
JSON.stringify({ previousRunId: 'pr-7-50', findings: [carried] }),
|
|
72
|
+
)
|
|
73
|
+
const result = await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })
|
|
74
|
+
expect(result).toEqual({ previousRunId: 'pr-7-100', findings: [carried] })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('a posted finding whose evidence is still in the file reads as anchor unchanged', async () => {
|
|
78
|
+
await seedFinal([finding('bugs-1', 'src/upload.ts', 4, ' } catch {}')])
|
|
79
|
+
await writeFile(join(prevRun, 'post-status.json'), JSON.stringify({ 'bugs-1': 'posted' }))
|
|
80
|
+
const result = await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })
|
|
81
|
+
expect(result?.findings).toEqual([
|
|
82
|
+
{
|
|
83
|
+
id: 'bugs-1',
|
|
84
|
+
fromRunId: 'pr-7-100',
|
|
85
|
+
file: 'src/upload.ts',
|
|
86
|
+
line: 4,
|
|
87
|
+
title: 'title of bugs-1',
|
|
88
|
+
domain: 'bugs',
|
|
89
|
+
label: 'posted',
|
|
90
|
+
anchor: 'unchanged',
|
|
91
|
+
},
|
|
92
|
+
])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('a posted finding whose evidence moved within the file still reads as unchanged', async () => {
|
|
96
|
+
await seedFinal([finding('bugs-1', 'src/upload.ts', 40, ' send()')])
|
|
97
|
+
await writeFile(join(prevRun, 'post-status.json'), JSON.stringify({ 'bugs-1': 'posted' }))
|
|
98
|
+
const result = await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })
|
|
99
|
+
expect(result?.findings[0]).toMatchObject({ anchor: 'unchanged' })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('a posted finding whose evidence is gone reads as anchor changed', async () => {
|
|
103
|
+
await seedFinal([finding('bugs-1', 'src/upload.ts', 4, ' } catch (e) { log(e) }')])
|
|
104
|
+
await writeFile(join(prevRun, 'post-status.json'), JSON.stringify({ 'bugs-1': 'posted' }))
|
|
105
|
+
const result = await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })
|
|
106
|
+
expect(result?.findings[0]).toMatchObject({ anchor: 'changed' })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('a posted finding on a file that no longer exists reads as anchor changed', async () => {
|
|
110
|
+
await seedFinal([finding('bugs-1', 'src/gone.ts', 4, 'x')])
|
|
111
|
+
await writeFile(join(prevRun, 'post-status.json'), JSON.stringify({ 'bugs-1': 'posted' }))
|
|
112
|
+
const result = await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })
|
|
113
|
+
expect(result?.findings[0]).toMatchObject({ anchor: 'changed' })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('a posted finding with no line reads as unanchored', async () => {
|
|
117
|
+
await seedFinal([finding('tests-1', 'src/upload.ts', null, undefined)])
|
|
118
|
+
await writeFile(join(prevRun, 'post-status.json'), JSON.stringify({ 'tests-1': 'posted' }))
|
|
119
|
+
const result = await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })
|
|
120
|
+
expect(result?.findings[0]).toMatchObject({ anchor: 'unanchored' })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('a dismissed finding carries its reason, and ignored findings are left out', async () => {
|
|
124
|
+
await seedFinal([
|
|
125
|
+
finding('bugs-1', 'src/upload.ts', 4, ' } catch {}'),
|
|
126
|
+
finding('bugs-2', 'src/upload.ts', 3, ' send()'),
|
|
127
|
+
])
|
|
128
|
+
await writeFile(
|
|
129
|
+
join(prevRun, 'state', 'events'),
|
|
130
|
+
`${JSON.stringify({ type: 'dismiss', findingId: 'bugs-1', reason: 'style', timestamp: 1 })}\n`,
|
|
131
|
+
)
|
|
132
|
+
const result = await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })
|
|
133
|
+
expect(result?.findings).toEqual([
|
|
134
|
+
{
|
|
135
|
+
id: 'bugs-1',
|
|
136
|
+
fromRunId: 'pr-7-100',
|
|
137
|
+
file: 'src/upload.ts',
|
|
138
|
+
line: 4,
|
|
139
|
+
title: 'title of bugs-1',
|
|
140
|
+
domain: 'bugs',
|
|
141
|
+
label: 'dismissed',
|
|
142
|
+
reason: 'style',
|
|
143
|
+
},
|
|
144
|
+
])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('labels.json on an archived run is read in place of re-folding the events', async () => {
|
|
148
|
+
await seedFinal([finding('bugs-1', 'src/upload.ts', 4, ' } catch {}')])
|
|
149
|
+
await writeFile(
|
|
150
|
+
join(prevRun, 'labels.json'),
|
|
151
|
+
JSON.stringify([{ id: 'bugs-1', label: 'dismissed', reason: 'wrong' }]),
|
|
152
|
+
)
|
|
153
|
+
const result = await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })
|
|
154
|
+
expect(result?.findings[0]).toMatchObject({ label: 'dismissed', reason: 'wrong' })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('dismissals the previous run carried from earlier runs are carried again', async () => {
|
|
158
|
+
await seedFinal([])
|
|
159
|
+
await writeFile(
|
|
160
|
+
join(prevRun, 'previous-findings.json'),
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
previousRunId: 'pr-7-50',
|
|
163
|
+
findings: [
|
|
164
|
+
{
|
|
165
|
+
id: 'perf-3',
|
|
166
|
+
fromRunId: 'pr-7-50',
|
|
167
|
+
file: 'src/cache.ts',
|
|
168
|
+
line: 9,
|
|
169
|
+
title: 'Cache never evicts',
|
|
170
|
+
domain: 'performance',
|
|
171
|
+
label: 'dismissed',
|
|
172
|
+
reason: 'not-worth-it',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'bugs-9',
|
|
176
|
+
fromRunId: 'pr-7-50',
|
|
177
|
+
file: 'src/a.ts',
|
|
178
|
+
line: 1,
|
|
179
|
+
title: 'posted long ago',
|
|
180
|
+
domain: 'bugs',
|
|
181
|
+
label: 'posted',
|
|
182
|
+
anchor: 'unchanged',
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
const result = await readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree })
|
|
188
|
+
expect(result).toEqual({
|
|
189
|
+
previousRunId: 'pr-7-100',
|
|
190
|
+
findings: [
|
|
191
|
+
{
|
|
192
|
+
id: 'perf-3',
|
|
193
|
+
fromRunId: 'pr-7-50',
|
|
194
|
+
file: 'src/cache.ts',
|
|
195
|
+
line: 9,
|
|
196
|
+
title: 'Cache never evicts',
|
|
197
|
+
domain: 'performance',
|
|
198
|
+
label: 'dismissed',
|
|
199
|
+
reason: 'not-worth-it',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
function posted(id: string, anchor: 'unchanged' | 'changed' | 'unanchored'): PreviousFinding {
|
|
206
|
+
return {
|
|
207
|
+
id,
|
|
208
|
+
fromRunId: 'pr-7-100',
|
|
209
|
+
file: 'src/a.ts',
|
|
210
|
+
line: 1,
|
|
211
|
+
title: id,
|
|
212
|
+
domain: 'bugs',
|
|
213
|
+
label: 'posted',
|
|
214
|
+
anchor,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
test('the previous-run line counts posted findings by whether their code changed', () => {
|
|
219
|
+
const line = describePreviousPosted({
|
|
220
|
+
previousRunId: 'pr-7-100',
|
|
221
|
+
findings: [posted('a', 'changed'), posted('b', 'changed'), posted('c', 'unchanged')],
|
|
222
|
+
})
|
|
223
|
+
expect(line).toBe('Last review: 3 posted · 2 on changed code · 1 on unchanged code')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('the previous-run line names unanchored posts only when there are some', () => {
|
|
227
|
+
const line = describePreviousPosted({
|
|
228
|
+
previousRunId: 'pr-7-100',
|
|
229
|
+
findings: [posted('a', 'unanchored'), posted('b', 'unchanged')],
|
|
230
|
+
})
|
|
231
|
+
expect(line).toBe(
|
|
232
|
+
'Last review: 2 posted · 0 on changed code · 1 on unchanged code · 1 not anchored to a line',
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('the previous-run line is null when nothing was posted last review', () => {
|
|
237
|
+
const line = describePreviousPosted({
|
|
238
|
+
previousRunId: 'pr-7-100',
|
|
239
|
+
findings: [
|
|
240
|
+
{
|
|
241
|
+
id: 'x',
|
|
242
|
+
fromRunId: 'pr-7-100',
|
|
243
|
+
file: 'src/a.ts',
|
|
244
|
+
line: 1,
|
|
245
|
+
title: 'x',
|
|
246
|
+
domain: 'bugs',
|
|
247
|
+
label: 'dismissed',
|
|
248
|
+
reason: 'wrong',
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
})
|
|
252
|
+
expect(line).toBeNull()
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('a carried dismissal with no file or title fails, naming the file', async () => {
|
|
256
|
+
await seedFinal([])
|
|
257
|
+
await writeFile(
|
|
258
|
+
join(prevRun, 'previous-findings.json'),
|
|
259
|
+
JSON.stringify({
|
|
260
|
+
previousRunId: 'pr-7-50',
|
|
261
|
+
findings: [{ id: 'perf-3', fromRunId: 'pr-7-50', label: 'dismissed', reason: 'wrong' }],
|
|
262
|
+
}),
|
|
263
|
+
)
|
|
264
|
+
await expect(
|
|
265
|
+
readPreviousFindings({ runDir: prevRun, runId: 'pr-7-100', worktree }),
|
|
266
|
+
).rejects.toThrow('previous-findings.json')
|
|
267
|
+
})
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test'
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { readProofDropped, runProofApply, runProofPrompt } from '../proof-cmd.ts'
|
|
6
|
+
import type { ReviewFinding, Risk } from '../types.ts'
|
|
7
|
+
|
|
8
|
+
let runDir: string
|
|
9
|
+
let repo: string
|
|
10
|
+
let stdout: string
|
|
11
|
+
let stderr: string
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
runDir = await mkdtemp(join(tmpdir(), 'magpie-proof-'))
|
|
15
|
+
await writeFile(join(runDir, 'log.jsonl'), '')
|
|
16
|
+
repo = await mkdtemp(join(tmpdir(), 'magpie-proof-repo-'))
|
|
17
|
+
const init = Bun.spawn(['git', 'init', '-q', repo], { stdout: 'pipe', stderr: 'pipe' })
|
|
18
|
+
if ((await init.exited) !== 0) throw new Error(await new Response(init.stderr).text())
|
|
19
|
+
stdout = ''
|
|
20
|
+
stderr = ''
|
|
21
|
+
spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => {
|
|
22
|
+
stdout += String(chunk)
|
|
23
|
+
return true
|
|
24
|
+
})
|
|
25
|
+
spyOn(process.stderr, 'write').mockImplementation((chunk: string | Uint8Array) => {
|
|
26
|
+
stderr += String(chunk)
|
|
27
|
+
return true
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
mock.restore()
|
|
33
|
+
await rm(runDir, { recursive: true, force: true })
|
|
34
|
+
await rm(repo, { recursive: true, force: true })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const HIGH: Risk = {
|
|
38
|
+
impact: 'high',
|
|
39
|
+
likelihood: 'possible',
|
|
40
|
+
confidence: 'high',
|
|
41
|
+
action: 'must-fix',
|
|
42
|
+
}
|
|
43
|
+
const LOW: Risk = { impact: 'low', likelihood: 'possible', confidence: 'high', action: 'optional' }
|
|
44
|
+
|
|
45
|
+
function finding(id: string, risk: Risk): ReviewFinding {
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
file: 'src/a.ts',
|
|
49
|
+
line: 10,
|
|
50
|
+
severity: risk.impact === 'high' ? 'high' : 'low',
|
|
51
|
+
risk,
|
|
52
|
+
title: `title ${id}`,
|
|
53
|
+
description: `description ${id}`,
|
|
54
|
+
domain: 'bugs',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const REPRO = {
|
|
59
|
+
path: 'magpie-proof-a.test.ts',
|
|
60
|
+
command: 'bun test magpie-proof-a.test.ts',
|
|
61
|
+
source: 'test',
|
|
62
|
+
output: 'fail',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const TEMPLATE_DOC = [
|
|
66
|
+
'# Proof',
|
|
67
|
+
'',
|
|
68
|
+
'````magpie-proof',
|
|
69
|
+
'Finding: <<FINDING>>',
|
|
70
|
+
'Worktree: <<WORKTREE>>',
|
|
71
|
+
'Diff: <<DIFF_PATH>>',
|
|
72
|
+
'Write to: <<OUTPUT_PATH>>',
|
|
73
|
+
'````',
|
|
74
|
+
'',
|
|
75
|
+
].join('\n')
|
|
76
|
+
|
|
77
|
+
async function seedRun(findings: ReviewFinding[]): Promise<string> {
|
|
78
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify(findings))
|
|
79
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'local', root: repo }))
|
|
80
|
+
const templatePath = join(runDir, 'proof.md')
|
|
81
|
+
await writeFile(templatePath, TEMPLATE_DOC)
|
|
82
|
+
return templatePath
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function readJson(name: string): Promise<unknown> {
|
|
86
|
+
return JSON.parse(await readFile(join(runDir, name), 'utf8'))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test('proof-prompt writes a prompt for each selected finding and prints its paths', async () => {
|
|
90
|
+
const templatePath = await seedRun([finding('a', HIGH), finding('b', LOW)])
|
|
91
|
+
|
|
92
|
+
const exit = await runProofPrompt(runDir, { templatePath })
|
|
93
|
+
|
|
94
|
+
expect(exit).toBe(0)
|
|
95
|
+
const promptPath = join(runDir, 'proof-prompt-a.md')
|
|
96
|
+
expect(stdout).toBe(`${promptPath}\t${join(runDir, 'proof-a.json')}\n`)
|
|
97
|
+
const prompt = await readFile(promptPath, 'utf8')
|
|
98
|
+
expect(prompt).toContain(`Worktree: ${repo}`)
|
|
99
|
+
expect(prompt).toContain('"id": "a"')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('proof-prompt prints nothing when no finding qualifies', async () => {
|
|
103
|
+
const templatePath = await seedRun([finding('b', LOW)])
|
|
104
|
+
expect(await runProofPrompt(runDir, { templatePath })).toBe(0)
|
|
105
|
+
expect(stdout).toBe('')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('proof-apply folds results into the final findings and records the disproved ones', async () => {
|
|
109
|
+
const templatePath = await seedRun([finding('a', HIGH), finding('c', HIGH), finding('b', LOW)])
|
|
110
|
+
await runProofPrompt(runDir, { templatePath })
|
|
111
|
+
await writeFile(
|
|
112
|
+
join(runDir, 'proof-a.json'),
|
|
113
|
+
JSON.stringify({ id: 'a', outcome: 'reproduced', reason: 'fails', repro: REPRO }),
|
|
114
|
+
)
|
|
115
|
+
await writeFile(
|
|
116
|
+
join(runDir, 'proof-c.json'),
|
|
117
|
+
JSON.stringify({ id: 'c', outcome: 'disproved', reason: 'guarded', repro: REPRO }),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const exit = await runProofApply(runDir)
|
|
121
|
+
|
|
122
|
+
expect(stderr).toBe('')
|
|
123
|
+
expect(exit).toBe(0)
|
|
124
|
+
const final = (await readJson('findings.final.json')) as ReviewFinding[]
|
|
125
|
+
expect(final.map((f) => [f.id, f.proof?.status ?? null])).toEqual([
|
|
126
|
+
['a', 'reproduced'],
|
|
127
|
+
['b', null],
|
|
128
|
+
])
|
|
129
|
+
expect(await readJson('proof-dropped.json')).toEqual([
|
|
130
|
+
{ id: 'c', file: 'src/a.ts', line: 10, title: 'title c', reason: 'guarded', repro: REPRO },
|
|
131
|
+
])
|
|
132
|
+
const log = (await readFile(join(runDir, 'log.jsonl'), 'utf8')).trim().split('\n')
|
|
133
|
+
expect(JSON.parse(log.at(-1) as string)).toMatchObject({
|
|
134
|
+
stage: 'proof',
|
|
135
|
+
status: 'done',
|
|
136
|
+
reproduced: 1,
|
|
137
|
+
notProven: 0,
|
|
138
|
+
disproved: 1,
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('proof-apply run twice gives the same final findings', async () => {
|
|
143
|
+
const templatePath = await seedRun([finding('a', HIGH), finding('c', HIGH)])
|
|
144
|
+
await runProofPrompt(runDir, { templatePath })
|
|
145
|
+
await writeFile(
|
|
146
|
+
join(runDir, 'proof-a.json'),
|
|
147
|
+
JSON.stringify({ id: 'a', outcome: 'not-proven', reason: 'no runner' }),
|
|
148
|
+
)
|
|
149
|
+
await writeFile(
|
|
150
|
+
join(runDir, 'proof-c.json'),
|
|
151
|
+
JSON.stringify({ id: 'c', outcome: 'disproved', reason: 'guarded', repro: REPRO }),
|
|
152
|
+
)
|
|
153
|
+
await runProofApply(runDir)
|
|
154
|
+
const first = await readJson('findings.final.json')
|
|
155
|
+
|
|
156
|
+
expect(await runProofApply(runDir)).toBe(0)
|
|
157
|
+
expect(await readJson('findings.final.json')).toEqual(first)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('proof-prompt run again after apply still selects from the findings before proof', async () => {
|
|
161
|
+
const templatePath = await seedRun([finding('a', HIGH)])
|
|
162
|
+
await runProofPrompt(runDir, { templatePath })
|
|
163
|
+
await writeFile(
|
|
164
|
+
join(runDir, 'proof-a.json'),
|
|
165
|
+
JSON.stringify({ id: 'a', outcome: 'disproved', reason: 'guarded', repro: REPRO }),
|
|
166
|
+
)
|
|
167
|
+
await runProofApply(runDir)
|
|
168
|
+
stdout = ''
|
|
169
|
+
|
|
170
|
+
await runProofPrompt(runDir, { templatePath })
|
|
171
|
+
|
|
172
|
+
expect(stdout).toBe(`${join(runDir, 'proof-prompt-a.md')}\t${join(runDir, 'proof-a.json')}\n`)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('proof-apply fails naming each selected finding whose result file is missing', async () => {
|
|
176
|
+
const templatePath = await seedRun([finding('a', HIGH), finding('c', HIGH)])
|
|
177
|
+
await runProofPrompt(runDir, { templatePath })
|
|
178
|
+
await writeFile(
|
|
179
|
+
join(runDir, 'proof-a.json'),
|
|
180
|
+
JSON.stringify({ id: 'a', outcome: 'not-proven', reason: 'no runner' }),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
expect(await runProofApply(runDir)).toBe(1)
|
|
184
|
+
expect(stderr).toBe(`proof-apply: no proof result: ${join(runDir, 'proof-c.json')}\n`)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('proof-apply fails on a result file whose id is not its finding', async () => {
|
|
188
|
+
const templatePath = await seedRun([finding('a', HIGH)])
|
|
189
|
+
await runProofPrompt(runDir, { templatePath })
|
|
190
|
+
await writeFile(
|
|
191
|
+
join(runDir, 'proof-a.json'),
|
|
192
|
+
JSON.stringify({ id: 'other', outcome: 'not-proven', reason: 'r' }),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
expect(await runProofApply(runDir)).toBe(1)
|
|
196
|
+
expect(stderr).toBe(`proof-apply: ${join(runDir, 'proof-a.json')}: id is other, expected a\n`)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('proof-apply before proof-prompt fails', async () => {
|
|
200
|
+
await seedRun([finding('a', HIGH)])
|
|
201
|
+
expect(await runProofApply(runDir)).toBe(1)
|
|
202
|
+
expect(stderr).toContain('findings.unproven.json')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('proof-apply fails naming a repro a prover left in the worktree', async () => {
|
|
206
|
+
const templatePath = await seedRun([finding('a', HIGH)])
|
|
207
|
+
await runProofPrompt(runDir, { templatePath })
|
|
208
|
+
await writeFile(
|
|
209
|
+
join(runDir, 'proof-a.json'),
|
|
210
|
+
JSON.stringify({ id: 'a', outcome: 'not-proven', reason: 'no runner' }),
|
|
211
|
+
)
|
|
212
|
+
await mkdir(join(repo, 'src'))
|
|
213
|
+
await writeFile(join(repo, 'src', 'magpie-proof-a.test.ts'), 'test')
|
|
214
|
+
|
|
215
|
+
expect(await runProofApply(runDir)).toBe(1)
|
|
216
|
+
expect(stderr).toBe(
|
|
217
|
+
`proof-apply: provers left files in ${repo}; delete them: src/magpie-proof-a.test.ts\n`,
|
|
218
|
+
)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('proof-apply rewrites the worktree path in a repro to a relative one', async () => {
|
|
222
|
+
const templatePath = await seedRun([finding('a', HIGH)])
|
|
223
|
+
await runProofPrompt(runDir, { templatePath })
|
|
224
|
+
await writeFile(
|
|
225
|
+
join(runDir, 'proof-a.json'),
|
|
226
|
+
JSON.stringify({
|
|
227
|
+
id: 'a',
|
|
228
|
+
outcome: 'reproduced',
|
|
229
|
+
reason: 'fails',
|
|
230
|
+
repro: {
|
|
231
|
+
path: 'src/magpie-proof-a.test.ts',
|
|
232
|
+
command: `cd ${repo} && bun test src/magpie-proof-a.test.ts`,
|
|
233
|
+
source: 'test',
|
|
234
|
+
output: `at ${repo}/src/a.ts:10`,
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
expect(await runProofApply(runDir)).toBe(0)
|
|
240
|
+
const final = (await readJson('findings.final.json')) as ReviewFinding[]
|
|
241
|
+
expect(final[0]?.proof).toEqual({
|
|
242
|
+
status: 'reproduced',
|
|
243
|
+
reason: 'fails',
|
|
244
|
+
repro: {
|
|
245
|
+
path: 'src/magpie-proof-a.test.ts',
|
|
246
|
+
command: 'cd . && bun test src/magpie-proof-a.test.ts',
|
|
247
|
+
source: 'test',
|
|
248
|
+
output: 'at ./src/a.ts:10',
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('a run with no proof-dropped.json has no disproved findings', async () => {
|
|
254
|
+
expect(await readProofDropped(runDir)).toEqual([])
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('readProofDropped fails on a file that does not parse, naming it', async () => {
|
|
258
|
+
await writeFile(join(runDir, 'proof-dropped.json'), '{nope')
|
|
259
|
+
await expect(readProofDropped(runDir)).rejects.toThrow(join(runDir, 'proof-dropped.json'))
|
|
260
|
+
})
|