@iceinvein/agent-skills 0.1.24 → 0.1.26
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 +1 -0
- package/package.json +1 -1
- package/skills/magpie/README.md +16 -1
- package/skills/magpie/bin/magpie.ts +60 -2
- package/skills/magpie/fixtures/example-pr/diff.patch +86 -0
- package/skills/magpie/fixtures/example-pr/findings.final.json +137 -0
- package/skills/magpie/fixtures/example-pr/post-status.json +5 -0
- package/skills/magpie/fixtures/example-pr/pr.json +21 -0
- package/skills/magpie/fixtures/fake-gh.sh +10 -1
- package/skills/magpie/package.json +1 -1
- package/skills/magpie/scripts/__tests__/cli.test.ts +119 -0
- package/skills/magpie/scripts/__tests__/diff-utils.test.ts +106 -0
- package/skills/magpie/scripts/__tests__/file-tree.test.ts +97 -0
- package/skills/magpie/scripts/__tests__/gh.test.ts +8 -0
- package/skills/magpie/scripts/__tests__/helper.test.ts +63 -0
- package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +10 -0
- package/skills/magpie/scripts/__tests__/post-cmd.test.ts +90 -1
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +210 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +2 -2
- package/skills/magpie/scripts/__tests__/render-action-bar.test.ts +66 -0
- package/skills/magpie/scripts/__tests__/render-annotation.test.ts +67 -0
- package/skills/magpie/scripts/__tests__/render-cmd.test.ts +46 -0
- package/skills/magpie/scripts/__tests__/render-diff.test.ts +84 -0
- package/skills/magpie/scripts/__tests__/render-file-tree.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +104 -182
- package/skills/magpie/scripts/__tests__/render-issues-list.test.ts +67 -0
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +86 -0
- package/skills/magpie/scripts/__tests__/server.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/types.test.ts +48 -3
- package/skills/magpie/scripts/diff-utils.ts +129 -0
- package/skills/magpie/scripts/file-tree.ts +74 -0
- package/skills/magpie/scripts/gh.ts +1 -0
- package/skills/magpie/scripts/helper.js +402 -192
- package/skills/magpie/scripts/post-cmd.ts +202 -0
- package/skills/magpie/scripts/preview-cmd.ts +300 -0
- package/skills/magpie/scripts/refresh.ts +16 -1
- package/skills/magpie/scripts/render-action-bar.ts +51 -0
- package/skills/magpie/scripts/render-annotation.ts +96 -0
- package/skills/magpie/scripts/render-cmd.ts +24 -1
- package/skills/magpie/scripts/render-diff.ts +125 -0
- package/skills/magpie/scripts/render-file-tree.ts +86 -0
- package/skills/magpie/scripts/render-findings.ts +165 -196
- package/skills/magpie/scripts/render-issues-list.ts +78 -0
- package/skills/magpie/scripts/render-progress.ts +74 -32
- package/skills/magpie/scripts/server.ts +43 -0
- package/skills/magpie/scripts/types.ts +13 -0
- package/skills/magpie/skill.json +4 -2
- package/skills/magpie/templates/styles.css +1288 -629
|
@@ -69,3 +69,49 @@ test('findings render with no findings json shows empty state', async () => {
|
|
|
69
69
|
const html = await Bun.file(join(runDir, 'screen', 'findings.html')).text()
|
|
70
70
|
expect(html).toContain('No findings')
|
|
71
71
|
})
|
|
72
|
+
|
|
73
|
+
test('findings render includes files and diff when provided', async () => {
|
|
74
|
+
await writeFile(
|
|
75
|
+
join(runDir, 'findings.final.json'),
|
|
76
|
+
JSON.stringify([
|
|
77
|
+
{
|
|
78
|
+
id: 'a',
|
|
79
|
+
file: 'src/app.ts',
|
|
80
|
+
line: 5,
|
|
81
|
+
severity: 'high',
|
|
82
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'must-fix' },
|
|
83
|
+
title: 'test finding',
|
|
84
|
+
description: 'd',
|
|
85
|
+
domain: 'bugs',
|
|
86
|
+
},
|
|
87
|
+
]),
|
|
88
|
+
)
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(runDir, 'pr.json'),
|
|
91
|
+
JSON.stringify({
|
|
92
|
+
number: 1234,
|
|
93
|
+
headRefName: 'feature-x',
|
|
94
|
+
headRefOid: 'deadbeef',
|
|
95
|
+
files: [
|
|
96
|
+
{ path: 'src/app.ts', additions: 10, deletions: 2 },
|
|
97
|
+
{ path: 'src/util.ts', additions: 5, deletions: 0 },
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
await writeFile(
|
|
102
|
+
join(runDir, 'diff.patch'),
|
|
103
|
+
`--- a/src/app.ts
|
|
104
|
+
+++ b/src/app.ts
|
|
105
|
+
@@ -1,3 +1,6 @@
|
|
106
|
+
function hello() {
|
|
107
|
+
+ console.log('added');
|
|
108
|
+
+ console.log('more');
|
|
109
|
+
}
|
|
110
|
+
`,
|
|
111
|
+
)
|
|
112
|
+
const exit = await runRender(runDir, 'findings')
|
|
113
|
+
expect(exit).toBe(0)
|
|
114
|
+
const html = await Bun.file(join(runDir, 'screen', 'findings.html')).text()
|
|
115
|
+
expect(html).toContain('data-file-pane="src/app.ts"')
|
|
116
|
+
expect(html).toContain('data-file-pane="src/util.ts"')
|
|
117
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { parseUnifiedDiffToHunks } from '../diff-utils.ts'
|
|
3
|
+
import { renderSplitDiff, renderUnifiedDiff } from '../render-diff.ts'
|
|
4
|
+
import type { ReviewFinding } from '../types.ts'
|
|
5
|
+
|
|
6
|
+
const DIFF = `--- a/x.ts
|
|
7
|
+
+++ b/x.ts
|
|
8
|
+
@@ -1,3 +1,4 @@
|
|
9
|
+
line1
|
|
10
|
+
-old
|
|
11
|
+
+new
|
|
12
|
+
+added
|
|
13
|
+
line3
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
const finding: ReviewFinding = {
|
|
17
|
+
id: 'f-1',
|
|
18
|
+
file: 'x.ts',
|
|
19
|
+
line: 2,
|
|
20
|
+
severity: 'high',
|
|
21
|
+
title: 't',
|
|
22
|
+
description: 'd',
|
|
23
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'should-fix' },
|
|
24
|
+
domain: 'bugs',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('renderUnifiedDiff', () => {
|
|
28
|
+
test('emits one row per diff line', () => {
|
|
29
|
+
const html = renderUnifiedDiff({
|
|
30
|
+
hunks: parseUnifiedDiffToHunks(DIFF),
|
|
31
|
+
findings: [],
|
|
32
|
+
postStatus: {},
|
|
33
|
+
selectedIds: new Set(),
|
|
34
|
+
})
|
|
35
|
+
expect((html.match(/class="diff-row/g) ?? []).length).toBe(5)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('places annotations after the matching new-line', () => {
|
|
39
|
+
const hunks = parseUnifiedDiffToHunks(DIFF)
|
|
40
|
+
const html = renderUnifiedDiff({
|
|
41
|
+
hunks,
|
|
42
|
+
findings: [finding],
|
|
43
|
+
postStatus: {},
|
|
44
|
+
selectedIds: new Set(),
|
|
45
|
+
})
|
|
46
|
+
const idx = html.indexOf('data-finding-id="f-1"')
|
|
47
|
+
const before = html.lastIndexOf('class="diff-row', idx)
|
|
48
|
+
const newRowSnippet = html.slice(before, idx)
|
|
49
|
+
expect(newRowSnippet).toContain('class="diff-row added"')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('emits a "no diff" placeholder when hunks is empty', () => {
|
|
53
|
+
const html = renderUnifiedDiff({
|
|
54
|
+
hunks: [],
|
|
55
|
+
findings: [],
|
|
56
|
+
postStatus: {},
|
|
57
|
+
selectedIds: new Set(),
|
|
58
|
+
})
|
|
59
|
+
expect(html).toContain('No changes')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('renders posted findings with data-posted="true"', () => {
|
|
63
|
+
const html = renderUnifiedDiff({
|
|
64
|
+
hunks: parseUnifiedDiffToHunks(DIFF),
|
|
65
|
+
findings: [finding],
|
|
66
|
+
postStatus: { 'f-1': 'posted' },
|
|
67
|
+
selectedIds: new Set(),
|
|
68
|
+
})
|
|
69
|
+
expect(html).toContain('data-posted="true"')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('renderSplitDiff', () => {
|
|
74
|
+
test('emits left/right cells per row', () => {
|
|
75
|
+
const html = renderSplitDiff({
|
|
76
|
+
hunks: parseUnifiedDiffToHunks(DIFF),
|
|
77
|
+
findings: [],
|
|
78
|
+
postStatus: {},
|
|
79
|
+
selectedIds: new Set(),
|
|
80
|
+
})
|
|
81
|
+
expect((html.match(/diff-row split/g) ?? []).length).toBeGreaterThan(0)
|
|
82
|
+
expect(html).toContain('diff-cell')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderFileTree } from '../render-file-tree.ts'
|
|
3
|
+
import type { PrFileEntry, ReviewFinding } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
const files: PrFileEntry[] = [
|
|
6
|
+
{ path: 'src/a.ts', additions: 5, deletions: 1 },
|
|
7
|
+
{ path: 'src/b.ts', additions: 2, deletions: 0 },
|
|
8
|
+
{ path: 'README.md', additions: 3, deletions: 0 },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const findings: ReviewFinding[] = [
|
|
12
|
+
{
|
|
13
|
+
id: '1',
|
|
14
|
+
file: 'src/a.ts',
|
|
15
|
+
line: 1,
|
|
16
|
+
severity: 'blocker',
|
|
17
|
+
title: 't',
|
|
18
|
+
description: 'd',
|
|
19
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'must-fix' },
|
|
20
|
+
domain: 'security',
|
|
21
|
+
},
|
|
22
|
+
] as ReviewFinding[]
|
|
23
|
+
|
|
24
|
+
describe('renderFileTree', () => {
|
|
25
|
+
test('emits a button per file with data-file attribute', () => {
|
|
26
|
+
const html = renderFileTree({ files, findings })
|
|
27
|
+
expect(html).toContain('data-file="src/a.ts"')
|
|
28
|
+
expect(html).toContain('data-file="src/b.ts"')
|
|
29
|
+
expect(html).toContain('data-file="README.md"')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('emits +adds -dels deltas per file', () => {
|
|
33
|
+
const html = renderFileTree({ files, findings })
|
|
34
|
+
expect(html).toContain('+5')
|
|
35
|
+
expect(html).toContain('-1')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('emits severity pip counts on files that have findings', () => {
|
|
39
|
+
const html = renderFileTree({ files, findings })
|
|
40
|
+
expect(html).toMatch(/data-file="src\/a\.ts"[\s\S]*?sev-pip blocker[\s\S]*?>1</)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('renders an Overview entry as data-file=""', () => {
|
|
44
|
+
const html = renderFileTree({ files, findings })
|
|
45
|
+
expect(html).toContain('data-file=""')
|
|
46
|
+
expect(html).toContain('Overview')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('collapses single-child directory chains', () => {
|
|
50
|
+
const f: PrFileEntry[] = [{ path: 'apps/server/lib/x.ts', additions: 1, deletions: 0 }]
|
|
51
|
+
const html = renderFileTree({ files: f, findings: [] })
|
|
52
|
+
expect(html).toContain('apps/server/lib')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -22,243 +22,165 @@ function f(p: Partial<ReviewFinding> & { id: string; title: string }): ReviewFin
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const SAMPLE_FINDINGS: ReviewFinding[] = [
|
|
26
|
+
f({ id: 'a', title: 'null deref', severity: 'high', file: 'src/a.ts', line: 2 }),
|
|
27
|
+
f({
|
|
28
|
+
id: 'b',
|
|
29
|
+
title: 'magic number',
|
|
30
|
+
severity: 'low',
|
|
31
|
+
file: 'src/a.ts',
|
|
32
|
+
line: 4,
|
|
33
|
+
risk: { impact: 'low', likelihood: 'unknown', confidence: 'medium', action: 'optional' },
|
|
34
|
+
}),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const SAMPLE_DIFF = `--- a/src/a.ts
|
|
38
|
+
+++ b/src/a.ts
|
|
39
|
+
@@ -1,3 +1,4 @@
|
|
40
|
+
line1
|
|
41
|
+
-old
|
|
42
|
+
+new
|
|
43
|
+
+added
|
|
44
|
+
line3
|
|
45
|
+
`
|
|
46
|
+
|
|
47
|
+
test('empty findings renders the page shell with an empty-state message', () => {
|
|
26
48
|
const html = renderFindingsHtml({ findings: [], postStatus: {} })
|
|
49
|
+
expect(html).toContain('class="pr-header"')
|
|
27
50
|
expect(html).toContain('No findings')
|
|
28
51
|
})
|
|
29
52
|
|
|
30
|
-
test('
|
|
31
|
-
const html = renderFindingsHtml({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
expect(html).toContain('null deref')
|
|
36
|
-
expect(html).toContain('sev-high')
|
|
37
|
-
expect(html).toContain('data-finding-id="a"')
|
|
38
|
-
expect(html).toContain('data-action="post"')
|
|
53
|
+
test('emits the two-tab segmented control with Files and All Issues buttons', () => {
|
|
54
|
+
const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
|
|
55
|
+
expect(html).toContain('data-action="set-view"')
|
|
56
|
+
expect(html).toContain('data-view="files"')
|
|
57
|
+
expect(html).toContain('data-view="all-issues"')
|
|
39
58
|
})
|
|
40
59
|
|
|
41
|
-
test('
|
|
42
|
-
const html = renderFindingsHtml({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
expect(html).toContain('
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('shows failed badge with message', () => {
|
|
50
|
-
const html = renderFindingsHtml({
|
|
51
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
52
|
-
postStatus: { a: { status: 'failed', message: 'rate limit' } },
|
|
53
|
-
})
|
|
54
|
-
expect(html).toContain('class="badge failed"')
|
|
55
|
-
expect(html).toContain('rate limit')
|
|
60
|
+
test('sets body data attributes for default view, diff-mode, and suggestions visibility', () => {
|
|
61
|
+
const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
|
|
62
|
+
expect(html).toContain('data-page="findings"')
|
|
63
|
+
expect(html).toContain('data-view="files"')
|
|
64
|
+
expect(html).toContain('data-diff-mode="unified"')
|
|
65
|
+
expect(html).toContain('data-show-suggestions="false"')
|
|
56
66
|
})
|
|
57
67
|
|
|
58
|
-
test('renders
|
|
68
|
+
test('renders one file pane per PrFileEntry, hidden by default', () => {
|
|
59
69
|
const html = renderFindingsHtml({
|
|
60
|
-
findings:
|
|
61
|
-
f({
|
|
62
|
-
id: 'a',
|
|
63
|
-
title: 'x',
|
|
64
|
-
suggestion: { body: 'if (!y) return', startLine: 10, endLine: 11 },
|
|
65
|
-
}),
|
|
66
|
-
],
|
|
70
|
+
findings: SAMPLE_FINDINGS,
|
|
67
71
|
postStatus: {},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
expect(html).toContain('class="finding-suggestion-wrap"')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test('parses labelled paragraphs into section headers', () => {
|
|
75
|
-
const html = renderFindingsHtml({
|
|
76
|
-
findings: [
|
|
77
|
-
f({
|
|
78
|
-
id: 'a',
|
|
79
|
-
title: 'unsafe userId',
|
|
80
|
-
description:
|
|
81
|
-
'Observation: trusts client-supplied userId.\n\nWhy it matters: cross-tenant exposure.\n\nSuggested direction: read from auth context.',
|
|
82
|
-
}),
|
|
72
|
+
files: [
|
|
73
|
+
{ path: 'src/a.ts', additions: 3, deletions: 1 },
|
|
74
|
+
{ path: 'src/b.ts', additions: 1, deletions: 0 },
|
|
83
75
|
],
|
|
84
|
-
|
|
76
|
+
diff: SAMPLE_DIFF,
|
|
85
77
|
})
|
|
86
|
-
expect(html).toContain('
|
|
87
|
-
expect(html).toContain('
|
|
88
|
-
expect(html).
|
|
89
|
-
expect(html).toContain('>Observation</h4>')
|
|
90
|
-
expect(html).toContain('>Why it matters</h4>')
|
|
91
|
-
expect(html).toContain('>Suggested direction</h4>')
|
|
92
|
-
expect(html).toContain('>trusts client-supplied userId.</p>')
|
|
78
|
+
expect(html).toContain('data-file-pane="src/a.ts"')
|
|
79
|
+
expect(html).toContain('data-file-pane="src/b.ts"')
|
|
80
|
+
expect(html).toMatch(/data-file-pane="src\/a\.ts"[^>]*hidden/)
|
|
93
81
|
})
|
|
94
82
|
|
|
95
|
-
test('renders
|
|
96
|
-
const html = renderFindingsHtml({
|
|
97
|
-
|
|
98
|
-
f({
|
|
99
|
-
id: 'a',
|
|
100
|
-
title: 'x',
|
|
101
|
-
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'should-fix' },
|
|
102
|
-
}),
|
|
103
|
-
],
|
|
104
|
-
postStatus: {},
|
|
105
|
-
})
|
|
106
|
-
expect(html).toContain('class="finding-risk"')
|
|
107
|
-
expect(html).toContain('Impact <strong>high</strong>')
|
|
108
|
-
expect(html).toContain('Likelihood <strong>likely</strong>')
|
|
109
|
-
expect(html).toContain('Confidence <strong>high</strong>')
|
|
110
|
-
expect(html).toContain('Action <strong>should-fix</strong>')
|
|
83
|
+
test('renders an Overview pane mapped to data-file-pane=""', () => {
|
|
84
|
+
const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
|
|
85
|
+
expect(html).toContain('data-file-pane=""')
|
|
111
86
|
})
|
|
112
87
|
|
|
113
|
-
test('
|
|
88
|
+
test('renders a per-file toolbar with breadcrumb, deltas, finding-nav and diff-mode toggle', () => {
|
|
114
89
|
const html = renderFindingsHtml({
|
|
115
|
-
findings:
|
|
90
|
+
findings: SAMPLE_FINDINGS,
|
|
116
91
|
postStatus: {},
|
|
92
|
+
files: [{ path: 'src/a.ts', additions: 3, deletions: 1 }],
|
|
93
|
+
diff: SAMPLE_DIFF,
|
|
117
94
|
})
|
|
118
|
-
expect(html).toContain('
|
|
119
|
-
expect(html).toContain('
|
|
95
|
+
expect(html).toContain('+3')
|
|
96
|
+
expect(html).toContain('-1')
|
|
97
|
+
expect(html).toContain('data-action="prev-finding"')
|
|
98
|
+
expect(html).toContain('data-action="next-finding"')
|
|
99
|
+
expect(html).toContain('data-action="set-diff-mode"')
|
|
100
|
+
expect(html).toContain('data-mode="unified"')
|
|
101
|
+
expect(html).toContain('data-mode="split"')
|
|
120
102
|
})
|
|
121
103
|
|
|
122
|
-
test('
|
|
104
|
+
test('renders both unified and split diff containers per file', () => {
|
|
123
105
|
const html = renderFindingsHtml({
|
|
124
|
-
findings:
|
|
106
|
+
findings: SAMPLE_FINDINGS,
|
|
125
107
|
postStatus: {},
|
|
108
|
+
files: [{ path: 'src/a.ts', additions: 3, deletions: 1 }],
|
|
109
|
+
diff: SAMPLE_DIFF,
|
|
126
110
|
})
|
|
127
|
-
expect(html).toContain('
|
|
128
|
-
expect(html).toContain('
|
|
111
|
+
expect(html).toContain('data-diff-mode="unified"')
|
|
112
|
+
expect(html).toContain('data-diff-mode="split"')
|
|
129
113
|
})
|
|
130
114
|
|
|
131
|
-
test('
|
|
115
|
+
test('places an inline annotation at the affected diff line', () => {
|
|
132
116
|
const html = renderFindingsHtml({
|
|
133
|
-
findings:
|
|
134
|
-
f({ id: 'a', title: 't1', severity: 'high' }),
|
|
135
|
-
f({ id: 'b', title: 't2', severity: 'high' }),
|
|
136
|
-
f({ id: 'c', title: 't3', severity: 'low' }),
|
|
137
|
-
],
|
|
117
|
+
findings: SAMPLE_FINDINGS,
|
|
138
118
|
postStatus: {},
|
|
119
|
+
files: [{ path: 'src/a.ts', additions: 3, deletions: 1 }],
|
|
120
|
+
diff: SAMPLE_DIFF,
|
|
139
121
|
})
|
|
140
|
-
expect(html).toContain('
|
|
141
|
-
expect(html).toContain('
|
|
142
|
-
expect(html).toContain('1</span> low')
|
|
122
|
+
expect(html).toContain('data-finding-id="a"')
|
|
123
|
+
expect(html).toContain('class="annot')
|
|
143
124
|
})
|
|
144
125
|
|
|
145
|
-
test('
|
|
146
|
-
const html = renderFindingsHtml({
|
|
147
|
-
|
|
148
|
-
postStatus: {},
|
|
149
|
-
})
|
|
150
|
-
expect(html).toMatch(/<button[^>]*data-action="post"[^>]*disabled/)
|
|
126
|
+
test('mounts the All Issues pane', () => {
|
|
127
|
+
const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
|
|
128
|
+
expect(html).toContain('data-role="issues-list"')
|
|
151
129
|
})
|
|
152
130
|
|
|
153
|
-
test('
|
|
154
|
-
const html = renderFindingsHtml({
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
f({ id: 'b', title: 't2', severity: 'high', domain: 'security' }),
|
|
158
|
-
f({ id: 'c', title: 't3', severity: 'low', domain: 'bugs' }),
|
|
159
|
-
],
|
|
160
|
-
postStatus: {},
|
|
161
|
-
})
|
|
162
|
-
// The bar
|
|
163
|
-
expect(html).toContain('class="filter-bar"')
|
|
164
|
-
// Search input
|
|
165
|
-
expect(html).toMatch(/<input[^>]+type="search"[^>]+data-role="search"/)
|
|
166
|
-
// Severity chips with counts
|
|
167
|
-
expect(html).toMatch(/data-filter-group="sev"[^>]+data-filter-value="high"/)
|
|
168
|
-
expect(html).toMatch(/data-filter-group="sev"[^>]+data-filter-value="low"/)
|
|
169
|
-
// Domain chips
|
|
170
|
-
expect(html).toMatch(/data-filter-group="domain"[^>]+data-filter-value="bugs"/)
|
|
171
|
-
expect(html).toMatch(/data-filter-group="domain"[^>]+data-filter-value="security"/)
|
|
172
|
-
// Each chip carries its current count
|
|
173
|
-
expect(html).toMatch(/<span class="filter-chip-count">2<\/span>/) // high count
|
|
174
|
-
expect(html).toMatch(/<span class="filter-chip-count">1<\/span>/) // low count
|
|
131
|
+
test('mounts the action bar at the page bottom', () => {
|
|
132
|
+
const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
|
|
133
|
+
expect(html).toContain('data-role="action-bar"')
|
|
134
|
+
expect(html).toContain('Post Recommended')
|
|
175
135
|
})
|
|
176
136
|
|
|
177
|
-
test('
|
|
137
|
+
test('marks posted findings with data-posted="true"', () => {
|
|
178
138
|
const html = renderFindingsHtml({
|
|
179
|
-
findings:
|
|
180
|
-
postStatus: {},
|
|
139
|
+
findings: SAMPLE_FINDINGS,
|
|
140
|
+
postStatus: { a: 'posted' },
|
|
181
141
|
})
|
|
182
|
-
expect(html).
|
|
183
|
-
expect(html).toMatch(/data-action="select-priority"/)
|
|
184
|
-
expect(html).toMatch(/data-action="select-none"/)
|
|
185
|
-
expect(html).toContain('class="kbd-hint"')
|
|
186
|
-
expect(html).toContain('<kbd>j</kbd>')
|
|
187
|
-
expect(html).toContain('<kbd>/</kbd>')
|
|
142
|
+
expect(html).toContain('data-posted="true"')
|
|
188
143
|
})
|
|
189
144
|
|
|
190
|
-
test('
|
|
145
|
+
test('renders the suggestion code block in the annotation', () => {
|
|
191
146
|
const html = renderFindingsHtml({
|
|
192
|
-
findings: [
|
|
147
|
+
findings: [
|
|
148
|
+
f({
|
|
149
|
+
id: 'x',
|
|
150
|
+
title: 'with patch',
|
|
151
|
+
suggestion: { body: 'if (!y) return', startLine: 10, endLine: 11 },
|
|
152
|
+
}),
|
|
153
|
+
],
|
|
193
154
|
postStatus: {},
|
|
194
|
-
runId: 'pr-42-1700000000',
|
|
195
155
|
})
|
|
196
|
-
expect(html).
|
|
156
|
+
expect(html).toContain('if (!y) return')
|
|
157
|
+
expect(html).toContain('Suggested change')
|
|
197
158
|
})
|
|
198
159
|
|
|
199
|
-
test('
|
|
160
|
+
test('parses labelled paragraphs into section headers', () => {
|
|
200
161
|
const html = renderFindingsHtml({
|
|
201
162
|
findings: [
|
|
202
163
|
f({
|
|
203
164
|
id: 'a',
|
|
204
|
-
title: '
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
file: 'src/main.ts',
|
|
208
|
-
description: 'fire-and-forget promise',
|
|
165
|
+
title: 'unsafe userId',
|
|
166
|
+
description:
|
|
167
|
+
'Observation: client picks userId.\n\nWhy it matters: leak risk.\n\nSuggested direction: drop arg.',
|
|
209
168
|
}),
|
|
210
169
|
],
|
|
211
170
|
postStatus: {},
|
|
212
171
|
})
|
|
213
|
-
expect(html).
|
|
214
|
-
expect(html).
|
|
215
|
-
expect(html).
|
|
216
|
-
expect(html).toMatch(/data-search-text="[^"]*race in handler[^"]*fire-and-forget promise/)
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
test('post button label is "Post to PR" and triggers the confirm flow', () => {
|
|
220
|
-
const html = renderFindingsHtml({
|
|
221
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
222
|
-
postStatus: {},
|
|
223
|
-
})
|
|
224
|
-
expect(html).toMatch(/<button[^>]*data-action="post"[^>]*>Post to PR<\/button>/)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
test('renders the inline confirm popup (hidden until user clicks Post to PR)', () => {
|
|
228
|
-
const html = renderFindingsHtml({
|
|
229
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
230
|
-
postStatus: {},
|
|
231
|
-
})
|
|
232
|
-
// It is anchored to the Post to PR button, not a separate row.
|
|
233
|
-
expect(html).toContain('class="post-button-group"')
|
|
234
|
-
expect(html).toMatch(/<div\s+class="confirm-popup"\s+data-role="confirm-bar"[^>]*hidden[^>]*>/)
|
|
235
|
-
expect(html).toContain('data-action="cancel-post"')
|
|
236
|
-
expect(html).toContain('data-action="confirm-post"')
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
test('lede explains the server-side post flow (not the terminal reply)', () => {
|
|
240
|
-
const html = renderFindingsHtml({
|
|
241
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
242
|
-
postStatus: {},
|
|
243
|
-
})
|
|
244
|
-
expect(html).toMatch(/Post to PR/)
|
|
245
|
-
expect(html).toMatch(/<code>gh<\/code>/)
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
test('renders a polite submit-status region for transient queue/error feedback', () => {
|
|
249
|
-
const html = renderFindingsHtml({
|
|
250
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
251
|
-
postStatus: {},
|
|
252
|
-
})
|
|
253
|
-
expect(html).toMatch(
|
|
254
|
-
/<p\s+class="submit-status"\s+data-role="submit-status"\s+role="status"\s+aria-live="polite"\s+hidden>/,
|
|
255
|
-
)
|
|
172
|
+
expect(html).toContain('Observation')
|
|
173
|
+
expect(html).toContain('Why it matters')
|
|
174
|
+
expect(html).toContain('Suggested direction')
|
|
256
175
|
})
|
|
257
176
|
|
|
258
|
-
test('renders
|
|
177
|
+
test('renders pr meta when supplied', () => {
|
|
259
178
|
const html = renderFindingsHtml({
|
|
260
|
-
findings:
|
|
179
|
+
findings: SAMPLE_FINDINGS,
|
|
261
180
|
postStatus: {},
|
|
181
|
+
pr: { number: 42, branch: 'feat/x', headSha: 'deadbeefdeadbeef0000111122223333' },
|
|
262
182
|
})
|
|
263
|
-
expect(html).
|
|
183
|
+
expect(html).toContain('PR #42')
|
|
184
|
+
expect(html).toContain('feat/x')
|
|
185
|
+
expect(html).toContain('deadbeefdead')
|
|
264
186
|
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderIssuesList } from '../render-issues-list.ts'
|
|
3
|
+
import type { ReviewFinding } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
const findings: ReviewFinding[] = [
|
|
6
|
+
{
|
|
7
|
+
id: '1',
|
|
8
|
+
file: 'a.ts',
|
|
9
|
+
line: 1,
|
|
10
|
+
severity: 'blocker',
|
|
11
|
+
title: 'A',
|
|
12
|
+
description: 'd',
|
|
13
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'must-fix' },
|
|
14
|
+
domain: 'security',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: '2',
|
|
18
|
+
file: 'b.ts',
|
|
19
|
+
line: 1,
|
|
20
|
+
severity: 'high',
|
|
21
|
+
title: 'B',
|
|
22
|
+
description: 'd',
|
|
23
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'should-fix' },
|
|
24
|
+
domain: 'bugs',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: '3',
|
|
28
|
+
file: 'c.ts',
|
|
29
|
+
line: 1,
|
|
30
|
+
severity: 'low',
|
|
31
|
+
title: 'C',
|
|
32
|
+
description: 'd',
|
|
33
|
+
risk: { impact: 'low', likelihood: 'unknown', confidence: 'medium', action: 'optional' },
|
|
34
|
+
domain: 'code-smells',
|
|
35
|
+
},
|
|
36
|
+
] as ReviewFinding[]
|
|
37
|
+
|
|
38
|
+
describe('renderIssuesList', () => {
|
|
39
|
+
test('renders an issue-card per finding', () => {
|
|
40
|
+
const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
|
|
41
|
+
expect((html.match(/class="issue-card/g) ?? []).length).toBe(3)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('renders severity filter pills with counts', () => {
|
|
45
|
+
const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
|
|
46
|
+
expect(html).toContain('Blocker (1)')
|
|
47
|
+
expect(html).toContain('High (1)')
|
|
48
|
+
expect(html).toContain('Low (1)')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('emits "N should review" with actionable count only', () => {
|
|
52
|
+
const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
|
|
53
|
+
expect(html).toContain('2 should review')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('emits "Show N suggestions" toggle when suggestions present', () => {
|
|
57
|
+
const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
|
|
58
|
+
expect(html).toContain('Show 1 suggestion')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('suggestion finding has data-suggestion="true"', () => {
|
|
62
|
+
const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
|
|
63
|
+
expect(html).toMatch(
|
|
64
|
+
/data-finding-id="3"[^>]*data-suggestion="true"|data-suggestion="true"[^>]*data-finding-id="3"/,
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
})
|