@iceinvein/agent-skills 0.1.30 → 0.1.32

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.
@@ -1,5 +1,7 @@
1
- import { describe, expect, test } from 'bun:test'
1
+ import { beforeAll, describe, expect, test } from 'bun:test'
2
+ import type { Highlighter } from 'shiki'
2
3
  import { parseUnifiedDiffToHunks } from '../diff-utils.ts'
4
+ import { getHighlighter } from '../highlight.ts'
3
5
  import { renderSplitDiff, renderUnifiedDiff } from '../render-diff.ts'
4
6
  import type { ReviewFinding } from '../types.ts'
5
7
 
@@ -24,6 +26,11 @@ const finding: ReviewFinding = {
24
26
  domain: 'bugs',
25
27
  }
26
28
 
29
+ let hl: Highlighter
30
+ beforeAll(async () => {
31
+ hl = await getHighlighter()
32
+ })
33
+
27
34
  describe('renderUnifiedDiff', () => {
28
35
  test('emits one row per diff line', () => {
29
36
  const html = renderUnifiedDiff({
@@ -31,6 +38,8 @@ describe('renderUnifiedDiff', () => {
31
38
  findings: [],
32
39
  postStatus: {},
33
40
  selectedIds: new Set(),
41
+ highlighter: hl,
42
+ file: 'x.ts',
34
43
  })
35
44
  expect((html.match(/class="diff-row/g) ?? []).length).toBe(5)
36
45
  })
@@ -42,6 +51,8 @@ describe('renderUnifiedDiff', () => {
42
51
  findings: [finding],
43
52
  postStatus: {},
44
53
  selectedIds: new Set(),
54
+ highlighter: hl,
55
+ file: 'x.ts',
45
56
  })
46
57
  const idx = html.indexOf('data-finding-id="f-1"')
47
58
  const before = html.lastIndexOf('class="diff-row', idx)
@@ -55,6 +66,8 @@ describe('renderUnifiedDiff', () => {
55
66
  findings: [],
56
67
  postStatus: {},
57
68
  selectedIds: new Set(),
69
+ highlighter: hl,
70
+ file: 'x.ts',
58
71
  })
59
72
  expect(html).toContain('No changes')
60
73
  })
@@ -65,11 +78,57 @@ describe('renderUnifiedDiff', () => {
65
78
  findings: [finding],
66
79
  postStatus: { 'f-1': 'posted' },
67
80
  selectedIds: new Set(),
81
+ highlighter: hl,
82
+ file: 'x.ts',
68
83
  })
69
84
  expect(html).toContain('data-posted="true"')
70
85
  })
71
86
  })
72
87
 
88
+ test('emits shiki-highlighted line content in unified mode', () => {
89
+ const diff = `--- a/x.ts
90
+ +++ b/x.ts
91
+ @@ -1,3 +1,3 @@
92
+ const a = 1
93
+ -const b = 2
94
+ +const b = 3
95
+ `
96
+ const html = renderUnifiedDiff({
97
+ hunks: parseUnifiedDiffToHunks(diff),
98
+ findings: [],
99
+ postStatus: {},
100
+ selectedIds: new Set(),
101
+ highlighter: hl,
102
+ file: 'x.ts',
103
+ })
104
+ expect(html).toContain('const')
105
+ expect(html).toContain('<span style=')
106
+ expect(html).toContain('class="gutter"')
107
+ expect(html).toContain('class="sign"')
108
+ })
109
+
110
+ test('keeps multi-line tokens consistent across diff rows', () => {
111
+ const diff = `--- a/x.ts
112
+ +++ b/x.ts
113
+ @@ -1,5 +1,5 @@
114
+ const x = \`first
115
+ second
116
+ -third\`
117
+ +THIRD\`
118
+ const y = 1
119
+ `
120
+ const html = renderUnifiedDiff({
121
+ hunks: parseUnifiedDiffToHunks(diff),
122
+ findings: [],
123
+ postStatus: {},
124
+ selectedIds: new Set(),
125
+ highlighter: hl,
126
+ file: 'x.ts',
127
+ })
128
+ const rowCount = (html.match(/class="diff-row /g) || []).length
129
+ expect(rowCount).toBe(5)
130
+ })
131
+
73
132
  describe('renderSplitDiff', () => {
74
133
  test('emits left/right cells per row', () => {
75
134
  const html = renderSplitDiff({
@@ -77,8 +136,31 @@ describe('renderSplitDiff', () => {
77
136
  findings: [],
78
137
  postStatus: {},
79
138
  selectedIds: new Set(),
139
+ highlighter: hl,
140
+ file: 'x.ts',
80
141
  })
81
142
  expect((html.match(/diff-row split/g) ?? []).length).toBeGreaterThan(0)
82
143
  expect(html).toContain('diff-cell')
83
144
  })
84
145
  })
146
+
147
+ test('emits shiki-highlighted line content in split mode', () => {
148
+ const diff = `--- a/x.ts
149
+ +++ b/x.ts
150
+ @@ -1,3 +1,3 @@
151
+ const a = 1
152
+ -const b = 2
153
+ +const b = 3
154
+ `
155
+ const html = renderSplitDiff({
156
+ hunks: parseUnifiedDiffToHunks(diff),
157
+ findings: [],
158
+ postStatus: {},
159
+ selectedIds: new Set(),
160
+ highlighter: hl,
161
+ file: 'x.ts',
162
+ })
163
+ expect(html).toContain('class="diff-row split"')
164
+ expect(html).toContain('<span style=')
165
+ expect(html).toContain('class="gutter"')
166
+ })
@@ -1,4 +1,6 @@
1
- import { expect, test } from 'bun:test'
1
+ import { beforeAll, expect, test } from 'bun:test'
2
+ import type { Highlighter } from 'shiki'
3
+ import { getHighlighter } from '../highlight.ts'
2
4
  import { renderFindingsHtml } from '../render-findings.ts'
3
5
  import type { ReviewFinding } from '../types.ts'
4
6
 
@@ -44,21 +46,26 @@ const SAMPLE_DIFF = `--- a/src/a.ts
44
46
  line3
45
47
  `
46
48
 
49
+ let hl: Highlighter
50
+ beforeAll(async () => {
51
+ hl = await getHighlighter()
52
+ })
53
+
47
54
  test('empty findings renders the page shell with an empty-state message', () => {
48
- const html = renderFindingsHtml({ findings: [], postStatus: {} })
55
+ const html = renderFindingsHtml({ findings: [], postStatus: {}, highlighter: hl })
49
56
  expect(html).toContain('class="pr-header"')
50
57
  expect(html).toContain('No findings')
51
58
  })
52
59
 
53
60
  test('emits the two-tab segmented control with Files and All Issues buttons', () => {
54
- const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
61
+ const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {}, highlighter: hl })
55
62
  expect(html).toContain('data-action="set-view"')
56
63
  expect(html).toContain('data-view="files"')
57
64
  expect(html).toContain('data-view="all-issues"')
58
65
  })
59
66
 
60
67
  test('sets body data attributes for default view, diff-mode, and suggestions visibility', () => {
61
- const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
68
+ const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {}, highlighter: hl })
62
69
  expect(html).toContain('data-page="findings"')
63
70
  expect(html).toContain('data-view="files"')
64
71
  expect(html).toContain('data-diff-mode="unified"')
@@ -74,6 +81,7 @@ test('renders one file pane per PrFileEntry, hidden by default', () => {
74
81
  { path: 'src/b.ts', additions: 1, deletions: 0 },
75
82
  ],
76
83
  diff: SAMPLE_DIFF,
84
+ highlighter: hl,
77
85
  })
78
86
  expect(html).toContain('data-file-pane="src/a.ts"')
79
87
  expect(html).toContain('data-file-pane="src/b.ts"')
@@ -81,7 +89,7 @@ test('renders one file pane per PrFileEntry, hidden by default', () => {
81
89
  })
82
90
 
83
91
  test('renders an Overview pane mapped to data-file-pane=""', () => {
84
- const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
92
+ const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {}, highlighter: hl })
85
93
  expect(html).toContain('data-file-pane=""')
86
94
  })
87
95
 
@@ -91,6 +99,7 @@ test('renders a per-file toolbar with breadcrumb, deltas, finding-nav and diff-m
91
99
  postStatus: {},
92
100
  files: [{ path: 'src/a.ts', additions: 3, deletions: 1 }],
93
101
  diff: SAMPLE_DIFF,
102
+ highlighter: hl,
94
103
  })
95
104
  expect(html).toContain('+3')
96
105
  expect(html).toContain('-1')
@@ -107,6 +116,7 @@ test('renders both unified and split diff containers per file', () => {
107
116
  postStatus: {},
108
117
  files: [{ path: 'src/a.ts', additions: 3, deletions: 1 }],
109
118
  diff: SAMPLE_DIFF,
119
+ highlighter: hl,
110
120
  })
111
121
  expect(html).toContain('data-diff-mode="unified"')
112
122
  expect(html).toContain('data-diff-mode="split"')
@@ -118,18 +128,19 @@ test('places an inline annotation at the affected diff line', () => {
118
128
  postStatus: {},
119
129
  files: [{ path: 'src/a.ts', additions: 3, deletions: 1 }],
120
130
  diff: SAMPLE_DIFF,
131
+ highlighter: hl,
121
132
  })
122
133
  expect(html).toContain('data-finding-id="a"')
123
134
  expect(html).toContain('class="annot')
124
135
  })
125
136
 
126
137
  test('mounts the All Issues pane', () => {
127
- const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
138
+ const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {}, highlighter: hl })
128
139
  expect(html).toContain('data-role="issues-list"')
129
140
  })
130
141
 
131
142
  test('mounts the action bar at the page bottom', () => {
132
- const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {} })
143
+ const html = renderFindingsHtml({ findings: SAMPLE_FINDINGS, postStatus: {}, highlighter: hl })
133
144
  expect(html).toContain('data-role="action-bar"')
134
145
  expect(html).toContain('Post Recommended')
135
146
  })
@@ -138,6 +149,7 @@ test('marks posted findings with data-posted="true"', () => {
138
149
  const html = renderFindingsHtml({
139
150
  findings: SAMPLE_FINDINGS,
140
151
  postStatus: { a: 'posted' },
152
+ highlighter: hl,
141
153
  })
142
154
  expect(html).toContain('data-posted="true"')
143
155
  })
@@ -152,8 +164,10 @@ test('renders the suggestion code block in the annotation', () => {
152
164
  }),
153
165
  ],
154
166
  postStatus: {},
167
+ highlighter: hl,
155
168
  })
156
- expect(html).toContain('if (!y) return')
169
+ // Shiki tokenizes identifiers into separate spans; check for a known token
170
+ expect(html).toContain('return')
157
171
  expect(html).toContain('Suggested change')
158
172
  })
159
173
 
@@ -168,6 +182,7 @@ test('parses labelled paragraphs into section headers', () => {
168
182
  }),
169
183
  ],
170
184
  postStatus: {},
185
+ highlighter: hl,
171
186
  })
172
187
  expect(html).toContain('Observation')
173
188
  expect(html).toContain('Why it matters')
@@ -179,6 +194,7 @@ test('renders pr meta when supplied', () => {
179
194
  findings: SAMPLE_FINDINGS,
180
195
  postStatus: {},
181
196
  pr: { number: 42, branch: 'feat/x', headSha: 'deadbeefdeadbeef0000111122223333' },
197
+ highlighter: hl,
182
198
  })
183
199
  expect(html).toContain('PR #42')
184
200
  expect(html).toContain('feat/x')
@@ -1,4 +1,6 @@
1
- import { describe, expect, test } from 'bun:test'
1
+ import { beforeAll, describe, expect, test } from 'bun:test'
2
+ import type { Highlighter } from 'shiki'
3
+ import { getHighlighter } from '../highlight.ts'
2
4
  import { renderIssuesList } from '../render-issues-list.ts'
3
5
  import type { ReviewFinding } from '../types.ts'
4
6
 
@@ -35,31 +37,61 @@ const findings: ReviewFinding[] = [
35
37
  },
36
38
  ] as ReviewFinding[]
37
39
 
40
+ let hl: Highlighter
41
+ beforeAll(async () => {
42
+ hl = await getHighlighter()
43
+ })
44
+
38
45
  describe('renderIssuesList', () => {
39
46
  test('renders an issue-card per finding', () => {
40
- const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
47
+ const html = renderIssuesList({
48
+ findings,
49
+ postStatus: {},
50
+ selectedIds: new Set(),
51
+ highlighter: hl,
52
+ })
41
53
  expect((html.match(/class="issue-card/g) ?? []).length).toBe(3)
42
54
  })
43
55
 
44
56
  test('renders severity filter pills with counts', () => {
45
- const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
57
+ const html = renderIssuesList({
58
+ findings,
59
+ postStatus: {},
60
+ selectedIds: new Set(),
61
+ highlighter: hl,
62
+ })
46
63
  expect(html).toContain('Blocker (1)')
47
64
  expect(html).toContain('High (1)')
48
65
  expect(html).toContain('Low (1)')
49
66
  })
50
67
 
51
68
  test('emits "N should review" with actionable count only', () => {
52
- const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
69
+ const html = renderIssuesList({
70
+ findings,
71
+ postStatus: {},
72
+ selectedIds: new Set(),
73
+ highlighter: hl,
74
+ })
53
75
  expect(html).toContain('2 should review')
54
76
  })
55
77
 
56
78
  test('emits "Show N suggestions" toggle when suggestions present', () => {
57
- const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
79
+ const html = renderIssuesList({
80
+ findings,
81
+ postStatus: {},
82
+ selectedIds: new Set(),
83
+ highlighter: hl,
84
+ })
58
85
  expect(html).toContain('Show 1 suggestion')
59
86
  })
60
87
 
61
88
  test('suggestion finding has data-suggestion="true"', () => {
62
- const html = renderIssuesList({ findings, postStatus: {}, selectedIds: new Set() })
89
+ const html = renderIssuesList({
90
+ findings,
91
+ postStatus: {},
92
+ selectedIds: new Set(),
93
+ highlighter: hl,
94
+ })
63
95
  expect(html).toMatch(
64
96
  /data-finding-id="3"[^>]*data-suggestion="true"|data-suggestion="true"[^>]*data-finding-id="3"/,
65
97
  )
@@ -52,7 +52,9 @@ test('styles.css declares a prefers-color-scheme:dark block that overrides core
52
52
  test('rendered HTML opts into light/dark with a color-scheme meta tag', async () => {
53
53
  const { renderFindingsHtml } = await import('../render-findings.ts')
54
54
  const { renderProgressHtml } = await import('../render-progress.ts')
55
- const findings = renderFindingsHtml({ findings: [], postStatus: {} })
55
+ const { getHighlighter } = await import('../highlight.ts')
56
+ const highlighter = await getHighlighter()
57
+ const findings = renderFindingsHtml({ findings: [], postStatus: {}, highlighter })
56
58
  const progress = renderProgressHtml({
57
59
  prNumber: 1,
58
60
  headSha: 'deadbeef0011',
@@ -1,6 +1,13 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import type { FocusId, PrFileEntry, ReviewFinding } from '../types.ts'
3
- import { FOCUS_IDS, isSuggestion, parseFinding } from '../types.ts'
3
+ import {
4
+ coerceRisk,
5
+ coerceSeverity,
6
+ FOCUS_IDS,
7
+ isSuggestion,
8
+ looksLikeProse,
9
+ parseFinding,
10
+ } from '../types.ts'
4
11
 
5
12
  test('FOCUS_IDS contains the LLM focuses plus the deterministic tests domain', () => {
6
13
  expect(FOCUS_IDS).toEqual([
@@ -30,19 +37,151 @@ test('parseFinding accepts a minimal finding', () => {
30
37
  expect(parsed.domain).toBe('bugs')
31
38
  })
32
39
 
33
- test('parseFinding rejects an unknown severity', () => {
34
- expect(() =>
35
- parseFinding({
36
- id: 'f1',
37
- file: 'src/x.ts',
38
- line: 10,
39
- severity: 'panic',
40
- risk: { impact: 'high', likelihood: 'possible', confidence: 'medium', action: 'should-fix' },
41
- title: 't',
42
- description: 'd',
43
- domain: 'bugs',
44
- }),
45
- ).toThrow(/severity/)
40
+ test('parseFinding coerces an unknown severity to medium', () => {
41
+ const parsed = parseFinding({
42
+ id: 'f1',
43
+ file: 'src/x.ts',
44
+ line: 10,
45
+ severity: 'panic',
46
+ risk: { impact: 'high', likelihood: 'possible', confidence: 'medium', action: 'should-fix' },
47
+ title: 't',
48
+ description: 'd',
49
+ domain: 'bugs',
50
+ })
51
+ expect(parsed.severity).toBe('medium')
52
+ })
53
+
54
+ test('parseFinding coerces sentence-form action to canonical enum', () => {
55
+ const parsed = parseFinding({
56
+ id: 'f1',
57
+ file: 'src/x.ts',
58
+ line: 10,
59
+ severity: 'high',
60
+ risk: {
61
+ impact: 'high',
62
+ likelihood: 'possible',
63
+ confidence: 'medium',
64
+ action: 'Should be fixed before merge',
65
+ },
66
+ title: 't',
67
+ description: 'd',
68
+ domain: 'bugs',
69
+ })
70
+ expect(parsed.risk.action).toBe('should-fix')
71
+ })
72
+
73
+ test('parseFinding coerces wrong-axis likelihood values', () => {
74
+ const parsed = parseFinding({
75
+ id: 'f1',
76
+ file: 'src/x.ts',
77
+ line: 10,
78
+ severity: 'high',
79
+ risk: { impact: 'high', likelihood: 'high', confidence: 'medium', action: 'should-fix' },
80
+ title: 't',
81
+ description: 'd',
82
+ domain: 'bugs',
83
+ })
84
+ expect(parsed.risk.likelihood).toBe('likely')
85
+ })
86
+
87
+ describe('coerceSeverity', () => {
88
+ test('passes through canonical values', () => {
89
+ for (const v of ['blocker', 'high', 'medium', 'low'] as const) {
90
+ expect(coerceSeverity(v)).toBe(v)
91
+ }
92
+ })
93
+ test('maps synonyms', () => {
94
+ expect(coerceSeverity('critical')).toBe('blocker')
95
+ expect(coerceSeverity('major')).toBe('high')
96
+ expect(coerceSeverity('moderate')).toBe('medium')
97
+ expect(coerceSeverity('minor')).toBe('low')
98
+ })
99
+ test('handles casing and whitespace', () => {
100
+ expect(coerceSeverity(' HIGH ')).toBe('high')
101
+ })
102
+ test('falls back to medium for garbage', () => {
103
+ expect(coerceSeverity('panic')).toBe('medium')
104
+ expect(coerceSeverity('')).toBe('medium')
105
+ expect(coerceSeverity(null)).toBe('medium')
106
+ expect(coerceSeverity(undefined)).toBe('medium')
107
+ expect(coerceSeverity(42)).toBe('medium')
108
+ })
109
+ })
110
+
111
+ describe('coerceRisk', () => {
112
+ test('passes through canonical risk object', () => {
113
+ expect(
114
+ coerceRisk({
115
+ impact: 'critical',
116
+ likelihood: 'likely',
117
+ confidence: 'high',
118
+ action: 'must-fix',
119
+ }),
120
+ ).toEqual({
121
+ impact: 'critical',
122
+ likelihood: 'likely',
123
+ confidence: 'high',
124
+ action: 'must-fix',
125
+ })
126
+ })
127
+ test('maps action synonyms and underscore/space variants', () => {
128
+ expect(coerceRisk({ action: 'must_fix' }).action).toBe('must-fix')
129
+ expect(coerceRisk({ action: 'should fix' }).action).toBe('should-fix')
130
+ expect(coerceRisk({ action: 'blocker' }).action).toBe('must-fix')
131
+ expect(coerceRisk({ action: 'nit' }).action).toBe('optional')
132
+ expect(coerceRisk({ action: 'recommend' }).action).toBe('should-fix')
133
+ })
134
+ test('extracts action from full sentences', () => {
135
+ expect(coerceRisk({ action: 'This must be fixed immediately.' }).action).toBe('must-fix')
136
+ expect(coerceRisk({ action: 'Should fix before merging the PR' }).action).toBe('should-fix')
137
+ expect(coerceRisk({ action: 'Consider refactoring this' }).action).toBe('consider')
138
+ expect(coerceRisk({ action: 'Nice to have' }).action).toBe('optional')
139
+ })
140
+ test('maps likelihood wrong-axis values', () => {
141
+ expect(coerceRisk({ likelihood: 'high' }).likelihood).toBe('likely')
142
+ expect(coerceRisk({ likelihood: 'medium' }).likelihood).toBe('possible')
143
+ expect(coerceRisk({ likelihood: 'low' }).likelihood).toBe('edge-case')
144
+ expect(coerceRisk({ likelihood: 'edge case' }).likelihood).toBe('edge-case')
145
+ expect(coerceRisk({ likelihood: 'n/a' }).likelihood).toBe('unknown')
146
+ })
147
+ test('maps impact synonyms', () => {
148
+ expect(coerceRisk({ impact: 'blocker' }).impact).toBe('critical')
149
+ expect(coerceRisk({ impact: 'major' }).impact).toBe('high')
150
+ expect(coerceRisk({ impact: 'moderate' }).impact).toBe('medium')
151
+ expect(coerceRisk({ impact: 'minor' }).impact).toBe('low')
152
+ })
153
+ test('maps confidence synonyms', () => {
154
+ expect(coerceRisk({ confidence: 'certain' }).confidence).toBe('high')
155
+ expect(coerceRisk({ confidence: 'moderate' }).confidence).toBe('medium')
156
+ expect(coerceRisk({ confidence: 'speculative' }).confidence).toBe('low')
157
+ })
158
+ test('falls back to safe defaults for garbage / missing', () => {
159
+ expect(coerceRisk({})).toEqual({
160
+ impact: 'medium',
161
+ likelihood: 'unknown',
162
+ confidence: 'medium',
163
+ action: 'consider',
164
+ })
165
+ expect(coerceRisk(null)).toEqual({
166
+ impact: 'medium',
167
+ likelihood: 'unknown',
168
+ confidence: 'medium',
169
+ action: 'consider',
170
+ })
171
+ expect(
172
+ coerceRisk({
173
+ impact: 'whatever',
174
+ likelihood: 42,
175
+ confidence: '',
176
+ action: 'do the thing',
177
+ }),
178
+ ).toEqual({
179
+ impact: 'medium',
180
+ likelihood: 'unknown',
181
+ confidence: 'medium',
182
+ action: 'consider',
183
+ })
184
+ })
46
185
  })
47
186
 
48
187
  test('parseFinding accepts null line', () => {
@@ -68,6 +207,80 @@ describe('PrFileEntry', () => {
68
207
  })
69
208
  })
70
209
 
210
+ describe('looksLikeProse', () => {
211
+ test('returns true for multi-sentence prose with backticks', () => {
212
+ const body =
213
+ 'Strip the delimiter from user-controlled fields. For example, inside `getX` apply a sanitiser that neutralises the tokens.'
214
+ expect(looksLikeProse(body)).toBe(true)
215
+ })
216
+ test('returns true for single-line sentence-shaped explanation', () => {
217
+ const body =
218
+ 'Reject the literal delimiter token in any field that flows into the draft before invoking Bedrock.'
219
+ expect(looksLikeProse(body)).toBe(true)
220
+ })
221
+ test('returns false for multi-line replacement code', () => {
222
+ const body =
223
+ 'if (input.includes("<<<END>>>")) {\n throw new Error("delimiter token not allowed")\n}'
224
+ expect(looksLikeProse(body)).toBe(false)
225
+ })
226
+ test('returns false for short single-line code', () => {
227
+ expect(looksLikeProse('return null;')).toBe(false)
228
+ expect(looksLikeProse('const x = foo(bar);')).toBe(false)
229
+ expect(looksLikeProse('user?.id ?? "";')).toBe(false)
230
+ })
231
+ test('returns false for one-line code that happens to end with a period', () => {
232
+ // No prose connectives, just a method chain.
233
+ expect(looksLikeProse('foo.bar.baz();')).toBe(false)
234
+ })
235
+ test('returns false for empty body', () => {
236
+ expect(looksLikeProse('')).toBe(false)
237
+ expect(looksLikeProse(' ')).toBe(false)
238
+ })
239
+ test('returns false for fenced code block', () => {
240
+ const body = '```ts\nconst guarded = sanitize(input)\nreturn guarded\n```'
241
+ expect(looksLikeProse(body)).toBe(false)
242
+ })
243
+ })
244
+
245
+ describe('parseFinding suggestion validator', () => {
246
+ const base = {
247
+ id: 'f1',
248
+ file: 'src/x.ts',
249
+ line: 10,
250
+ severity: 'medium',
251
+ risk: { impact: 'medium', likelihood: 'possible', confidence: 'medium', action: 'should-fix' },
252
+ title: 't',
253
+ description: 'd',
254
+ domain: 'security',
255
+ }
256
+ test('drops suggestion when body is prose', () => {
257
+ const parsed = parseFinding({
258
+ ...base,
259
+ suggestion: {
260
+ body: 'Strip the delimiter tokens before they enter the draft. Add a regression test to confirm.',
261
+ startLine: 10,
262
+ endLine: 12,
263
+ },
264
+ })
265
+ expect(parsed.suggestion).toBeUndefined()
266
+ })
267
+ test('keeps suggestion when body is real replacement code', () => {
268
+ const parsed = parseFinding({
269
+ ...base,
270
+ suggestion: {
271
+ body: 'const guarded = sanitize(input)\nreturn guarded',
272
+ startLine: 10,
273
+ endLine: 11,
274
+ },
275
+ })
276
+ expect(parsed.suggestion).toEqual({
277
+ body: 'const guarded = sanitize(input)\nreturn guarded',
278
+ startLine: 10,
279
+ endLine: 11,
280
+ })
281
+ })
282
+ })
283
+
71
284
  describe('isSuggestion', () => {
72
285
  const base = {
73
286
  id: '1',