@iceinvein/agent-skills 0.23.8 → 0.23.12
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 +3 -3
- package/skills/magpie/README.md +1 -1
- package/skills/magpie/SKILL.md +22 -21
- package/skills/magpie/bin/magpie.ts +63 -2
- package/skills/magpie/evals/README.md +22 -8
- package/skills/magpie/evals/bare-invocation-goes-local/case.yaml +4 -0
- package/skills/magpie/evals/bare-invocation-goes-local/fixture.sh +159 -0
- package/skills/magpie/evals/bare-invocation-goes-local/graders/branch-was-reachable.md +5 -0
- package/skills/magpie/evals/bare-invocation-goes-local/graders/never-went-to-a-pr.md +7 -0
- package/skills/magpie/evals/bare-invocation-goes-local/graders/setup-ran-local.md +6 -0
- package/skills/magpie/evals/bare-invocation-goes-local/prompt.md +11 -0
- package/skills/magpie/evals/baselines/claude-opus-5-5.json +27 -3
- 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/evals/rules.json +8 -0
- 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 +99 -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 +204 -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 +219 -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 +47 -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 +137 -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/SKILL.md +13 -0
- package/skills/sluice/evals/README.md +5 -2
- package/skills/sluice/evals/deep-plan-checks-the-intent/case.yaml +4 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/fixture.sh +124 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/graders/announces-deep-channel.md +12 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/graders/checker-dispatched.md +12 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/graders/no-source-edit.md +11 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/graders/no-source-write.md +11 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/graders/signoff-reports-the-check.md +16 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/graders/spec-has-an-intent-map.md +19 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/graders/stopped-for-signoff.md +8 -0
- package/skills/sluice/evals/deep-plan-checks-the-intent/prompt.md +20 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/case.yaml +4 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/fixture.sh +178 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/graders/announces-deep-channel.md +12 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/graders/every-decision-built.md +15 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/graders/no-source-edit.md +11 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/graders/no-source-write.md +11 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/graders/plan-checker-dispatched.md +11 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/graders/plan-cites-the-spec.md +7 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/graders/stops-at-preflight.md +14 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/graders/validate-ran.md +9 -0
- package/skills/sluice/evals/deep-plan-checks-the-plan/prompt.md +12 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/case.yaml +4 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/fixture.sh +125 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/graders/announces-deep-channel.md +12 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/graders/context-cites-the-prior-spec.md +6 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/graders/design-follows-the-prior-spec.md +21 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/graders/design-written-to-docs.md +5 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/graders/no-source-edit.md +11 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/graders/no-source-write.md +11 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/deep-plan-reads-the-prior-spec/prompt.md +17 -0
- package/skills/sluice/evals/rules.json +24 -0
- package/skills/sluice/references/deep-channel.md +145 -8
- package/skills/sluice/scripts/plan.sh +115 -4
- package/skills/sluice/scripts/status.sh +32 -13
- package/skills/sluice/skill.json +2 -2
|
@@ -138,7 +138,8 @@ Re-runs stages 3 to 6 (context, specialists, dedupe, critic) on the run's `diff.
|
|
|
138
138
|
`results/<ts>-full-<runId>.json` with `recall`, `recallVerdicts`, `judgeModel` and
|
|
139
139
|
`judgeCostUsd` all null, plus `log`, `shardManifest`, `findingsFiles`, `dedupedCount`,
|
|
140
140
|
`criticDropped`, `evidenceDropped` and `thresholdDropped` read from the scratch run dir
|
|
141
|
-
before it is deleted
|
|
141
|
+
before it is deleted, and `impactBytes`, the size of `impact.md`. It is null unless
|
|
142
|
+
`magpie impact` was run on the replay's run dir, which no review stage does now. `shardCoverage` is derived from the manifest and the findings file
|
|
142
143
|
names: a shard counts as reviewed when any specialist wrote a file for it, so it overstates
|
|
143
144
|
a shard only some focuses reviewed. The printed line ends with `, shards <reviewed>/<total>`.
|
|
144
145
|
4. When the run has a recall set (see [Recall](#recall)), judges the new kept findings
|
|
@@ -153,6 +154,78 @@ spend cap; `claude -p` has no `--max-cost-usd` flag. Hitting the cap stops the s
|
|
|
153
154
|
and the replay then fails without scoring, on Claude's error result or on the missing
|
|
154
155
|
`findings.kept.json`.
|
|
155
156
|
|
|
157
|
+
## Specialist probe
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
bun evals/quality/probe-specialist.ts --corpus <runId> --repo <path-to-local-clone> \
|
|
161
|
+
--focus <focus> --file <diff path> --misses <id,id> --arm base|impact --repeats <n> \
|
|
162
|
+
[--max-cost-usd N] [--judge-model M] [--dry-run]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Runs one specialist on one shard, with (`impact`) or without (`base`) the impact map in
|
|
166
|
+
its prompt, and judges its findings against the named misses from the run's recall set.
|
|
167
|
+
It asks a narrower question than a full replay, for about a tenth of the cost.
|
|
168
|
+
|
|
169
|
+
The `impact` arm needs a `references/specialists.md` that carries a `magpie-impact-part`
|
|
170
|
+
block, as it did at commit 731a686 (since reverted). Without that block the probe exits
|
|
171
|
+
before any paid session, naming the missing block.
|
|
172
|
+
|
|
173
|
+
Repeats run one after another, each in its own scratch run dir and worktree:
|
|
174
|
+
|
|
175
|
+
1. Scaffolds the run dir as the full replay does (worktree at the PR head, `pr.json`,
|
|
176
|
+
`diff.patch` and `brief.json` copied, a `setup` done line, then `magpie shard`), and
|
|
177
|
+
picks the shard whose files include `--file`; no such shard exits 1, naming the file.
|
|
178
|
+
2. Impact arm only: `magpie impact`. A skipped impact stage fails the probe, since the arm
|
|
179
|
+
would then be a base arm. The prompt then carries `Outside the diff: <path>` under
|
|
180
|
+
`Diff:` and the `magpie-impact-part` block, with `<top entries>` filled from the
|
|
181
|
+
shard's `impact.shard-<n>.brief.md` and `<the path above>` with the path
|
|
182
|
+
(`impact.md` and `impact.brief.md` on an unsharded run). A shard with no impact file
|
|
183
|
+
exits 1, naming the shard, before any session is paid for.
|
|
184
|
+
3. Builds the prompt from this checkout's `references/specialists.md` the way stage 4
|
|
185
|
+
does: focus block, run header, Output Contract, the excluded-files block when the
|
|
186
|
+
corpus run has `excluded-files.json`, and the brief rendered from `brief.json`.
|
|
187
|
+
4. Runs `claude -p --output-format stream-json --verbose --allowedTools
|
|
188
|
+
Bash,Read,Grep,Glob,Write --max-budget-usd <N>` from the worktree, prompt on stdin.
|
|
189
|
+
5. Writes `results/<ts>-probe-<runId>-<focus>-s<shard>-<arm>-r<k>.json` as soon as the
|
|
190
|
+
session ends, with `costUsd` and `sessionError` (a non-zero exit, an `is_error` result
|
|
191
|
+
such as a spent budget, or no result event; the probe then exits 1 after finishing).
|
|
192
|
+
`transcriptBadLines` counts transcript lines that are not JSON (a killed session cuts
|
|
193
|
+
its last line off); they are skipped, not fatal. The results dir is checked writable
|
|
194
|
+
before the session starts. Each later step fills its fields and rewrites the file; a
|
|
195
|
+
step that throws records `error`, rewrites it, prints one stderr line naming the
|
|
196
|
+
repeat and moves on to the next repeat, and the command exits 1 at the end. A failure
|
|
197
|
+
before the session (setup, worktree, a missing impact file) stops every repeat. In
|
|
198
|
+
order:
|
|
199
|
+
- `findings`, from the focus's findings file for the shard, numbering any finding
|
|
200
|
+
with no id as `<focus>-s<n>-<i>`. A missing file records `findings: 0` and
|
|
201
|
+
`missingFindings` with the reason rather than failing. `readImpact`: the session
|
|
202
|
+
opened the impact file with the Read tool (a Bash `cat` is not counted); null on
|
|
203
|
+
the base arm.
|
|
204
|
+
- `citesOutside`: findings whose description cites a `file:line` the shard's impact
|
|
205
|
+
Markdown lists outside the shard. The base arm runs `magpie impact` after its
|
|
206
|
+
session ends, so both arms count against the same map. It is a lower bound: only an
|
|
207
|
+
exact `path:line` match counts, so a citation by line range or by a nearby line is
|
|
208
|
+
missed.
|
|
209
|
+
- `judged`, `caught`, `verdicts` and `judgeCostUsd` from `judgeRecall`. With no
|
|
210
|
+
findings the judge is not called, `judged` stays false and `notJudged` says why.
|
|
211
|
+
6. Removes the worktree and scratch dir whatever happened. A probe killed before its
|
|
212
|
+
cleanup ran leaves a stale worktree registered in the clone: run
|
|
213
|
+
`git -C <repo> worktree prune` there.
|
|
214
|
+
|
|
215
|
+
`--dry-run` stops after step 3, prints the chosen shard and the assembled prompt, cleans
|
|
216
|
+
up and exits 0. It calls no model and spends nothing.
|
|
217
|
+
|
|
218
|
+
**Reading results:** a phase A tally counts only runs with `judged: true` or with zero
|
|
219
|
+
findings (`findings: 0`); a run with an `error`, or with findings that were never judged,
|
|
220
|
+
is left out and re-run. Findings are judged straight from the specialist, before dedupe's
|
|
221
|
+
evidence filter, on both arms alike, so a finding dedupe would drop can still count as a
|
|
222
|
+
catch.
|
|
223
|
+
|
|
224
|
+
**Cost:** one specialist session per repeat, about $1.1 to $1.3 on the corpus PRs, capped
|
|
225
|
+
by `--max-cost-usd` (default 3, passed as `--max-budget-usd`), plus one judge call when
|
|
226
|
+
there are findings. A session that hits the cap is recorded with its cost and
|
|
227
|
+
`sessionError`, and its findings are still read and judged.
|
|
228
|
+
|
|
156
229
|
## Recall
|
|
157
230
|
|
|
158
231
|
The labels above only score findings magpie raised. The recall set scores the other side:
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import type { ReviewFinding } from '../../../scripts/types.ts'
|
|
6
|
+
import {
|
|
7
|
+
assembleSpecialistPrompt,
|
|
8
|
+
countOutsideCitations,
|
|
9
|
+
EXCLUDED_FILES_BLOCK,
|
|
10
|
+
numberProbeFindings,
|
|
11
|
+
readImpactPart,
|
|
12
|
+
renderBriefPart,
|
|
13
|
+
RUN_HEADER_SHARDED,
|
|
14
|
+
RUN_HEADER_UNSHARDED,
|
|
15
|
+
BRIEF_CLOSING,
|
|
16
|
+
PostSessionError,
|
|
17
|
+
runRepeats,
|
|
18
|
+
sessionOutcome,
|
|
19
|
+
transcriptBadLines,
|
|
20
|
+
transcriptReadPath,
|
|
21
|
+
} from '../probe-specialist.ts'
|
|
22
|
+
|
|
23
|
+
const FENCE = '```'
|
|
24
|
+
|
|
25
|
+
const SPECIALISTS_MD = `# Specialist prompts
|
|
26
|
+
|
|
27
|
+
Prose for the orchestrator.
|
|
28
|
+
|
|
29
|
+
## Output Contract
|
|
30
|
+
|
|
31
|
+
Write findings to <RUN_DIR>/findings/<focus>.json.
|
|
32
|
+
|
|
33
|
+
${FENCE}
|
|
34
|
+
{ "id": string }
|
|
35
|
+
${FENCE}
|
|
36
|
+
|
|
37
|
+
If you have no findings, write [].
|
|
38
|
+
|
|
39
|
+
## Focus blocks
|
|
40
|
+
|
|
41
|
+
### security
|
|
42
|
+
|
|
43
|
+
${FENCE}magpie-specialist-security
|
|
44
|
+
You are a security reviewer.
|
|
45
|
+
|
|
46
|
+
## What to look for
|
|
47
|
+
|
|
48
|
+
Injection.
|
|
49
|
+
${FENCE}
|
|
50
|
+
|
|
51
|
+
### bugs
|
|
52
|
+
|
|
53
|
+
${FENCE}magpie-specialist-bugs
|
|
54
|
+
You are a bug hunter.
|
|
55
|
+
${FENCE}
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
const IMPACT_PART = `
|
|
59
|
+
6. The impact map:
|
|
60
|
+
|
|
61
|
+
${FENCE}magpie-impact-part
|
|
62
|
+
## Outside the diff
|
|
63
|
+
|
|
64
|
+
<top entries>
|
|
65
|
+
|
|
66
|
+
<the path above> has the full list.
|
|
67
|
+
${FENCE}
|
|
68
|
+
`
|
|
69
|
+
|
|
70
|
+
const base = {
|
|
71
|
+
specialistsMd: SPECIALISTS_MD,
|
|
72
|
+
focus: 'security',
|
|
73
|
+
runDir: '/runs/r1',
|
|
74
|
+
worktree: '/runs/r1/worktree',
|
|
75
|
+
target: 'PR #70',
|
|
76
|
+
shard: null,
|
|
77
|
+
brief: null,
|
|
78
|
+
excludedFiles: false,
|
|
79
|
+
impact: null,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const SECURITY_BLOCK = 'You are a security reviewer.\n\n## What to look for\n\nInjection.'
|
|
83
|
+
const CONTRACT =
|
|
84
|
+
'## Output Contract\n\nWrite findings to /runs/r1/findings/security.json.\n\n' +
|
|
85
|
+
'```\n{ "id": string }\n```\n\nIf you have no findings, write [].'
|
|
86
|
+
|
|
87
|
+
test('an unsharded prompt is the focus block, the run header and the output contract', () => {
|
|
88
|
+
expect(assembleSpecialistPrompt(base)).toBe(
|
|
89
|
+
`${SECURITY_BLOCK}\n\n` +
|
|
90
|
+
'You are reviewing PR #70.\nWorking directory: /runs/r1/worktree\nDiff: /runs/r1/diff.patch\n\n' +
|
|
91
|
+
CONTRACT,
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('the output contract names the focus id, not a placeholder the session fills in', () => {
|
|
96
|
+
const prompt = assembleSpecialistPrompt(base)
|
|
97
|
+
expect(prompt).toContain('Write findings to /runs/r1/findings/security.json.')
|
|
98
|
+
expect(prompt).not.toContain('<focus>')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('a sharded prompt names the shard, its patch and the scope rule', () => {
|
|
102
|
+
expect(assembleSpecialistPrompt({ ...base, shard: { n: 2, total: 5 } })).toBe(
|
|
103
|
+
`${SECURITY_BLOCK}\n\n` +
|
|
104
|
+
'You are reviewing PR #70.\nWorking directory: /runs/r1/worktree\nShard: 2 of 5\n' +
|
|
105
|
+
'Diff: /runs/r1/shards/shard-2.patch\n\n' +
|
|
106
|
+
'This shard is your review scope. The other shards belong to other agents working\n' +
|
|
107
|
+
'in parallel: do not review files outside your shard, and do not report findings\n' +
|
|
108
|
+
'anchored to them. Reading any file in the worktree for context is expected and\n' +
|
|
109
|
+
'encouraged.\n\n' +
|
|
110
|
+
CONTRACT,
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('the excluded-files block and the brief follow the contract in that order', () => {
|
|
115
|
+
const prompt = assembleSpecialistPrompt({
|
|
116
|
+
...base,
|
|
117
|
+
excludedFiles: true,
|
|
118
|
+
brief: '## What this PR is for\n\nLocks the namespace.',
|
|
119
|
+
})
|
|
120
|
+
expect(prompt).toBe(
|
|
121
|
+
`${SECURITY_BLOCK}\n\n` +
|
|
122
|
+
'You are reviewing PR #70.\nWorking directory: /runs/r1/worktree\nDiff: /runs/r1/diff.patch\n\n' +
|
|
123
|
+
`${CONTRACT}\n\n` +
|
|
124
|
+
'Files excluded by the path filter are absent from your diff but present in the\n' +
|
|
125
|
+
'worktree, with their full patches in /runs/r1/diff.full.patch and the exclusion\n' +
|
|
126
|
+
'list in /runs/r1/excluded-files.json. When a change in your scope implies a change\n' +
|
|
127
|
+
'in an excluded file (a migration implies a model snapshot, a schema change implies\n' +
|
|
128
|
+
'generated types), open it and cross-check rather than treating it as out of scope.\n\n' +
|
|
129
|
+
'## What this PR is for\n\nLocks the namespace.',
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('an impact prompt puts the outside-diff line under Diff and the impact part last', () => {
|
|
134
|
+
const prompt = assembleSpecialistPrompt({
|
|
135
|
+
...base,
|
|
136
|
+
specialistsMd: SPECIALISTS_MD + IMPACT_PART,
|
|
137
|
+
shard: { n: 2, total: 5 },
|
|
138
|
+
brief: '## What this PR is for\n\nLocks the namespace.',
|
|
139
|
+
impact: {
|
|
140
|
+
path: '/runs/r1/impact.shard-2.md',
|
|
141
|
+
inline: '### Sibling routes\n- `/a` (diff: a.ts:3)',
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
expect(prompt).toBe(
|
|
145
|
+
`${SECURITY_BLOCK}\n\n` +
|
|
146
|
+
'You are reviewing PR #70.\nWorking directory: /runs/r1/worktree\nShard: 2 of 5\n' +
|
|
147
|
+
'Diff: /runs/r1/shards/shard-2.patch\nOutside the diff: /runs/r1/impact.shard-2.md\n\n' +
|
|
148
|
+
'This shard is your review scope. The other shards belong to other agents working\n' +
|
|
149
|
+
'in parallel: do not review files outside your shard, and do not report findings\n' +
|
|
150
|
+
'anchored to them. Reading any file in the worktree for context is expected and\n' +
|
|
151
|
+
'encouraged.\n\n' +
|
|
152
|
+
`${CONTRACT}\n\n` +
|
|
153
|
+
'## What this PR is for\n\nLocks the namespace.\n\n' +
|
|
154
|
+
'## Outside the diff\n\n### Sibling routes\n- `/a` (diff: a.ts:3)\n\n' +
|
|
155
|
+
'/runs/r1/impact.shard-2.md has the full list.',
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('dollar patterns in the inline entries are pasted as written', () => {
|
|
160
|
+
const prompt = assembleSpecialistPrompt({
|
|
161
|
+
...base,
|
|
162
|
+
specialistsMd: SPECIALISTS_MD + IMPACT_PART,
|
|
163
|
+
impact: { path: '/runs/r1/impact.md', inline: " - a.ts:3 `s.replace(x, '$&$1')` `$'` `$$`" },
|
|
164
|
+
})
|
|
165
|
+
expect(prompt).toEndWith(
|
|
166
|
+
"## Outside the diff\n\n - a.ts:3 `s.replace(x, '$&$1')` `$'` `$$`\n\n" +
|
|
167
|
+
'/runs/r1/impact.md has the full list.',
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('dollar patterns in the impact path are pasted as written', () => {
|
|
172
|
+
const prompt = assembleSpecialistPrompt({
|
|
173
|
+
...base,
|
|
174
|
+
specialistsMd: SPECIALISTS_MD + IMPACT_PART,
|
|
175
|
+
impact: { path: "/runs/$&$'/impact.md", inline: 'top' },
|
|
176
|
+
})
|
|
177
|
+
expect(prompt).toEndWith("top\n\n/runs/$&$'/impact.md has the full list.")
|
|
178
|
+
expect(prompt).toContain("Diff: /runs/r1/diff.patch\nOutside the diff: /runs/$&$'/impact.md\n")
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('an unsharded impact prompt adds the outside-diff line as the header’s last line', () => {
|
|
182
|
+
const prompt = assembleSpecialistPrompt({
|
|
183
|
+
...base,
|
|
184
|
+
specialistsMd: SPECIALISTS_MD + IMPACT_PART,
|
|
185
|
+
impact: { path: '/runs/r1/impact.md', inline: 'top' },
|
|
186
|
+
})
|
|
187
|
+
expect(prompt).toContain(
|
|
188
|
+
'Diff: /runs/r1/diff.patch\nOutside the diff: /runs/r1/impact.md\n\n## Output Contract',
|
|
189
|
+
)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('an impact prompt without the impact part in specialists.md is refused, naming the block', () => {
|
|
193
|
+
expect(() =>
|
|
194
|
+
assembleSpecialistPrompt({ ...base, impact: { path: '/runs/r1/impact.md', inline: 'top' } }),
|
|
195
|
+
).toThrow('magpie-impact-part')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('a focus with no block in specialists.md is refused, naming the focus', () => {
|
|
199
|
+
expect(() => assembleSpecialistPrompt({ ...base, focus: 'performance' })).toThrow(
|
|
200
|
+
'magpie-specialist-performance',
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('the live specialists.md carries the run header, excluded-files and brief text verbatim', async () => {
|
|
205
|
+
const live = await readFile(join(import.meta.dir, '../../../references/specialists.md'), 'utf8')
|
|
206
|
+
for (const template of [
|
|
207
|
+
RUN_HEADER_UNSHARDED,
|
|
208
|
+
RUN_HEADER_SHARDED,
|
|
209
|
+
EXCLUDED_FILES_BLOCK,
|
|
210
|
+
BRIEF_CLOSING,
|
|
211
|
+
]) {
|
|
212
|
+
expect(live).toContain(template)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('a full brief renders every heading in the part-5 order with the closing paragraph', () => {
|
|
217
|
+
expect(
|
|
218
|
+
renderBriefPart({
|
|
219
|
+
purpose: 'Lock the UUID namespace.',
|
|
220
|
+
changes: ['Adds a namespace constant', 'Verifies reports'],
|
|
221
|
+
watchItems: ['Old reports'],
|
|
222
|
+
unclear: ['Is the namespace public?'],
|
|
223
|
+
reviewRules: [{ rule: 'No raw SQL', source: 'CONTRIBUTING.md' }],
|
|
224
|
+
}),
|
|
225
|
+
).toBe(
|
|
226
|
+
'## What this PR is for\n\nLock the UUID namespace.\n\n' +
|
|
227
|
+
'What it does:\n- Adds a namespace constant\n- Verifies reports\n\n' +
|
|
228
|
+
'Watch items:\n- Old reports\n\n' +
|
|
229
|
+
'Open questions the scout could not resolve:\n- Is the namespace public?\n\n' +
|
|
230
|
+
'Repository review rules:\n- No raw SQL (CONTRIBUTING.md)\n\n' +
|
|
231
|
+
"This brief is the author's claim as understood by a reader who has not reviewed the\n" +
|
|
232
|
+
'code. It is not ground truth. Where the diff contradicts it, that is a finding in\n' +
|
|
233
|
+
'your domain, not a correction to the brief. A watch item is a pointer, not a verdict:\n' +
|
|
234
|
+
'escalate it into a finding with your own risk fields, or leave it alone. A repository\n' +
|
|
235
|
+
'review rule is a convention the maintainers wrote down: a change that breaks one is a\n' +
|
|
236
|
+
"finding in your domain when the rule bears on it, citing the rule's source.",
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('a brief with no watch items, open questions or rules omits those headings', () => {
|
|
241
|
+
expect(
|
|
242
|
+
renderBriefPart({
|
|
243
|
+
purpose: 'Lock it.',
|
|
244
|
+
changes: ['Adds a constant'],
|
|
245
|
+
watchItems: [],
|
|
246
|
+
unclear: [],
|
|
247
|
+
reviewRules: [],
|
|
248
|
+
}),
|
|
249
|
+
).toBe(
|
|
250
|
+
'## What this PR is for\n\nLock it.\n\nWhat it does:\n- Adds a constant\n\n' +
|
|
251
|
+
"This brief is the author's claim as understood by a reader who has not reviewed the\n" +
|
|
252
|
+
'code. It is not ground truth. Where the diff contradicts it, that is a finding in\n' +
|
|
253
|
+
'your domain, not a correction to the brief. A watch item is a pointer, not a verdict:\n' +
|
|
254
|
+
'escalate it into a finding with your own risk fields, or leave it alone. A repository\n' +
|
|
255
|
+
'review rule is a convention the maintainers wrote down: a change that breaks one is a\n' +
|
|
256
|
+
"finding in your domain when the rule bears on it, citing the rule's source.",
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
function assistantToolUse(name: string, input: Record<string, unknown>): string {
|
|
261
|
+
return JSON.stringify({
|
|
262
|
+
type: 'assistant',
|
|
263
|
+
message: {
|
|
264
|
+
content: [
|
|
265
|
+
{ type: 'text', text: 'looking' },
|
|
266
|
+
{ type: 'tool_use', name, input },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const STREAM_HEAD = '{"type":"system","subtype":"init"}\n'
|
|
273
|
+
|
|
274
|
+
test('a transcript that Reads the impact file counts as reading it', () => {
|
|
275
|
+
const stream = `${STREAM_HEAD}${assistantToolUse('Read', { file_path: '/runs/r1/impact.md' })}\n`
|
|
276
|
+
expect(transcriptReadPath(stream, '/runs/r1/impact.md')).toBe(true)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('a transcript that only cats the impact file through Bash does not count', () => {
|
|
280
|
+
const stream = `${STREAM_HEAD}${assistantToolUse('Bash', { command: 'cat /runs/r1/impact.md' })}\n`
|
|
281
|
+
expect(transcriptReadPath(stream, '/runs/r1/impact.md')).toBe(false)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('a transcript that Reads other files does not count as reading the impact file', () => {
|
|
285
|
+
const stream =
|
|
286
|
+
`${STREAM_HEAD}${assistantToolUse('Read', { file_path: '/runs/r1/impact.shard-2.md' })}\n` +
|
|
287
|
+
`${assistantToolUse('Read', { file_path: '/runs/r1/diff.patch' })}\n`
|
|
288
|
+
expect(transcriptReadPath(stream, '/runs/r1/impact.md')).toBe(false)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const READ_IMPACT = assistantToolUse('Read', { file_path: '/runs/r1/impact.md' })
|
|
292
|
+
|
|
293
|
+
test('a transcript cut off mid-line still shows the Read made before the cut', () => {
|
|
294
|
+
const stream = `${STREAM_HEAD}${READ_IMPACT}\n{"type":"assistant","message":{"con`
|
|
295
|
+
expect(transcriptReadPath(stream, '/runs/r1/impact.md')).toBe(true)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('a transcript cut off mid-line counts the cut line as bad', () => {
|
|
299
|
+
const stream = `${STREAM_HEAD}${READ_IMPACT}\n{"type":"assistant","message":{"con`
|
|
300
|
+
expect(transcriptBadLines(stream)).toBe(1)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('a whole transcript has no bad lines', () => {
|
|
304
|
+
expect(transcriptBadLines(`${STREAM_HEAD}${READ_IMPACT}\n`)).toBe(0)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('a successful session reports its cost and no error', () => {
|
|
308
|
+
const stream = `${STREAM_HEAD}{"type":"result","subtype":"success","is_error":false,"total_cost_usd":1.25}\n`
|
|
309
|
+
expect(sessionOutcome(stream, 0, '')).toEqual({ costUsd: 1.25, error: null })
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('a result event with no cost reports the cost as null', () => {
|
|
313
|
+
expect(
|
|
314
|
+
sessionOutcome(`${STREAM_HEAD}{"type":"result","subtype":"success","is_error":false}\n`, 0, ''),
|
|
315
|
+
).toEqual({ costUsd: null, error: null })
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('a session that ended in error keeps its cost and records the error', () => {
|
|
319
|
+
const stream =
|
|
320
|
+
`${STREAM_HEAD}{"type":"result","subtype":"error_max_budget_usd","is_error":true,` +
|
|
321
|
+
'"total_cost_usd":3.02,"result":"budget spent"}\n'
|
|
322
|
+
expect(sessionOutcome(stream, 1, 'boom')).toEqual({
|
|
323
|
+
costUsd: 3.02,
|
|
324
|
+
error: 'session ended in error (error_max_budget_usd): budget spent; exit 1: boom',
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('a session with no result event records that instead of throwing', () => {
|
|
329
|
+
expect(sessionOutcome(STREAM_HEAD, 0, '')).toEqual({
|
|
330
|
+
costUsd: null,
|
|
331
|
+
error: 'stream-json has no result event',
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('a killed session whose transcript is cut off records no result event and the exit', () => {
|
|
336
|
+
expect(sessionOutcome(`${STREAM_HEAD}{"type":"res`, 137, 'killed')).toEqual({
|
|
337
|
+
costUsd: null,
|
|
338
|
+
error: 'stream-json has no result event; exit 137: killed',
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
function finding(id: string, file: string, description: string): ReviewFinding {
|
|
343
|
+
return {
|
|
344
|
+
id,
|
|
345
|
+
file,
|
|
346
|
+
line: 1,
|
|
347
|
+
severity: 'medium',
|
|
348
|
+
risk: { impact: 'medium', likelihood: 'possible', confidence: 'medium', action: 'should-fix' },
|
|
349
|
+
title: 't',
|
|
350
|
+
description,
|
|
351
|
+
domain: 'security',
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const IMPACT_MD = [
|
|
356
|
+
'### Other writers of the same data',
|
|
357
|
+
'- `reports` (diff: src/api/lock.ts:10)',
|
|
358
|
+
' - src/db/writer.ts:44 `insert into reports`',
|
|
359
|
+
' - src/api/lock.ts:80 `update reports`',
|
|
360
|
+
].join('\n')
|
|
361
|
+
|
|
362
|
+
test('a finding citing a map location outside the shard counts as an outside citation', () => {
|
|
363
|
+
const findings = [
|
|
364
|
+
finding('a', 'src/api/lock.ts', 'Observation: src/db/writer.ts:44 also writes reports.'),
|
|
365
|
+
]
|
|
366
|
+
expect(countOutsideCitations(findings, IMPACT_MD, ['src/api/lock.ts'])).toBe(1)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test('a map location inside the shard, or a location not in the map, is not an outside citation', () => {
|
|
370
|
+
const findings = [
|
|
371
|
+
finding('a', 'src/api/lock.ts', 'Observation: src/api/lock.ts:80 updates reports.'),
|
|
372
|
+
finding('b', 'src/api/lock.ts', 'Observation: src/db/other.ts:12 reads reports.'),
|
|
373
|
+
finding('c', 'src/api/lock.ts', 'Observation: src/db/writer.ts:45 is near the writer.'),
|
|
374
|
+
]
|
|
375
|
+
expect(countOutsideCitations(findings, IMPACT_MD, ['src/api/lock.ts'])).toBe(0)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
test('a finding citing several outside locations counts once', () => {
|
|
379
|
+
const impact = `${IMPACT_MD}\n - src/db/audit.ts:7 \`insert into reports\``
|
|
380
|
+
const findings = [
|
|
381
|
+
finding('a', 'src/api/lock.ts', 'See src/db/writer.ts:44 and src/db/audit.ts:7.'),
|
|
382
|
+
]
|
|
383
|
+
expect(countOutsideCitations(findings, impact, ['src/api/lock.ts'])).toBe(1)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
const RISK = { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'must-fix' }
|
|
387
|
+
|
|
388
|
+
test('findings without an id are numbered by focus and shard from 1', () => {
|
|
389
|
+
const findings = numberProbeFindings(
|
|
390
|
+
[
|
|
391
|
+
{ file: 'a.ts', line: 1, risk: RISK, title: 'x', description: 'd' },
|
|
392
|
+
{ id: 'security-7', file: 'b.ts', line: 2, risk: RISK, title: 'y', description: 'd' },
|
|
393
|
+
{ file: 'c.ts', line: 3, risk: RISK, title: 'z', description: 'd' },
|
|
394
|
+
],
|
|
395
|
+
'security',
|
|
396
|
+
3,
|
|
397
|
+
'findings/security.shard-3.json',
|
|
398
|
+
)
|
|
399
|
+
expect(findings.map((f) => f.id)).toEqual(['security-s3-1', 'security-7', 'security-s3-3'])
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('a findings file that is not an array is rejected, naming it', () => {
|
|
403
|
+
expect(() => numberProbeFindings({}, 'security', 1, 'findings/security.json')).toThrow(
|
|
404
|
+
'findings/security.json',
|
|
405
|
+
)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
async function runDirWith(files: Record<string, string>): Promise<string> {
|
|
409
|
+
const dir = await mkdtemp(join(tmpdir(), 'magpie-probe-impact-'))
|
|
410
|
+
for (const [name, text] of Object.entries(files)) await writeFile(join(dir, name), text)
|
|
411
|
+
return dir
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
test('a shard with no impact file is refused, naming the shard', async () => {
|
|
415
|
+
const dir = await runDirWith({ 'impact.md': 'whole run\n', 'impact.brief.md': 'top\n' })
|
|
416
|
+
await expect(readImpactPart(dir, { n: 2, total: 5 })).rejects.toThrow('shard 2')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
test('a sharded run reads its shard’s impact file and brief', async () => {
|
|
420
|
+
const dir = await runDirWith({
|
|
421
|
+
'impact.shard-2.md': 'full\n',
|
|
422
|
+
'impact.shard-2.brief.md': 'top entries\n',
|
|
423
|
+
})
|
|
424
|
+
expect(await readImpactPart(dir, { n: 2, total: 5 })).toEqual({
|
|
425
|
+
path: join(dir, 'impact.shard-2.md'),
|
|
426
|
+
inline: 'top entries',
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('an unsharded run with no impact.md is refused', async () => {
|
|
431
|
+
const dir = await runDirWith({})
|
|
432
|
+
await expect(readImpactPart(dir, null)).rejects.toThrow('impact.md')
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
const LIVE_MD = join(import.meta.dir, '../../../references/specialists.md')
|
|
436
|
+
|
|
437
|
+
test('a base prompt from the live specialists.md carries the focus block and the whole contract', async () => {
|
|
438
|
+
const prompt = assembleSpecialistPrompt({
|
|
439
|
+
...base,
|
|
440
|
+
specialistsMd: await readFile(LIVE_MD, 'utf8'),
|
|
441
|
+
focus: 'bugs',
|
|
442
|
+
})
|
|
443
|
+
expect(prompt).toStartWith(
|
|
444
|
+
'You are a senior software engineer specialized in finding bugs through code review.\n',
|
|
445
|
+
)
|
|
446
|
+
expect(prompt).toContain(
|
|
447
|
+
'If you have no findings, write []. Return as your final tool result a single line: ' +
|
|
448
|
+
'`bugs: <N> findings (<critical>/<high>/<medium>/<low> by risk.impact)`. ' +
|
|
449
|
+
'Do not include other prose.',
|
|
450
|
+
)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
test('the live specialists.md names the part-5 headings renderBriefPart writes', async () => {
|
|
454
|
+
const live = await readFile(LIVE_MD, 'utf8')
|
|
455
|
+
for (const heading of [
|
|
456
|
+
'## What this PR is for',
|
|
457
|
+
'What it does:',
|
|
458
|
+
'Watch items:',
|
|
459
|
+
'Open questions the scout could not resolve:',
|
|
460
|
+
'Repository review rules:',
|
|
461
|
+
]) {
|
|
462
|
+
expect(live).toContain(`\n${heading}\n`)
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
test('a repeat that fails after its session is reported and the next repeat still runs', async () => {
|
|
467
|
+
const ran: number[] = []
|
|
468
|
+
const warnings: string[] = []
|
|
469
|
+
const failed = await runRepeats(
|
|
470
|
+
3,
|
|
471
|
+
async (repeat) => {
|
|
472
|
+
ran.push(repeat)
|
|
473
|
+
if (repeat === 2) throw new PostSessionError('judge failed (probe result written to /r.json)')
|
|
474
|
+
},
|
|
475
|
+
(line) => warnings.push(line),
|
|
476
|
+
)
|
|
477
|
+
expect({ ran, failed, warnings }).toEqual({
|
|
478
|
+
ran: [1, 2, 3],
|
|
479
|
+
failed: 1,
|
|
480
|
+
warnings: [
|
|
481
|
+
'repeat 2 failed after its session: judge failed (probe result written to /r.json)\n',
|
|
482
|
+
],
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
test('a repeat that fails before its session stops the remaining repeats', async () => {
|
|
487
|
+
const ran: number[] = []
|
|
488
|
+
const run = runRepeats(
|
|
489
|
+
3,
|
|
490
|
+
async (repeat) => {
|
|
491
|
+
ran.push(repeat)
|
|
492
|
+
if (repeat === 1) throw new Error('worktree add failed')
|
|
493
|
+
},
|
|
494
|
+
() => {},
|
|
495
|
+
)
|
|
496
|
+
await expect(run).rejects.toThrow('worktree add failed')
|
|
497
|
+
expect(ran).toEqual([1])
|
|
498
|
+
})
|
|
@@ -114,6 +114,7 @@ test('diagnostics carry the log, manifest, findings files, deduped count and cri
|
|
|
114
114
|
criticDropped: [{ id: 'b', reason: 'speculative' }],
|
|
115
115
|
evidenceDropped: null,
|
|
116
116
|
thresholdDropped: null,
|
|
117
|
+
impactBytes: null,
|
|
117
118
|
})
|
|
118
119
|
})
|
|
119
120
|
|
|
@@ -129,6 +130,7 @@ test('diagnostics report absent manifest, deduped and drop files as null', async
|
|
|
129
130
|
criticDropped: null,
|
|
130
131
|
evidenceDropped: null,
|
|
131
132
|
thresholdDropped: null,
|
|
133
|
+
impactBytes: null,
|
|
132
134
|
})
|
|
133
135
|
})
|
|
134
136
|
|
|
@@ -162,3 +164,10 @@ test('a deduped findings file that is not an array is rejected', async () => {
|
|
|
162
164
|
await writeFile(join(dir, 'findings.deduped.json'), '{"id":"a"}')
|
|
163
165
|
await expect(replayDiagnostics(dir)).rejects.toThrow('findings.deduped.json')
|
|
164
166
|
})
|
|
167
|
+
|
|
168
|
+
test('diagnostics report the impact map size when the run wrote impact.md', async () => {
|
|
169
|
+
const dir = await scratchRunDir()
|
|
170
|
+
await writeFile(join(dir, 'log.jsonl'), '{"stage":"setup","status":"done"}\n')
|
|
171
|
+
await writeFile(join(dir, 'impact.md'), '### Sibling routes\n- `/é`\n')
|
|
172
|
+
expect((await replayDiagnostics(dir)).impactBytes).toBe(27)
|
|
173
|
+
})
|