@iceinvein/agent-skills 0.1.25 → 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/fixtures/example-pr/diff.patch +86 -0
- package/skills/magpie/fixtures/example-pr/pr.json +13 -1
- package/skills/magpie/fixtures/fake-gh.sh +10 -1
- package/skills/magpie/package.json +1 -1
- 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 +3 -3
- 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 -229
- package/skills/magpie/scripts/__tests__/render-issues-list.test.ts +67 -0
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +43 -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 +24 -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 +14 -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 +151 -341
- package/skills/magpie/scripts/render-issues-list.ts +78 -0
- package/skills/magpie/scripts/render-progress.ts +44 -104
- package/skills/magpie/scripts/server.ts +43 -0
- package/skills/magpie/scripts/types.ts +13 -0
- package/skills/magpie/skill.json +1 -1
- package/skills/magpie/templates/styles.css +1082 -965
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderAnnotation } from '../render-annotation.ts'
|
|
3
|
+
import type { ReviewFinding } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
const finding: ReviewFinding = {
|
|
6
|
+
id: 'f-1',
|
|
7
|
+
file: 'src/a.ts',
|
|
8
|
+
line: 42,
|
|
9
|
+
severity: 'high',
|
|
10
|
+
title: 'Race in cache.refresh()',
|
|
11
|
+
description: 'Observation: two refreshes overlap.\n\nWhy it matters: stale data.',
|
|
12
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'should-fix' },
|
|
13
|
+
domain: 'bugs',
|
|
14
|
+
suggestion: { body: 'await lock.acquire()', startLine: 42, endLine: 42 },
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('renderAnnotation', () => {
|
|
18
|
+
test('emits a div with data-finding-id and severity class', () => {
|
|
19
|
+
const html = renderAnnotation(finding, { checked: false, posted: false, asCard: false })
|
|
20
|
+
expect(html).toContain(`data-finding-id="f-1"`)
|
|
21
|
+
expect(html).toContain('annot')
|
|
22
|
+
expect(html).toContain('data-severity="high"')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('renders severity label, domain chip, and title', () => {
|
|
26
|
+
const html = renderAnnotation(finding, { checked: false, posted: false, asCard: false })
|
|
27
|
+
expect(html).toContain('HIGH')
|
|
28
|
+
expect(html).toContain('Bugs')
|
|
29
|
+
expect(html).toContain('Race in cache.refresh()')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('renders parsed description sections', () => {
|
|
33
|
+
const html = renderAnnotation(finding, { checked: false, posted: false, asCard: false })
|
|
34
|
+
expect(html).toContain('Observation')
|
|
35
|
+
expect(html).toContain('two refreshes overlap')
|
|
36
|
+
expect(html).toContain('Why it matters')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('renders the suggestion code block when present', () => {
|
|
40
|
+
const html = renderAnnotation(finding, { checked: false, posted: false, asCard: false })
|
|
41
|
+
expect(html).toContain('<pre')
|
|
42
|
+
expect(html).toContain('await lock.acquire()')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('renders risk dimensions footer', () => {
|
|
46
|
+
const html = renderAnnotation(finding, { checked: false, posted: false, asCard: false })
|
|
47
|
+
expect(html).toContain('Impact: <span class="tag">high</span>')
|
|
48
|
+
expect(html).toContain('Likelihood: <span class="tag">likely</span>')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('marks posted findings and disables checkbox', () => {
|
|
52
|
+
const html = renderAnnotation(finding, { checked: true, posted: true, asCard: false })
|
|
53
|
+
expect(html).toContain('data-posted="true"')
|
|
54
|
+
expect(html).toContain('checked disabled')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('marks suggestion-only findings with data-suggestion="true"', () => {
|
|
58
|
+
const suggestion: ReviewFinding = { ...finding, risk: { ...finding.risk, action: 'optional' } }
|
|
59
|
+
const html = renderAnnotation(suggestion, { checked: false, posted: false, asCard: false })
|
|
60
|
+
expect(html).toContain('data-suggestion="true"')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('asCard=true wraps in .issue-card', () => {
|
|
64
|
+
const html = renderAnnotation(finding, { checked: false, posted: false, asCard: true })
|
|
65
|
+
expect(html).toContain('class="issue-card')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -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,290 +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('
|
|
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"')
|
|
47
66
|
})
|
|
48
67
|
|
|
49
|
-
test('
|
|
68
|
+
test('renders one file pane per PrFileEntry, hidden by default', () => {
|
|
50
69
|
const html = renderFindingsHtml({
|
|
51
|
-
findings:
|
|
52
|
-
postStatus: { a: { status: 'failed', message: 'rate limit' } },
|
|
53
|
-
})
|
|
54
|
-
expect(html).toContain('class="badge failed"')
|
|
55
|
-
expect(html).toContain('rate limit')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('renders suggestion block when present, with a "Suggested change" caption', () => {
|
|
59
|
-
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
|
-
// The row is a <dl> of risk-dim cells so screen readers can read it as
|
|
107
|
-
// term/definition pairs. Each cell carries the dimension as a <dt> and
|
|
108
|
-
// the value as a <strong>; action also picks up a leading glyph that
|
|
109
|
-
// signals must-fix vs should-fix vs consider at a glance.
|
|
110
|
-
expect(html).toContain('class="finding-risk"')
|
|
111
|
-
expect(html).toMatch(/<dt>Impact<\/dt>\s*<strong>high<\/strong>/)
|
|
112
|
-
expect(html).toMatch(/<dt>Likelihood<\/dt>\s*<strong>likely<\/strong>/)
|
|
113
|
-
expect(html).toMatch(/<dt>Confidence<\/dt>\s*<strong>high<\/strong>/)
|
|
114
|
-
expect(html).toMatch(/<dt>Action<\/dt>\s*<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=""')
|
|
115
86
|
})
|
|
116
87
|
|
|
117
|
-
test('
|
|
88
|
+
test('renders a per-file toolbar with breadcrumb, deltas, finding-nav and diff-mode toggle', () => {
|
|
118
89
|
const html = renderFindingsHtml({
|
|
119
|
-
findings:
|
|
90
|
+
findings: SAMPLE_FINDINGS,
|
|
120
91
|
postStatus: {},
|
|
92
|
+
files: [{ path: 'src/a.ts', additions: 3, deletions: 1 }],
|
|
93
|
+
diff: SAMPLE_DIFF,
|
|
121
94
|
})
|
|
122
|
-
expect(html).toContain('
|
|
123
|
-
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"')
|
|
124
102
|
})
|
|
125
103
|
|
|
126
|
-
test('
|
|
104
|
+
test('renders both unified and split diff containers per file', () => {
|
|
127
105
|
const html = renderFindingsHtml({
|
|
128
|
-
findings:
|
|
106
|
+
findings: SAMPLE_FINDINGS,
|
|
129
107
|
postStatus: {},
|
|
108
|
+
files: [{ path: 'src/a.ts', additions: 3, deletions: 1 }],
|
|
109
|
+
diff: SAMPLE_DIFF,
|
|
130
110
|
})
|
|
131
|
-
expect(html).toContain('
|
|
132
|
-
expect(html).toContain('
|
|
111
|
+
expect(html).toContain('data-diff-mode="unified"')
|
|
112
|
+
expect(html).toContain('data-diff-mode="split"')
|
|
133
113
|
})
|
|
134
114
|
|
|
135
|
-
test('
|
|
115
|
+
test('places an inline annotation at the affected diff line', () => {
|
|
136
116
|
const html = renderFindingsHtml({
|
|
137
|
-
findings:
|
|
138
|
-
f({ id: 'a', title: 't1', severity: 'high' }),
|
|
139
|
-
f({ id: 'b', title: 't2', severity: 'high' }),
|
|
140
|
-
f({ id: 'c', title: 't3', severity: 'low' }),
|
|
141
|
-
],
|
|
117
|
+
findings: SAMPLE_FINDINGS,
|
|
142
118
|
postStatus: {},
|
|
119
|
+
files: [{ path: 'src/a.ts', additions: 3, deletions: 1 }],
|
|
120
|
+
diff: SAMPLE_DIFF,
|
|
143
121
|
})
|
|
144
|
-
expect(html).toContain('
|
|
145
|
-
expect(html).toContain('
|
|
146
|
-
expect(html).toContain('1</span> low')
|
|
122
|
+
expect(html).toContain('data-finding-id="a"')
|
|
123
|
+
expect(html).toContain('class="annot')
|
|
147
124
|
})
|
|
148
125
|
|
|
149
|
-
test('
|
|
150
|
-
const html = renderFindingsHtml({
|
|
151
|
-
|
|
152
|
-
postStatus: {},
|
|
153
|
-
})
|
|
154
|
-
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"')
|
|
155
129
|
})
|
|
156
130
|
|
|
157
|
-
test('
|
|
158
|
-
const html = renderFindingsHtml({
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
f({ id: 'b', title: 't2', severity: 'high', domain: 'security' }),
|
|
162
|
-
f({ id: 'c', title: 't3', severity: 'low', domain: 'bugs' }),
|
|
163
|
-
],
|
|
164
|
-
postStatus: {},
|
|
165
|
-
})
|
|
166
|
-
// The bar
|
|
167
|
-
expect(html).toContain('class="filter-bar"')
|
|
168
|
-
// Search input
|
|
169
|
-
expect(html).toMatch(/<input[^>]+type="search"[^>]+data-role="search"/)
|
|
170
|
-
// Severity chips with counts
|
|
171
|
-
expect(html).toMatch(/data-filter-group="sev"[^>]+data-filter-value="high"/)
|
|
172
|
-
expect(html).toMatch(/data-filter-group="sev"[^>]+data-filter-value="low"/)
|
|
173
|
-
// Domain chips
|
|
174
|
-
expect(html).toMatch(/data-filter-group="domain"[^>]+data-filter-value="bugs"/)
|
|
175
|
-
expect(html).toMatch(/data-filter-group="domain"[^>]+data-filter-value="security"/)
|
|
176
|
-
// Each chip carries its current count
|
|
177
|
-
expect(html).toMatch(/<span class="filter-chip-count">2<\/span>/) // high count
|
|
178
|
-
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')
|
|
179
135
|
})
|
|
180
136
|
|
|
181
|
-
test('
|
|
137
|
+
test('marks posted findings with data-posted="true"', () => {
|
|
182
138
|
const html = renderFindingsHtml({
|
|
183
|
-
findings:
|
|
184
|
-
postStatus: {},
|
|
139
|
+
findings: SAMPLE_FINDINGS,
|
|
140
|
+
postStatus: { a: 'posted' },
|
|
185
141
|
})
|
|
186
|
-
expect(html).
|
|
187
|
-
expect(html).toMatch(/data-action="select-priority"/)
|
|
188
|
-
expect(html).toMatch(/data-action="select-none"/)
|
|
189
|
-
expect(html).toContain('class="kbd-hint"')
|
|
190
|
-
expect(html).toContain('<kbd>j</kbd>')
|
|
191
|
-
expect(html).toContain('<kbd>/</kbd>')
|
|
142
|
+
expect(html).toContain('data-posted="true"')
|
|
192
143
|
})
|
|
193
144
|
|
|
194
|
-
test('
|
|
145
|
+
test('renders the suggestion code block in the annotation', () => {
|
|
195
146
|
const html = renderFindingsHtml({
|
|
196
|
-
findings: [
|
|
147
|
+
findings: [
|
|
148
|
+
f({
|
|
149
|
+
id: 'x',
|
|
150
|
+
title: 'with patch',
|
|
151
|
+
suggestion: { body: 'if (!y) return', startLine: 10, endLine: 11 },
|
|
152
|
+
}),
|
|
153
|
+
],
|
|
197
154
|
postStatus: {},
|
|
198
|
-
runId: 'pr-42-1700000000',
|
|
199
155
|
})
|
|
200
|
-
expect(html).
|
|
156
|
+
expect(html).toContain('if (!y) return')
|
|
157
|
+
expect(html).toContain('Suggested change')
|
|
201
158
|
})
|
|
202
159
|
|
|
203
|
-
test('
|
|
160
|
+
test('parses labelled paragraphs into section headers', () => {
|
|
204
161
|
const html = renderFindingsHtml({
|
|
205
162
|
findings: [
|
|
206
163
|
f({
|
|
207
164
|
id: 'a',
|
|
208
|
-
title: '
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
file: 'src/main.ts',
|
|
212
|
-
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.',
|
|
213
168
|
}),
|
|
214
169
|
],
|
|
215
170
|
postStatus: {},
|
|
216
171
|
})
|
|
217
|
-
expect(html).
|
|
218
|
-
expect(html).
|
|
219
|
-
expect(html).
|
|
220
|
-
expect(html).toMatch(/data-search-text="[^"]*race in handler[^"]*fire-and-forget promise/)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
test('post button label is "Post to PR" and triggers the confirm flow', () => {
|
|
224
|
-
const html = renderFindingsHtml({
|
|
225
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
226
|
-
postStatus: {},
|
|
227
|
-
})
|
|
228
|
-
expect(html).toMatch(/<button[^>]*data-action="post"[^>]*>Post to PR<\/button>/)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
test('renders the inline confirm popup (hidden until user clicks Post to PR)', () => {
|
|
232
|
-
const html = renderFindingsHtml({
|
|
233
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
234
|
-
postStatus: {},
|
|
235
|
-
})
|
|
236
|
-
// It is anchored to the Post to PR button, not a separate row.
|
|
237
|
-
expect(html).toContain('class="post-button-group"')
|
|
238
|
-
expect(html).toMatch(/<div\s+class="confirm-popup"\s+data-role="confirm-bar"[^>]*hidden[^>]*>/)
|
|
239
|
-
expect(html).toContain('data-action="cancel-post"')
|
|
240
|
-
expect(html).toContain('data-action="confirm-post"')
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
test('lede explains the server-side post flow (not the terminal reply)', () => {
|
|
244
|
-
const html = renderFindingsHtml({
|
|
245
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
246
|
-
postStatus: {},
|
|
247
|
-
})
|
|
248
|
-
expect(html).toMatch(/Post to PR/)
|
|
249
|
-
expect(html).toMatch(/<code>gh<\/code>/)
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
test('renders a polite submit-status region for transient queue/error feedback', () => {
|
|
253
|
-
const html = renderFindingsHtml({
|
|
254
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
255
|
-
postStatus: {},
|
|
256
|
-
})
|
|
257
|
-
expect(html).toMatch(
|
|
258
|
-
/<p\s+class="submit-status"\s+data-role="submit-status"\s+role="status"\s+aria-live="polite"\s+hidden>/,
|
|
259
|
-
)
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
test('renders a no-matches placeholder for the client-side filter to reveal', () => {
|
|
263
|
-
const html = renderFindingsHtml({
|
|
264
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
265
|
-
postStatus: {},
|
|
266
|
-
})
|
|
267
|
-
expect(html).toMatch(/data-role="no-matches"[^>]+hidden/)
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
test('renders PR meta in the page header when pr is provided', () => {
|
|
271
|
-
const html = renderFindingsHtml({
|
|
272
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
273
|
-
postStatus: {},
|
|
274
|
-
pr: { number: 2048, branch: 'feature-x', headSha: '0123456789abdeadbeef' },
|
|
275
|
-
})
|
|
276
|
-
expect(html).toContain('class="pr-meta"')
|
|
277
|
-
expect(html).toContain('PR #2048')
|
|
278
|
-
expect(html).toContain('feature-x')
|
|
279
|
-
// sha is truncated to 12 chars to match the progress page
|
|
280
|
-
expect(html).toContain('0123456789ab')
|
|
281
|
-
expect(html).not.toContain('deadbeef')
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
test('omits the pr-meta block when no pr is provided', () => {
|
|
285
|
-
const html = renderFindingsHtml({
|
|
286
|
-
findings: [f({ id: 'a', title: 'x' })],
|
|
287
|
-
postStatus: {},
|
|
288
|
-
})
|
|
289
|
-
expect(html).not.toContain('class="pr-meta"')
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
test('finding titles render as h2 so the heading outline does not skip from h1', () => {
|
|
293
|
-
const html = renderFindingsHtml({
|
|
294
|
-
findings: [f({ id: 'a', title: 'race in handler' })],
|
|
295
|
-
postStatus: {},
|
|
296
|
-
})
|
|
297
|
-
expect(html).toMatch(/<h2[^>]*class="finding-title"[^>]*>race in handler<\/h2>/)
|
|
298
|
-
// And the findings list has a visually-hidden h2 to bridge the outline.
|
|
299
|
-
expect(html).toContain('class="findings-list-heading"')
|
|
172
|
+
expect(html).toContain('Observation')
|
|
173
|
+
expect(html).toContain('Why it matters')
|
|
174
|
+
expect(html).toContain('Suggested direction')
|
|
300
175
|
})
|
|
301
176
|
|
|
302
|
-
test('
|
|
303
|
-
// parseFindingDescription returns [] only when the description is empty
|
|
304
|
-
// after normalization. The renderer wraps that in a fallback Observation
|
|
305
|
-
// section so the card never shows up with no prose between anchor and risk.
|
|
177
|
+
test('renders pr meta when supplied', () => {
|
|
306
178
|
const html = renderFindingsHtml({
|
|
307
|
-
findings:
|
|
179
|
+
findings: SAMPLE_FINDINGS,
|
|
308
180
|
postStatus: {},
|
|
181
|
+
pr: { number: 42, branch: 'feat/x', headSha: 'deadbeefdeadbeef0000111122223333' },
|
|
309
182
|
})
|
|
310
|
-
expect(html).toContain('
|
|
183
|
+
expect(html).toContain('PR #42')
|
|
184
|
+
expect(html).toContain('feat/x')
|
|
185
|
+
expect(html).toContain('deadbeefdead')
|
|
311
186
|
})
|