@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.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/skills/magpie/fixtures/example-pr/diff.patch +86 -0
  4. package/skills/magpie/fixtures/example-pr/pr.json +13 -1
  5. package/skills/magpie/fixtures/fake-gh.sh +10 -1
  6. package/skills/magpie/package.json +1 -1
  7. package/skills/magpie/scripts/__tests__/diff-utils.test.ts +106 -0
  8. package/skills/magpie/scripts/__tests__/file-tree.test.ts +97 -0
  9. package/skills/magpie/scripts/__tests__/gh.test.ts +8 -0
  10. package/skills/magpie/scripts/__tests__/helper.test.ts +63 -0
  11. package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +10 -0
  12. package/skills/magpie/scripts/__tests__/post-cmd.test.ts +90 -1
  13. package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +3 -3
  14. package/skills/magpie/scripts/__tests__/refresh.test.ts +2 -2
  15. package/skills/magpie/scripts/__tests__/render-action-bar.test.ts +66 -0
  16. package/skills/magpie/scripts/__tests__/render-annotation.test.ts +67 -0
  17. package/skills/magpie/scripts/__tests__/render-cmd.test.ts +46 -0
  18. package/skills/magpie/scripts/__tests__/render-diff.test.ts +84 -0
  19. package/skills/magpie/scripts/__tests__/render-file-tree.test.ts +54 -0
  20. package/skills/magpie/scripts/__tests__/render-findings.test.ts +104 -229
  21. package/skills/magpie/scripts/__tests__/render-issues-list.test.ts +67 -0
  22. package/skills/magpie/scripts/__tests__/render-progress.test.ts +43 -0
  23. package/skills/magpie/scripts/__tests__/server.test.ts +54 -0
  24. package/skills/magpie/scripts/__tests__/types.test.ts +48 -3
  25. package/skills/magpie/scripts/diff-utils.ts +129 -0
  26. package/skills/magpie/scripts/file-tree.ts +74 -0
  27. package/skills/magpie/scripts/gh.ts +1 -0
  28. package/skills/magpie/scripts/helper.js +402 -192
  29. package/skills/magpie/scripts/post-cmd.ts +202 -0
  30. package/skills/magpie/scripts/preview-cmd.ts +24 -1
  31. package/skills/magpie/scripts/render-action-bar.ts +51 -0
  32. package/skills/magpie/scripts/render-annotation.ts +96 -0
  33. package/skills/magpie/scripts/render-cmd.ts +14 -1
  34. package/skills/magpie/scripts/render-diff.ts +125 -0
  35. package/skills/magpie/scripts/render-file-tree.ts +86 -0
  36. package/skills/magpie/scripts/render-findings.ts +151 -341
  37. package/skills/magpie/scripts/render-issues-list.ts +78 -0
  38. package/skills/magpie/scripts/render-progress.ts +44 -104
  39. package/skills/magpie/scripts/server.ts +43 -0
  40. package/skills/magpie/scripts/types.ts +13 -0
  41. package/skills/magpie/skill.json +1 -1
  42. 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
- test('empty findings renders an explicit empty state', () => {
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('renders one finding with severity chip and post button', () => {
31
- const html = renderFindingsHtml({
32
- findings: [f({ id: 'a', title: 'null deref', severity: 'high' })],
33
- postStatus: {},
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('shows posted badge when present in postStatus', () => {
42
- const html = renderFindingsHtml({
43
- findings: [f({ id: 'a', title: 'x' })],
44
- postStatus: { a: 'posted' },
45
- })
46
- expect(html).toContain('class="badge posted"')
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('shows failed badge with message', () => {
68
+ test('renders one file pane per PrFileEntry, hidden by default', () => {
50
69
  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')
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
- expect(html).toContain('if (!y) return')
70
- expect(html).toContain('Suggested change')
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
- postStatus: {},
76
+ diff: SAMPLE_DIFF,
85
77
  })
86
- expect(html).toContain('class="finding-section finding-section-observation"')
87
- expect(html).toContain('class="finding-section finding-section-impact"')
88
- expect(html).toContain('class="finding-section finding-section-suggestion"')
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 the risk footer with Impact/Likelihood/Confidence/Action', () => {
96
- const html = renderFindingsHtml({
97
- findings: [
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('falls back to a single observation when description has no labels', () => {
88
+ test('renders a per-file toolbar with breadcrumb, deltas, finding-nav and diff-mode toggle', () => {
118
89
  const html = renderFindingsHtml({
119
- findings: [f({ id: 'a', title: 'x', description: 'Just one sentence.' })],
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('finding-section-observation')
123
- expect(html).toContain('>Just one sentence.</p>')
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('includes the archived-mode banner element so file:// view can self-degrade', () => {
104
+ test('renders both unified and split diff containers per file', () => {
127
105
  const html = renderFindingsHtml({
128
- findings: [f({ id: 'a', title: 'x' })],
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('archived-banner')
132
- expect(html).toContain('Archived view')
111
+ expect(html).toContain('data-diff-mode="unified"')
112
+ expect(html).toContain('data-diff-mode="split"')
133
113
  })
134
114
 
135
- test('renders a severity-breakdown summary line', () => {
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('findings-summary')
145
- expect(html).toContain('2</span>&nbsp;high')
146
- expect(html).toContain('1</span>&nbsp;low')
122
+ expect(html).toContain('data-finding-id="a"')
123
+ expect(html).toContain('class="annot')
147
124
  })
148
125
 
149
- test('post button starts disabled (enabled by helper.js after first select)', () => {
150
- const html = renderFindingsHtml({
151
- findings: [f({ id: 'a', title: 'x' })],
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('renders a filter bar with severity and domain chips when findings exist', () => {
158
- const html = renderFindingsHtml({
159
- findings: [
160
- f({ id: 'a', title: 't1', severity: 'high', domain: 'bugs' }),
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('exposes bulk select actions and a keyboard-shortcut hint', () => {
137
+ test('marks posted findings with data-posted="true"', () => {
182
138
  const html = renderFindingsHtml({
183
- findings: [f({ id: 'a', title: 'x' })],
184
- postStatus: {},
139
+ findings: SAMPLE_FINDINGS,
140
+ postStatus: { a: 'posted' },
185
141
  })
186
- expect(html).toMatch(/data-action="select-visible"/)
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('threads runId into a data-run-id attribute on body', () => {
145
+ test('renders the suggestion code block in the annotation', () => {
195
146
  const html = renderFindingsHtml({
196
- findings: [f({ id: 'a', title: 'x' })],
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).toMatch(/<body[^>]+data-run-id="pr-42-1700000000"/)
156
+ expect(html).toContain('if (!y) return')
157
+ expect(html).toContain('Suggested change')
201
158
  })
202
159
 
203
- test('each finding card carries severity/domain/file/search-text data attrs for client-side filtering', () => {
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: 'race in handler',
209
- severity: 'high',
210
- domain: 'bugs',
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).toMatch(/data-severity="high"/)
218
- expect(html).toMatch(/data-domain="bugs"/)
219
- expect(html).toMatch(/data-file="src\/main\.ts"/)
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('falls back to the raw description when no labelled paragraphs are detected and description is empty-ish', () => {
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: [f({ id: 'a', title: 'x', description: '' })],
179
+ findings: SAMPLE_FINDINGS,
308
180
  postStatus: {},
181
+ pr: { number: 42, branch: 'feat/x', headSha: 'deadbeefdeadbeef0000111122223333' },
309
182
  })
310
- expect(html).toContain('finding-section-observation')
183
+ expect(html).toContain('PR #42')
184
+ expect(html).toContain('feat/x')
185
+ expect(html).toContain('deadbeefdead')
311
186
  })