@iceinvein/agent-skills 0.1.39 → 0.2.0
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/README.md +18 -2
- package/dist/cli/index.js +105 -28
- package/package.json +1 -1
- package/skills/index.json +14 -2
- package/skills/magpie/README.md +2 -1
- package/skills/magpie/SKILL.md +138 -45
- package/skills/magpie/bin/magpie.ts +43 -0
- package/skills/magpie/fixtures/example-pr/brief.json +18 -0
- package/skills/magpie/fixtures/fake-gh-nodiff.sh +38 -0
- package/skills/magpie/fixtures/fake-gh.sh +14 -0
- package/skills/magpie/package.json +1 -1
- package/skills/magpie/references/peer-review.md +7 -2
- package/skills/magpie/references/scout.md +90 -0
- package/skills/magpie/references/specialists.md +117 -6
- package/skills/magpie/scripts/__tests__/cli.test.ts +101 -1
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +187 -0
- package/skills/magpie/scripts/__tests__/diff-chunks.test.ts +51 -0
- package/skills/magpie/scripts/__tests__/filter-diff-preservation.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/findings-files.test.ts +35 -0
- package/skills/magpie/scripts/__tests__/gh.test.ts +90 -0
- package/skills/magpie/scripts/__tests__/git-diff.test.ts +83 -0
- package/skills/magpie/scripts/__tests__/helpers/git-fixture.ts +47 -0
- package/skills/magpie/scripts/__tests__/path-filter.test.ts +27 -0
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +16 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +45 -0
- package/skills/magpie/scripts/__tests__/render-cmd.test.ts +173 -1
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +151 -1
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +42 -0
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +83 -1
- package/skills/magpie/scripts/__tests__/shard.test.ts +165 -0
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +207 -7
- package/skills/magpie/scripts/__tests__/types.test.ts +47 -0
- package/skills/magpie/scripts/dedupe-cmd.ts +58 -3
- package/skills/magpie/scripts/diff-chunks.ts +28 -0
- package/skills/magpie/scripts/findings-files.ts +32 -0
- package/skills/magpie/scripts/gh.ts +68 -14
- package/skills/magpie/scripts/git-diff.ts +111 -0
- package/skills/magpie/scripts/path-filter.ts +9 -5
- package/skills/magpie/scripts/preview-cmd.ts +11 -1
- package/skills/magpie/scripts/refresh.ts +32 -3
- package/skills/magpie/scripts/render-cmd.ts +35 -11
- package/skills/magpie/scripts/render-findings.ts +78 -2
- package/skills/magpie/scripts/render-progress.ts +6 -1
- package/skills/magpie/scripts/setup-cmd.ts +38 -1
- package/skills/magpie/scripts/shard.ts +171 -0
- package/skills/magpie/scripts/status-cmd.ts +7 -4
- package/skills/magpie/scripts/types.ts +50 -0
- package/skills/magpie/skill.json +2 -2
- package/skills/magpie/templates/styles.css +66 -0
- package/skills/migrate/README.md +194 -0
- package/skills/migrate/SKILL.md +197 -0
- package/skills/migrate/bin/migrate +15 -0
- package/skills/migrate/bin/migrate.ts +309 -0
- package/skills/migrate/biome.json +35 -0
- package/skills/migrate/bun.lock +24 -0
- package/skills/migrate/docs/architecture.md +294 -0
- package/skills/migrate/docs/reference.md +590 -0
- package/skills/migrate/fixtures/tiny-express/GROUND-TRUTH.md +39 -0
- package/skills/migrate/fixtures/tiny-express/app.js +29 -0
- package/skills/migrate/fixtures/tiny-express/cron.js +6 -0
- package/skills/migrate/fixtures/tiny-express/reports/daily-users.json +6 -0
- package/skills/migrate/fixtures/tiny-express/schema.sql +12 -0
- package/skills/migrate/fixtures/tiny-express/settings.json +4 -0
- package/skills/migrate/fixtures/tiny-express/views/users.html +9 -0
- package/skills/migrate/fixtures/tiny-webforms/Controllers/UsersController.cs +68 -0
- package/skills/migrate/fixtures/tiny-webforms/Default.aspx +7 -0
- package/skills/migrate/fixtures/tiny-webforms/Default.aspx.cs +14 -0
- package/skills/migrate/fixtures/tiny-webforms/GROUND-TRUTH.md +50 -0
- package/skills/migrate/fixtures/tiny-webforms/Integrations/BillingClient.cs +16 -0
- package/skills/migrate/fixtures/tiny-webforms/Jobs/NightlyDigestJob.cs +33 -0
- package/skills/migrate/fixtures/tiny-webforms/Reports/DailyUsers.rdl +11 -0
- package/skills/migrate/fixtures/tiny-webforms/Schema.sql +12 -0
- package/skills/migrate/fixtures/tiny-webforms/Site.master +16 -0
- package/skills/migrate/fixtures/tiny-webforms/Users.aspx +8 -0
- package/skills/migrate/fixtures/tiny-webforms/Users.aspx.cs +14 -0
- package/skills/migrate/fixtures/tiny-webforms/web.config +10 -0
- package/skills/migrate/install.sh +68 -0
- package/skills/migrate/package.json +17 -0
- package/skills/migrate/references/phases/enumerate.md +291 -0
- package/skills/migrate/references/phases/extract.md +652 -0
- package/skills/migrate/references/phases/parity.md +275 -0
- package/skills/migrate/references/phases/probe.md +135 -0
- package/skills/migrate/references/phases/queue.md +242 -0
- package/skills/migrate/references/phases/seam.md +416 -0
- package/skills/migrate/references/recipes/README.md +116 -0
- package/skills/migrate/references/recipes/aspnet.md +287 -0
- package/skills/migrate/references/run-ops.md +280 -0
- package/skills/migrate/scripts/__tests__/census.test.ts +775 -0
- package/skills/migrate/scripts/__tests__/check.test.ts +458 -0
- package/skills/migrate/scripts/__tests__/citations.test.ts +156 -0
- package/skills/migrate/scripts/__tests__/cli.test.ts +183 -0
- package/skills/migrate/scripts/__tests__/concurrency.test.ts +164 -0
- package/skills/migrate/scripts/__tests__/config.test.ts +112 -0
- package/skills/migrate/scripts/__tests__/e2e-express.test.ts +1093 -0
- package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +1276 -0
- package/skills/migrate/scripts/__tests__/e2e.test.ts +320 -0
- package/skills/migrate/scripts/__tests__/ids.test.ts +38 -0
- package/skills/migrate/scripts/__tests__/import.test.ts +155 -0
- package/skills/migrate/scripts/__tests__/init.test.ts +192 -0
- package/skills/migrate/scripts/__tests__/leaks.test.ts +176 -0
- package/skills/migrate/scripts/__tests__/lock.test.ts +183 -0
- package/skills/migrate/scripts/__tests__/paths.test.ts +129 -0
- package/skills/migrate/scripts/__tests__/phase-cmd.test.ts +151 -0
- package/skills/migrate/scripts/__tests__/phases.test.ts +70 -0
- package/skills/migrate/scripts/__tests__/queue.test.ts +475 -0
- package/skills/migrate/scripts/__tests__/report.test.ts +150 -0
- package/skills/migrate/scripts/__tests__/run-state.test.ts +136 -0
- package/skills/migrate/scripts/__tests__/status-reset.test.ts +318 -0
- package/skills/migrate/scripts/__tests__/store.test.ts +132 -0
- package/skills/migrate/scripts/__tests__/validate.test.ts +54 -0
- package/skills/migrate/scripts/census-cmd.ts +109 -0
- package/skills/migrate/scripts/census.ts +342 -0
- package/skills/migrate/scripts/check-cmd.ts +24 -0
- package/skills/migrate/scripts/check.ts +376 -0
- package/skills/migrate/scripts/citations.ts +92 -0
- package/skills/migrate/scripts/config.ts +237 -0
- package/skills/migrate/scripts/ids.ts +31 -0
- package/skills/migrate/scripts/import-cmd.ts +141 -0
- package/skills/migrate/scripts/init-cmd.ts +118 -0
- package/skills/migrate/scripts/leaks.ts +184 -0
- package/skills/migrate/scripts/lock.ts +188 -0
- package/skills/migrate/scripts/paths.ts +103 -0
- package/skills/migrate/scripts/phase-cmd.ts +63 -0
- package/skills/migrate/scripts/phases.ts +113 -0
- package/skills/migrate/scripts/queue-cmd.ts +98 -0
- package/skills/migrate/scripts/queue.ts +258 -0
- package/skills/migrate/scripts/report-cmd.ts +47 -0
- package/skills/migrate/scripts/report.ts +131 -0
- package/skills/migrate/scripts/reset-cmd.ts +120 -0
- package/skills/migrate/scripts/status-cmd.ts +52 -0
- package/skills/migrate/scripts/store.ts +159 -0
- package/skills/migrate/scripts/types.ts +137 -0
- package/skills/migrate/scripts/validate.ts +221 -0
- package/skills/migrate/skill.json +33 -0
- package/skills/migrate/templates/config.toml +27 -0
- package/skills/migrate/templates/queue-item.md +17 -0
- package/skills/migrate/tsconfig.json +18 -0
- package/skills/migrate/uninstall.sh +31 -0
- package/skills/sluice/SKILL.md +82 -0
- package/skills/sluice/references/deep-channel.md +94 -0
- package/skills/sluice/references/finish.md +35 -0
- package/skills/sluice/references/intent.md +29 -0
- package/skills/sluice/references/review.md +42 -0
- package/skills/sluice/references/root-cause.md +38 -0
- package/skills/sluice/references/show-or-say.md +36 -0
- package/skills/sluice/references/test-first.md +35 -0
- package/skills/sluice/references/verify.md +26 -0
- package/skills/sluice/skill.json +32 -0
|
@@ -3,10 +3,13 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { fetchPr } from '../gh.ts'
|
|
6
|
+
import { createGitFixtureRepo } from './helpers/git-fixture.ts'
|
|
6
7
|
|
|
7
8
|
const FAKE_GH = new URL('../../fixtures/fake-gh.sh', import.meta.url).pathname
|
|
9
|
+
const FAKE_GH_NODIFF = new URL('../../fixtures/fake-gh-nodiff.sh', import.meta.url).pathname
|
|
8
10
|
|
|
9
11
|
let runDir: string
|
|
12
|
+
let repo = ''
|
|
10
13
|
|
|
11
14
|
beforeEach(async () => {
|
|
12
15
|
runDir = await mkdtemp(join(tmpdir(), 'magpie-gh-'))
|
|
@@ -14,6 +17,10 @@ beforeEach(async () => {
|
|
|
14
17
|
|
|
15
18
|
afterEach(async () => {
|
|
16
19
|
await rm(runDir, { recursive: true, force: true })
|
|
20
|
+
if (repo) {
|
|
21
|
+
await rm(repo, { recursive: true, force: true })
|
|
22
|
+
repo = ''
|
|
23
|
+
}
|
|
17
24
|
})
|
|
18
25
|
|
|
19
26
|
test('fetchPr writes pr.json and diff.patch', async () => {
|
|
@@ -45,3 +52,86 @@ test('fetchPr returns ok=false when gh exits non-zero', async () => {
|
|
|
45
52
|
expect(result.error).toBeDefined()
|
|
46
53
|
}
|
|
47
54
|
})
|
|
55
|
+
|
|
56
|
+
test('PR_VIEW_FIELDS requests commit messages and linked issues', async () => {
|
|
57
|
+
const { PR_VIEW_FIELDS } = await import('../gh.ts')
|
|
58
|
+
const fields = PR_VIEW_FIELDS.split(',')
|
|
59
|
+
expect(fields).toContain('commits')
|
|
60
|
+
expect(fields).toContain('closingIssuesReferences')
|
|
61
|
+
// The pre-existing fields must survive the edit.
|
|
62
|
+
for (const field of ['number', 'title', 'body', 'url', 'files', 'headRefOid']) {
|
|
63
|
+
expect(fields).toContain(field)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('fetchPr carries commits and linked issues into pr.json', async () => {
|
|
68
|
+
const result = await fetchPr({ ghBin: FAKE_GH, prNumber: 1234, runDir })
|
|
69
|
+
expect(result.ok).toBe(true)
|
|
70
|
+
const pr = JSON.parse(await readFile(join(runDir, 'pr.json'), 'utf8'))
|
|
71
|
+
expect(Array.isArray(pr.commits)).toBe(true)
|
|
72
|
+
expect(pr.commits[0].messageHeadline).toBe('Add retry handling to the upload path')
|
|
73
|
+
expect(Array.isArray(pr.closingIssuesReferences)).toBe(true)
|
|
74
|
+
expect(pr.closingIssuesReferences[0]).toMatchObject({ number: 42 })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('fetchPr falls back to local git when gh pr diff fails', async () => {
|
|
78
|
+
const { repoPath, head } = await createGitFixtureRepo()
|
|
79
|
+
repo = repoPath
|
|
80
|
+
const result = await fetchPr({
|
|
81
|
+
ghBin: FAKE_GH_NODIFF,
|
|
82
|
+
gitBin: 'git',
|
|
83
|
+
prNumber: 7,
|
|
84
|
+
runDir,
|
|
85
|
+
cwd: repo,
|
|
86
|
+
env: { MAGPIE_FAKE_HEAD_OID: head },
|
|
87
|
+
})
|
|
88
|
+
expect(result.ok).toBe(true)
|
|
89
|
+
if (!result.ok) return
|
|
90
|
+
expect(result.source).toBe('git')
|
|
91
|
+
expect(result.mergeBase).toMatch(/^[0-9a-f]{40}$/)
|
|
92
|
+
const diff = await readFile(join(runDir, 'diff.patch'), 'utf8')
|
|
93
|
+
expect(diff).toContain('export const x = 2')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('fetchPr falls back when gh returns an empty diff for a non-empty PR', async () => {
|
|
97
|
+
const { repoPath, head } = await createGitFixtureRepo()
|
|
98
|
+
repo = repoPath
|
|
99
|
+
const result = await fetchPr({
|
|
100
|
+
ghBin: FAKE_GH_NODIFF,
|
|
101
|
+
gitBin: 'git',
|
|
102
|
+
prNumber: 7,
|
|
103
|
+
runDir,
|
|
104
|
+
cwd: repo,
|
|
105
|
+
env: { MAGPIE_FAKE_HEAD_OID: head, MAGPIE_FAKE_DIFF_MODE: 'empty' },
|
|
106
|
+
})
|
|
107
|
+
expect(result.ok).toBe(true)
|
|
108
|
+
if (!result.ok) return
|
|
109
|
+
expect(result.source).toBe('git')
|
|
110
|
+
const diff = await readFile(join(runDir, 'diff.patch'), 'utf8')
|
|
111
|
+
expect(diff).toContain('export const x = 2')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('fetchPr reports source gh when gh succeeds', async () => {
|
|
115
|
+
const result = await fetchPr({ ghBin: FAKE_GH, prNumber: 1234, runDir })
|
|
116
|
+
expect(result.ok).toBe(true)
|
|
117
|
+
if (!result.ok) return
|
|
118
|
+
expect(result.source).toBe('gh')
|
|
119
|
+
expect(result.mergeBase).toBeUndefined()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('fetchPr fails when gh refuses and the head SHA is not local', async () => {
|
|
123
|
+
const { repoPath } = await createGitFixtureRepo()
|
|
124
|
+
repo = repoPath
|
|
125
|
+
const result = await fetchPr({
|
|
126
|
+
ghBin: FAKE_GH_NODIFF,
|
|
127
|
+
gitBin: 'git',
|
|
128
|
+
prNumber: 7,
|
|
129
|
+
runDir,
|
|
130
|
+
cwd: repo,
|
|
131
|
+
// No MAGPIE_FAKE_HEAD_OID, so pr.json carries a SHA this repo has never seen.
|
|
132
|
+
})
|
|
133
|
+
expect(result.ok).toBe(false)
|
|
134
|
+
if (result.ok) return
|
|
135
|
+
expect(result.error).toContain('cannot resolve PR head')
|
|
136
|
+
expect(result.error).toContain('406')
|
|
137
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
import { rm } from 'node:fs/promises'
|
|
3
|
+
import { localPrDiff } from '../git-diff.ts'
|
|
4
|
+
import { createGitFixtureRepo, git } from './helpers/git-fixture.ts'
|
|
5
|
+
|
|
6
|
+
let repo = ''
|
|
7
|
+
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
if (repo) {
|
|
10
|
+
await rm(repo, { recursive: true, force: true })
|
|
11
|
+
repo = ''
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('localPrDiff produces the three-dot diff from the merge base', async () => {
|
|
16
|
+
const { repoPath, head } = await createGitFixtureRepo()
|
|
17
|
+
repo = repoPath
|
|
18
|
+
const result = await localPrDiff({
|
|
19
|
+
gitBin: 'git',
|
|
20
|
+
repoPath,
|
|
21
|
+
prNumber: 7,
|
|
22
|
+
baseRefName: 'main',
|
|
23
|
+
headRefOid: head,
|
|
24
|
+
})
|
|
25
|
+
expect(result.ok).toBe(true)
|
|
26
|
+
if (!result.ok) return
|
|
27
|
+
expect(result.diff).toContain('export const x = 2')
|
|
28
|
+
// b.ts landed on main after the branch point. A two-dot diff against the
|
|
29
|
+
// base tip would show it as a deletion; the three-dot diff must not.
|
|
30
|
+
expect(result.diff).not.toContain('b.ts')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('localPrDiff errors when the head SHA is not present locally', async () => {
|
|
34
|
+
const { repoPath } = await createGitFixtureRepo()
|
|
35
|
+
repo = repoPath
|
|
36
|
+
const result = await localPrDiff({
|
|
37
|
+
gitBin: 'git',
|
|
38
|
+
repoPath,
|
|
39
|
+
prNumber: 7,
|
|
40
|
+
baseRefName: 'main',
|
|
41
|
+
headRefOid: '0'.repeat(40),
|
|
42
|
+
})
|
|
43
|
+
expect(result.ok).toBe(false)
|
|
44
|
+
if (result.ok) return
|
|
45
|
+
expect(result.error).toContain('cannot resolve PR head')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('localPrDiff errors when the base branch is not present locally', async () => {
|
|
49
|
+
const { repoPath, head } = await createGitFixtureRepo()
|
|
50
|
+
repo = repoPath
|
|
51
|
+
const result = await localPrDiff({
|
|
52
|
+
gitBin: 'git',
|
|
53
|
+
repoPath,
|
|
54
|
+
prNumber: 7,
|
|
55
|
+
baseRefName: 'no-such-branch',
|
|
56
|
+
headRefOid: head,
|
|
57
|
+
})
|
|
58
|
+
expect(result.ok).toBe(false)
|
|
59
|
+
if (result.ok) return
|
|
60
|
+
expect(result.error).toContain('cannot resolve base branch')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('localPrDiff errors when a stale refs/magpie/pr-<n> ref points at an older commit', async () => {
|
|
64
|
+
const { repoPath, head } = await createGitFixtureRepo()
|
|
65
|
+
repo = repoPath
|
|
66
|
+
// Simulate a previous run's fetch having landed an older commit under the
|
|
67
|
+
// PR ref, with no origin remote configured to refresh it from. head~1 is
|
|
68
|
+
// the pre-branch "base" commit: a valid, resolvable, genuinely older commit.
|
|
69
|
+
const staleSha = await git(repoPath, 'rev-parse', `${head}~1`)
|
|
70
|
+
await git(repoPath, 'update-ref', 'refs/magpie/pr-7', staleSha)
|
|
71
|
+
const result = await localPrDiff({
|
|
72
|
+
gitBin: 'git',
|
|
73
|
+
repoPath,
|
|
74
|
+
prNumber: 7,
|
|
75
|
+
baseRefName: 'main',
|
|
76
|
+
headRefOid: head,
|
|
77
|
+
})
|
|
78
|
+
expect(result.ok).toBe(false)
|
|
79
|
+
if (result.ok) return
|
|
80
|
+
expect(result.error).toContain('stale')
|
|
81
|
+
expect(result.error).toContain(staleSha)
|
|
82
|
+
expect(result.error).toContain(head)
|
|
83
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/** Spawn `git` in `cwd` and return trimmed stdout, throwing on a non-zero exit. */
|
|
6
|
+
export async function git(cwd: string, ...args: string[]): Promise<string> {
|
|
7
|
+
const proc = Bun.spawn(['git', ...args], { cwd, stdout: 'pipe', stderr: 'pipe' })
|
|
8
|
+
const stdout = await new Response(proc.stdout).text()
|
|
9
|
+
const exit = await proc.exited
|
|
10
|
+
if (exit !== 0) {
|
|
11
|
+
const stderr = await new Response(proc.stderr).text()
|
|
12
|
+
throw new Error(`git ${args.join(' ')} exit ${exit}: ${stderr}`)
|
|
13
|
+
}
|
|
14
|
+
return stdout.trim()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type GitFixtureRepo = {
|
|
18
|
+
repoPath: string
|
|
19
|
+
head: string
|
|
20
|
+
baseTip: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A fresh repo with `main` and a `feature-x` branch one commit ahead, plus a
|
|
25
|
+
* commit on `main` after the branch point so a two-dot diff would differ
|
|
26
|
+
* from a three-dot one. Shared by gh.test.ts and git-diff.test.ts, which both
|
|
27
|
+
* exercise the local-git PR-diff fallback against a real clone.
|
|
28
|
+
*/
|
|
29
|
+
export async function createGitFixtureRepo(): Promise<GitFixtureRepo> {
|
|
30
|
+
const repoPath = await mkdtemp(join(tmpdir(), 'magpie-git-fixture-'))
|
|
31
|
+
await git(repoPath, 'init', '-b', 'main')
|
|
32
|
+
await git(repoPath, 'config', 'user.email', 'test@example.com')
|
|
33
|
+
await git(repoPath, 'config', 'user.name', 'Test')
|
|
34
|
+
await writeFile(join(repoPath, 'a.ts'), 'export const x = 1\n')
|
|
35
|
+
await git(repoPath, 'add', '.')
|
|
36
|
+
await git(repoPath, 'commit', '-m', 'base')
|
|
37
|
+
await git(repoPath, 'checkout', '-b', 'feature-x')
|
|
38
|
+
await writeFile(join(repoPath, 'a.ts'), 'export const x = 2\n')
|
|
39
|
+
await git(repoPath, 'commit', '-am', 'change x')
|
|
40
|
+
const head = await git(repoPath, 'rev-parse', 'HEAD')
|
|
41
|
+
await git(repoPath, 'checkout', 'main')
|
|
42
|
+
await writeFile(join(repoPath, 'b.ts'), 'export const y = 9\n')
|
|
43
|
+
await git(repoPath, 'add', '.')
|
|
44
|
+
await git(repoPath, 'commit', '-m', 'unrelated base commit')
|
|
45
|
+
const baseTip = await git(repoPath, 'rev-parse', 'HEAD')
|
|
46
|
+
return { repoPath, head, baseTip }
|
|
47
|
+
}
|
|
@@ -159,3 +159,30 @@ test('filterDiff: include records not-in-include reason', () => {
|
|
|
159
159
|
})
|
|
160
160
|
expect(excluded.find((e) => e.path === 'bun.lock')?.pattern).toBe('not-in-include')
|
|
161
161
|
})
|
|
162
|
+
|
|
163
|
+
test('DEFAULT_EXCLUDES covers generated .NET sources', () => {
|
|
164
|
+
const generated = [
|
|
165
|
+
'src/Data/Migrations/20260801_AddUsers.Designer.cs',
|
|
166
|
+
'src/Data/Migrations/AppDbContextModelSnapshot.cs',
|
|
167
|
+
'src/Generated/Api.g.cs',
|
|
168
|
+
'src/Generated/Api.g.i.cs',
|
|
169
|
+
'src/Forms/MainForm.designer.vb',
|
|
170
|
+
]
|
|
171
|
+
for (const path of generated) {
|
|
172
|
+
expect(matchesAny(path, DEFAULT_EXCLUDES)).not.toBeNull()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('DEFAULT_EXCLUDES leaves hand-written C# and docs alone', () => {
|
|
177
|
+
// The migration itself is reviewable; only its generated designer is not.
|
|
178
|
+
const reviewable = [
|
|
179
|
+
'src/Data/Migrations/20260801_AddUsers.cs',
|
|
180
|
+
'src/Services/UserService.cs',
|
|
181
|
+
'docs/architecture.md',
|
|
182
|
+
'docs/work/notes.md',
|
|
183
|
+
'README.md',
|
|
184
|
+
]
|
|
185
|
+
for (const path of reviewable) {
|
|
186
|
+
expect(matchesAny(path, DEFAULT_EXCLUDES)).toBeNull()
|
|
187
|
+
}
|
|
188
|
+
})
|
|
@@ -208,3 +208,19 @@ test('bundled fixture covers all five focus domains', async () => {
|
|
|
208
208
|
expect(html).toContain(`data-domain="${domain}"`)
|
|
209
209
|
}
|
|
210
210
|
})
|
|
211
|
+
|
|
212
|
+
test('bundled fixture brief.json renders the PR brief header', async () => {
|
|
213
|
+
await runPreview({
|
|
214
|
+
page: 'findings',
|
|
215
|
+
stage: DEFAULT_STAGE,
|
|
216
|
+
outDir: workDir,
|
|
217
|
+
openInBrowser: false,
|
|
218
|
+
})
|
|
219
|
+
const html = await readFile(join(workDir, 'findings.html'), 'utf8')
|
|
220
|
+
expect(html).toContain('class="pr-brief"')
|
|
221
|
+
// Unique to fixtures/example-pr/brief.json; would not appear from any other
|
|
222
|
+
// part of the fixture or the page shell.
|
|
223
|
+
expect(html).toContain(
|
|
224
|
+
'Adds bounded retries to the upload path so transient S3 failures stop surfacing to end users as hard errors.',
|
|
225
|
+
)
|
|
226
|
+
})
|
|
@@ -84,3 +84,48 @@ test('refreshFindings sets data-run-id from the run directory basename', async (
|
|
|
84
84
|
const expectedId = runDir.split('/').pop()
|
|
85
85
|
expect(fresh).toContain(`data-run-id="${expectedId}"`)
|
|
86
86
|
})
|
|
87
|
+
|
|
88
|
+
test('refreshFindings carries the brief header through, so archives keep it on re-render', async () => {
|
|
89
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
90
|
+
await writeFile(
|
|
91
|
+
join(runDir, 'brief.json'),
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
purpose: 'Adds bounded retries to the upload path.',
|
|
94
|
+
changes: ['Wraps the S3 put in a bounded retry'],
|
|
95
|
+
subsystems: [{ name: 'upload', role: 'owns the put path' }],
|
|
96
|
+
watchItems: [],
|
|
97
|
+
unclear: [],
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
const result = await refreshFindings(runDir)
|
|
101
|
+
expect(result.refreshed).toBe(true)
|
|
102
|
+
const fresh = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
103
|
+
expect(fresh).toContain('class="pr-brief"')
|
|
104
|
+
expect(fresh).toContain('Adds bounded retries to the upload path.')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('refreshFindings omits the brief header when brief.json is absent', async () => {
|
|
108
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
109
|
+
await refreshFindings(runDir)
|
|
110
|
+
const fresh = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
111
|
+
expect(fresh).not.toContain('class="pr-brief"')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('refreshFindings carries linked issues from pr.json through the brief header', async () => {
|
|
115
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
116
|
+
await writeFile(
|
|
117
|
+
join(runDir, 'pr.json'),
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
number: 1234,
|
|
120
|
+
headRefName: 'feature-x',
|
|
121
|
+
headRefOid: 'deadbeef',
|
|
122
|
+
closingIssuesReferences: [
|
|
123
|
+
{ number: 42, title: 'Uploads fail', url: 'https://example.test/issues/42' },
|
|
124
|
+
],
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
await writeFile(join(runDir, 'brief.json'), JSON.stringify({ purpose: 'Fixes uploads.' }))
|
|
128
|
+
await refreshFindings(runDir)
|
|
129
|
+
const fresh = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
130
|
+
expect(fresh).toContain('https://example.test/issues/42')
|
|
131
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { runRender } from '../render-cmd.ts'
|
|
@@ -115,3 +115,175 @@ test('findings render includes files and diff when provided', async () => {
|
|
|
115
115
|
expect(html).toContain('data-file-pane="src/app.ts"')
|
|
116
116
|
expect(html).toContain('data-file-pane="src/util.ts"')
|
|
117
117
|
})
|
|
118
|
+
|
|
119
|
+
test('findings render includes the brief header when brief.json is present', async () => {
|
|
120
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
121
|
+
await writeFile(
|
|
122
|
+
join(runDir, 'brief.json'),
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
purpose: 'Adds bounded retries to the upload path.',
|
|
125
|
+
changes: ['Wraps the S3 put in a bounded retry'],
|
|
126
|
+
subsystems: [{ name: 'upload', role: 'owns the put path' }],
|
|
127
|
+
watchItems: [],
|
|
128
|
+
unclear: [],
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
132
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
133
|
+
expect(html).toContain('class="pr-brief"')
|
|
134
|
+
expect(html).toContain('Adds bounded retries to the upload path.')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('findings render omits the brief header when brief.json is absent', async () => {
|
|
138
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
139
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
140
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
141
|
+
expect(html).not.toContain('class="pr-brief"')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('a malformed brief.json degrades to no header instead of failing the render', async () => {
|
|
145
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
146
|
+
await writeFile(join(runDir, 'brief.json'), '{ this is not json')
|
|
147
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
148
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
149
|
+
expect(html).not.toContain('class="pr-brief"')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('linked issues from pr.json render in the brief header', async () => {
|
|
153
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
154
|
+
await writeFile(
|
|
155
|
+
join(runDir, 'pr.json'),
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
number: 1234,
|
|
158
|
+
headRefName: 'feature-x',
|
|
159
|
+
headRefOid: 'deadbeef',
|
|
160
|
+
closingIssuesReferences: [
|
|
161
|
+
{ number: 42, title: 'Uploads fail', url: 'https://example.test/issues/42' },
|
|
162
|
+
],
|
|
163
|
+
}),
|
|
164
|
+
)
|
|
165
|
+
await writeFile(join(runDir, 'brief.json'), JSON.stringify({ purpose: 'Fixes uploads.' }))
|
|
166
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
167
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
168
|
+
expect(html).toContain('https://example.test/issues/42')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('issue entries with a missing, zero, or negative number are dropped, not defaulted to 0', async () => {
|
|
172
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
173
|
+
await writeFile(
|
|
174
|
+
join(runDir, 'pr.json'),
|
|
175
|
+
JSON.stringify({
|
|
176
|
+
number: 1234,
|
|
177
|
+
headRefName: 'feature-x',
|
|
178
|
+
headRefOid: 'deadbeef',
|
|
179
|
+
closingIssuesReferences: [
|
|
180
|
+
{ number: 42, title: 'Uploads fail', url: 'https://example.test/issues/42' },
|
|
181
|
+
{ title: 'Missing number', url: 'https://example.test/issues/missing' },
|
|
182
|
+
{ number: 0, title: 'Zero number', url: 'https://example.test/issues/zero' },
|
|
183
|
+
{ number: -1, title: 'Negative number', url: 'https://example.test/issues/negative' },
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
await writeFile(join(runDir, 'brief.json'), JSON.stringify({ purpose: 'Fixes uploads.' }))
|
|
188
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
189
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
190
|
+
expect(html).toContain('https://example.test/issues/42')
|
|
191
|
+
expect(html).not.toContain('https://example.test/issues/missing')
|
|
192
|
+
expect(html).not.toContain('https://example.test/issues/zero')
|
|
193
|
+
expect(html).not.toContain('https://example.test/issues/negative')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('progress sums specialist counts across shards', async () => {
|
|
197
|
+
await mkdir(join(runDir, 'shards'), { recursive: true })
|
|
198
|
+
await writeFile(
|
|
199
|
+
join(runDir, 'shards', 'manifest.json'),
|
|
200
|
+
JSON.stringify({
|
|
201
|
+
budget: 6000,
|
|
202
|
+
maxFiles: 80,
|
|
203
|
+
totalFiles: 2,
|
|
204
|
+
totalLines: 20,
|
|
205
|
+
shards: [
|
|
206
|
+
{ id: 1, path: 'shards/shard-1.patch', files: ['a.ts'], lines: 10 },
|
|
207
|
+
{ id: 2, path: 'shards/shard-2.patch', files: ['b.ts'], lines: 10 },
|
|
208
|
+
],
|
|
209
|
+
}),
|
|
210
|
+
)
|
|
211
|
+
await writeFile(
|
|
212
|
+
join(runDir, 'log.jsonl'),
|
|
213
|
+
[
|
|
214
|
+
JSON.stringify({ stage: 'specialists', status: 'running' }),
|
|
215
|
+
JSON.stringify({
|
|
216
|
+
stage: 'specialist',
|
|
217
|
+
focus: 'security',
|
|
218
|
+
shard: 1,
|
|
219
|
+
status: 'done',
|
|
220
|
+
findings: 2,
|
|
221
|
+
}),
|
|
222
|
+
JSON.stringify({
|
|
223
|
+
stage: 'specialist',
|
|
224
|
+
focus: 'security',
|
|
225
|
+
shard: 2,
|
|
226
|
+
status: 'done',
|
|
227
|
+
findings: 3,
|
|
228
|
+
}),
|
|
229
|
+
JSON.stringify({ stage: 'specialist', focus: 'bugs', shard: 1, status: 'done', findings: 1 }),
|
|
230
|
+
].join('\n'),
|
|
231
|
+
)
|
|
232
|
+
const exit = await runRender(runDir, 'progress')
|
|
233
|
+
expect(exit).toBe(0)
|
|
234
|
+
const html = await readFile(join(runDir, 'screen', 'progress.html'), 'utf8')
|
|
235
|
+
expect(html).toContain('Five reviewers across 2 shards')
|
|
236
|
+
// security is 2 + 3, not 3.
|
|
237
|
+
expect(html).toContain('security <span class="count">5</span>')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
/** Log with the specialists stage in flight, which is the only state where the
|
|
241
|
+
* shard count reaches the rendered wording. */
|
|
242
|
+
async function specialistsRunning(): Promise<void> {
|
|
243
|
+
await writeFile(
|
|
244
|
+
join(runDir, 'log.jsonl'),
|
|
245
|
+
JSON.stringify({ stage: 'specialists', status: 'running' }),
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
test('a malformed shards/manifest.json degrades to the unsharded progress wording', async () => {
|
|
250
|
+
await mkdir(join(runDir, 'shards'), { recursive: true })
|
|
251
|
+
await writeFile(join(runDir, 'shards', 'manifest.json'), '{ "shards": [ truncated')
|
|
252
|
+
await specialistsRunning()
|
|
253
|
+
expect(await runRender(runDir, 'progress')).toBe(0)
|
|
254
|
+
const html = await readFile(join(runDir, 'screen', 'progress.html'), 'utf8')
|
|
255
|
+
expect(html).toContain('Five reviewers reading the diff in parallel')
|
|
256
|
+
expect(html).not.toMatch(/across \S+ shards/)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('a manifest with shards: [] renders the unsharded progress wording', async () => {
|
|
260
|
+
// A lockfile-only PR filters down to nothing, and shardDiff writes an
|
|
261
|
+
// otherwise-normal manifest with no shards. Zero must not read as "across 0
|
|
262
|
+
// shards"; stage 4 treats it as the unsharded path.
|
|
263
|
+
await mkdir(join(runDir, 'shards'), { recursive: true })
|
|
264
|
+
await writeFile(
|
|
265
|
+
join(runDir, 'shards', 'manifest.json'),
|
|
266
|
+
JSON.stringify({ budget: 6000, maxFiles: 80, totalFiles: 0, totalLines: 0, shards: [] }),
|
|
267
|
+
)
|
|
268
|
+
await specialistsRunning()
|
|
269
|
+
expect(await runRender(runDir, 'progress')).toBe(0)
|
|
270
|
+
const html = await readFile(join(runDir, 'screen', 'progress.html'), 'utf8')
|
|
271
|
+
expect(html).toContain('Five reviewers reading the diff in parallel')
|
|
272
|
+
expect(html).not.toContain('across 0 shards')
|
|
273
|
+
expect(html).not.toMatch(/across \S+ shards/)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('progress re-dispatch of one shard replaces rather than doubles its count', async () => {
|
|
277
|
+
await writeFile(
|
|
278
|
+
join(runDir, 'log.jsonl'),
|
|
279
|
+
[
|
|
280
|
+
JSON.stringify({ stage: 'specialists', status: 'running' }),
|
|
281
|
+
JSON.stringify({ stage: 'specialist', focus: 'bugs', shard: 1, status: 'done', findings: 4 }),
|
|
282
|
+
JSON.stringify({ stage: 'specialist', focus: 'bugs', shard: 1, status: 'done', findings: 2 }),
|
|
283
|
+
].join('\n'),
|
|
284
|
+
)
|
|
285
|
+
const exit = await runRender(runDir, 'progress')
|
|
286
|
+
expect(exit).toBe(0)
|
|
287
|
+
const html = await readFile(join(runDir, 'screen', 'progress.html'), 'utf8')
|
|
288
|
+
expect(html).toContain('bugs <span class="count">2</span>')
|
|
289
|
+
})
|