@sentry/warden 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/find-bugs/SKILL.md +75 -0
- package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
- package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.claude/settings.json +57 -0
- package/.claude/settings.local.json +88 -0
- package/.claude/skills/agent-prompt/SKILL.md +54 -0
- package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
- package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
- package/.claude/skills/agent-prompt/references/context-design.md +124 -0
- package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
- package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
- package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
- package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
- package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
- package/.claude/skills/notseer/SKILL.md +131 -0
- package/.claude/skills/skill-writer/SKILL.md +140 -0
- package/.claude/skills/testing-guidelines/SKILL.md +132 -0
- package/.claude/skills/warden-skill/SKILL.md +250 -0
- package/.claude/skills/warden-skill/references/config-schema.md +133 -0
- package/.dex/config.toml +2 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/release.yml +54 -0
- package/.github/workflows/warden.yml +40 -0
- package/AGENTS.md +89 -0
- package/CONTRIBUTING.md +60 -0
- package/LICENSE +105 -0
- package/README.md +43 -0
- package/SPEC.md +263 -0
- package/action.yml +87 -0
- package/assets/favicon.png +0 -0
- package/assets/warden-icon-bw.svg +5 -0
- package/assets/warden-icon-purple.png +0 -0
- package/assets/warden-icon-purple.svg +5 -0
- package/docs/.claude/settings.local.json +11 -0
- package/docs/astro.config.mjs +43 -0
- package/docs/package.json +19 -0
- package/docs/pnpm-lock.yaml +4000 -0
- package/docs/public/favicon.svg +5 -0
- package/docs/src/components/Code.astro +141 -0
- package/docs/src/components/PackageManagerTabs.astro +183 -0
- package/docs/src/components/Terminal.astro +212 -0
- package/docs/src/layouts/Base.astro +380 -0
- package/docs/src/pages/cli.astro +167 -0
- package/docs/src/pages/config.astro +394 -0
- package/docs/src/pages/guide.astro +449 -0
- package/docs/src/pages/index.astro +490 -0
- package/docs/src/styles/global.css +551 -0
- package/docs/tsconfig.json +3 -0
- package/docs/vercel.json +5 -0
- package/eslint.config.js +33 -0
- package/package.json +73 -0
- package/src/action/index.ts +1 -0
- package/src/action/main.ts +868 -0
- package/src/cli/args.test.ts +477 -0
- package/src/cli/args.ts +415 -0
- package/src/cli/commands/add.ts +447 -0
- package/src/cli/commands/init.test.ts +136 -0
- package/src/cli/commands/init.ts +132 -0
- package/src/cli/commands/setup-app/browser.ts +38 -0
- package/src/cli/commands/setup-app/credentials.ts +45 -0
- package/src/cli/commands/setup-app/manifest.ts +48 -0
- package/src/cli/commands/setup-app/server.ts +172 -0
- package/src/cli/commands/setup-app.ts +156 -0
- package/src/cli/commands/sync.ts +114 -0
- package/src/cli/context.ts +131 -0
- package/src/cli/files.test.ts +155 -0
- package/src/cli/files.ts +89 -0
- package/src/cli/fix.test.ts +310 -0
- package/src/cli/fix.ts +387 -0
- package/src/cli/git.test.ts +119 -0
- package/src/cli/git.ts +318 -0
- package/src/cli/index.ts +14 -0
- package/src/cli/main.ts +672 -0
- package/src/cli/output/box.ts +235 -0
- package/src/cli/output/formatters.test.ts +187 -0
- package/src/cli/output/formatters.ts +269 -0
- package/src/cli/output/icons.ts +13 -0
- package/src/cli/output/index.ts +44 -0
- package/src/cli/output/ink-runner.tsx +337 -0
- package/src/cli/output/jsonl.test.ts +347 -0
- package/src/cli/output/jsonl.ts +126 -0
- package/src/cli/output/reporter.ts +435 -0
- package/src/cli/output/tasks.ts +374 -0
- package/src/cli/output/tty.test.ts +117 -0
- package/src/cli/output/tty.ts +60 -0
- package/src/cli/output/verbosity.test.ts +40 -0
- package/src/cli/output/verbosity.ts +31 -0
- package/src/cli/terminal.test.ts +148 -0
- package/src/cli/terminal.ts +301 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.test.ts +313 -0
- package/src/config/loader.ts +103 -0
- package/src/config/schema.ts +168 -0
- package/src/config/writer.test.ts +119 -0
- package/src/config/writer.ts +84 -0
- package/src/diff/classify.test.ts +162 -0
- package/src/diff/classify.ts +92 -0
- package/src/diff/coalesce.test.ts +208 -0
- package/src/diff/coalesce.ts +133 -0
- package/src/diff/context.test.ts +226 -0
- package/src/diff/context.ts +201 -0
- package/src/diff/index.ts +4 -0
- package/src/diff/parser.test.ts +212 -0
- package/src/diff/parser.ts +149 -0
- package/src/event/context.ts +132 -0
- package/src/event/index.ts +2 -0
- package/src/event/schedule-context.ts +101 -0
- package/src/examples/examples.integration.test.ts +66 -0
- package/src/examples/index.test.ts +101 -0
- package/src/examples/index.ts +122 -0
- package/src/examples/setup.ts +25 -0
- package/src/index.ts +115 -0
- package/src/output/dedup.test.ts +419 -0
- package/src/output/dedup.ts +607 -0
- package/src/output/github-checks.test.ts +300 -0
- package/src/output/github-checks.ts +476 -0
- package/src/output/github-issues.ts +329 -0
- package/src/output/index.ts +5 -0
- package/src/output/issue-renderer.ts +197 -0
- package/src/output/renderer.test.ts +727 -0
- package/src/output/renderer.ts +217 -0
- package/src/output/stale.test.ts +375 -0
- package/src/output/stale.ts +155 -0
- package/src/output/types.ts +34 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/runner.test.ts +806 -0
- package/src/sdk/runner.ts +1232 -0
- package/src/skills/index.ts +36 -0
- package/src/skills/loader.test.ts +300 -0
- package/src/skills/loader.ts +423 -0
- package/src/skills/remote.test.ts +704 -0
- package/src/skills/remote.ts +604 -0
- package/src/triggers/matcher.test.ts +277 -0
- package/src/triggers/matcher.ts +152 -0
- package/src/types/index.ts +194 -0
- package/src/utils/async.ts +18 -0
- package/src/utils/index.test.ts +84 -0
- package/src/utils/index.ts +50 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +11 -0
- package/warden.toml +19 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderSkillReport } from './renderer.js';
|
|
3
|
+
import { parseMarker } from './dedup.js';
|
|
4
|
+
import type { SkillReport } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
describe('renderSkillReport', () => {
|
|
7
|
+
const baseReport: SkillReport = {
|
|
8
|
+
skill: 'security-review',
|
|
9
|
+
summary: 'Found 2 potential issues',
|
|
10
|
+
findings: [],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it('renders empty findings report', () => {
|
|
14
|
+
const result = renderSkillReport(baseReport);
|
|
15
|
+
|
|
16
|
+
expect(result.review).toBeUndefined();
|
|
17
|
+
expect(result.summaryComment).toContain('security-review');
|
|
18
|
+
expect(result.summaryComment).toContain('No findings to report');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders findings with inline comments', () => {
|
|
22
|
+
const report: SkillReport = {
|
|
23
|
+
...baseReport,
|
|
24
|
+
findings: [
|
|
25
|
+
{
|
|
26
|
+
id: 'sql-injection-1',
|
|
27
|
+
severity: 'critical',
|
|
28
|
+
title: 'SQL Injection',
|
|
29
|
+
description: 'User input passed directly to query',
|
|
30
|
+
location: {
|
|
31
|
+
path: 'src/db.ts',
|
|
32
|
+
startLine: 42,
|
|
33
|
+
endLine: 45,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = renderSkillReport(report);
|
|
40
|
+
|
|
41
|
+
expect(result.review).toBeDefined();
|
|
42
|
+
const review = result.review!;
|
|
43
|
+
expect(review.event).toBe('REQUEST_CHANGES');
|
|
44
|
+
expect(review.comments).toHaveLength(1);
|
|
45
|
+
expect(review.comments[0]!.path).toBe('src/db.ts');
|
|
46
|
+
expect(review.comments[0]!.line).toBe(45);
|
|
47
|
+
expect(review.comments[0]!.body).toContain('SQL Injection');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('includes skill attribution footnote in comments', () => {
|
|
51
|
+
const report: SkillReport = {
|
|
52
|
+
...baseReport,
|
|
53
|
+
skill: 'code-review',
|
|
54
|
+
findings: [
|
|
55
|
+
{
|
|
56
|
+
id: 'f1',
|
|
57
|
+
severity: 'medium',
|
|
58
|
+
title: 'Issue',
|
|
59
|
+
description: 'Details',
|
|
60
|
+
location: {
|
|
61
|
+
path: 'src/file.ts',
|
|
62
|
+
startLine: 10,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = renderSkillReport(report);
|
|
69
|
+
|
|
70
|
+
expect(result.review).toBeDefined();
|
|
71
|
+
expect(result.review!.comments[0]!.body).toContain('<sub>warden: code-review</sub>');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('includes deduplication marker in comments', () => {
|
|
75
|
+
const report: SkillReport = {
|
|
76
|
+
...baseReport,
|
|
77
|
+
skill: 'security-review',
|
|
78
|
+
findings: [
|
|
79
|
+
{
|
|
80
|
+
id: 'f1',
|
|
81
|
+
severity: 'high',
|
|
82
|
+
title: 'SQL Injection',
|
|
83
|
+
description: 'User input passed to query',
|
|
84
|
+
location: {
|
|
85
|
+
path: 'src/db.ts',
|
|
86
|
+
startLine: 42,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = renderSkillReport(report);
|
|
93
|
+
|
|
94
|
+
expect(result.review).toBeDefined();
|
|
95
|
+
const body = result.review!.comments[0]!.body;
|
|
96
|
+
|
|
97
|
+
// Verify marker is present and parseable
|
|
98
|
+
const marker = parseMarker(body);
|
|
99
|
+
expect(marker).not.toBeNull();
|
|
100
|
+
expect(marker!.path).toBe('src/db.ts');
|
|
101
|
+
expect(marker!.line).toBe(42);
|
|
102
|
+
expect(marker!.contentHash).toMatch(/^[a-f0-9]{8}$/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('sets start_line for multi-line findings', () => {
|
|
106
|
+
const report: SkillReport = {
|
|
107
|
+
...baseReport,
|
|
108
|
+
findings: [
|
|
109
|
+
{
|
|
110
|
+
id: 'multi-line-1',
|
|
111
|
+
severity: 'medium',
|
|
112
|
+
title: 'Multi-line issue',
|
|
113
|
+
description: 'Spans multiple lines',
|
|
114
|
+
location: {
|
|
115
|
+
path: 'src/code.ts',
|
|
116
|
+
startLine: 10,
|
|
117
|
+
endLine: 15,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = renderSkillReport(report);
|
|
124
|
+
|
|
125
|
+
const comment = result.review!.comments[0]!;
|
|
126
|
+
expect(comment.line).toBe(15);
|
|
127
|
+
expect(comment.start_line).toBe(10);
|
|
128
|
+
expect(comment.start_side).toBe('RIGHT');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not set start_line for single-line findings', () => {
|
|
132
|
+
const report: SkillReport = {
|
|
133
|
+
...baseReport,
|
|
134
|
+
findings: [
|
|
135
|
+
{
|
|
136
|
+
id: 'single-line-1',
|
|
137
|
+
severity: 'medium',
|
|
138
|
+
title: 'Single-line issue',
|
|
139
|
+
description: 'On one line',
|
|
140
|
+
location: {
|
|
141
|
+
path: 'src/code.ts',
|
|
142
|
+
startLine: 25,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const result = renderSkillReport(report);
|
|
149
|
+
|
|
150
|
+
const comment = result.review!.comments[0]!;
|
|
151
|
+
expect(comment.line).toBe(25);
|
|
152
|
+
expect(comment.start_line).toBeUndefined();
|
|
153
|
+
expect(comment.start_side).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('does not set start_line when startLine equals endLine', () => {
|
|
157
|
+
const report: SkillReport = {
|
|
158
|
+
...baseReport,
|
|
159
|
+
findings: [
|
|
160
|
+
{
|
|
161
|
+
id: 'same-line-1',
|
|
162
|
+
severity: 'medium',
|
|
163
|
+
title: 'Same line issue',
|
|
164
|
+
description: 'Start and end are same',
|
|
165
|
+
location: {
|
|
166
|
+
path: 'src/code.ts',
|
|
167
|
+
startLine: 30,
|
|
168
|
+
endLine: 30,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = renderSkillReport(report);
|
|
175
|
+
|
|
176
|
+
const comment = result.review!.comments[0]!;
|
|
177
|
+
expect(comment.line).toBe(30);
|
|
178
|
+
expect(comment.start_line).toBeUndefined();
|
|
179
|
+
expect(comment.start_side).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('renders suggested fixes as GitHub suggestions', () => {
|
|
183
|
+
const report: SkillReport = {
|
|
184
|
+
...baseReport,
|
|
185
|
+
findings: [
|
|
186
|
+
{
|
|
187
|
+
id: 'fix-1',
|
|
188
|
+
severity: 'medium',
|
|
189
|
+
title: 'Use parameterized query',
|
|
190
|
+
description: 'Replace string concatenation with parameters',
|
|
191
|
+
location: {
|
|
192
|
+
path: 'src/db.ts',
|
|
193
|
+
startLine: 10,
|
|
194
|
+
},
|
|
195
|
+
suggestedFix: {
|
|
196
|
+
description: 'Use prepared statement',
|
|
197
|
+
diff: `--- a/src/db.ts
|
|
198
|
+
+++ b/src/db.ts
|
|
199
|
+
@@ -10,1 +10,1 @@
|
|
200
|
+
-const query = "SELECT * FROM users WHERE id = " + id;
|
|
201
|
+
+const query = "SELECT * FROM users WHERE id = ?";`,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = renderSkillReport(report);
|
|
208
|
+
|
|
209
|
+
const review = result.review!;
|
|
210
|
+
expect(review.comments[0]!.body).toContain('```suggestion');
|
|
211
|
+
expect(review.comments[0]!.body).toContain(
|
|
212
|
+
'const query = "SELECT * FROM users WHERE id = ?";'
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('groups findings by file in summary', () => {
|
|
217
|
+
const report: SkillReport = {
|
|
218
|
+
...baseReport,
|
|
219
|
+
findings: [
|
|
220
|
+
{
|
|
221
|
+
id: 'f1',
|
|
222
|
+
severity: 'medium',
|
|
223
|
+
title: 'Issue A',
|
|
224
|
+
description: 'Details',
|
|
225
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: 'f2',
|
|
229
|
+
severity: 'low',
|
|
230
|
+
title: 'Issue B',
|
|
231
|
+
description: 'Details',
|
|
232
|
+
location: { path: 'src/b.ts', startLine: 20 },
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'f3',
|
|
236
|
+
severity: 'info',
|
|
237
|
+
title: 'Issue C',
|
|
238
|
+
description: 'Details',
|
|
239
|
+
location: { path: 'src/a.ts', startLine: 30 },
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const result = renderSkillReport(report);
|
|
245
|
+
|
|
246
|
+
expect(result.summaryComment).toContain('`src/a.ts`');
|
|
247
|
+
expect(result.summaryComment).toContain('`src/b.ts`');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('sorts findings by severity', () => {
|
|
251
|
+
const report: SkillReport = {
|
|
252
|
+
...baseReport,
|
|
253
|
+
findings: [
|
|
254
|
+
{
|
|
255
|
+
id: 'f1',
|
|
256
|
+
severity: 'low',
|
|
257
|
+
title: 'Low Issue',
|
|
258
|
+
description: 'Details',
|
|
259
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'f2',
|
|
263
|
+
severity: 'critical',
|
|
264
|
+
title: 'Critical Issue',
|
|
265
|
+
description: 'Details',
|
|
266
|
+
location: { path: 'src/a.ts', startLine: 20 },
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const result = renderSkillReport(report);
|
|
272
|
+
|
|
273
|
+
const review = result.review!;
|
|
274
|
+
expect(review.comments[0]!.body).toContain('Critical Issue');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('requests changes for critical/high severity', () => {
|
|
278
|
+
const criticalReport: SkillReport = {
|
|
279
|
+
...baseReport,
|
|
280
|
+
findings: [
|
|
281
|
+
{
|
|
282
|
+
id: 'f1',
|
|
283
|
+
severity: 'critical',
|
|
284
|
+
title: 'Critical',
|
|
285
|
+
description: 'Details',
|
|
286
|
+
location: { path: 'src/a.ts', startLine: 1 },
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const highReport: SkillReport = {
|
|
292
|
+
...baseReport,
|
|
293
|
+
findings: [
|
|
294
|
+
{
|
|
295
|
+
id: 'f1',
|
|
296
|
+
severity: 'high',
|
|
297
|
+
title: 'High',
|
|
298
|
+
description: 'Details',
|
|
299
|
+
location: { path: 'src/a.ts', startLine: 1 },
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const mediumReport: SkillReport = {
|
|
305
|
+
...baseReport,
|
|
306
|
+
findings: [
|
|
307
|
+
{
|
|
308
|
+
id: 'f1',
|
|
309
|
+
severity: 'medium',
|
|
310
|
+
title: 'Medium',
|
|
311
|
+
description: 'Details',
|
|
312
|
+
location: { path: 'src/a.ts', startLine: 1 },
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const criticalResult = renderSkillReport(criticalReport);
|
|
318
|
+
const highResult = renderSkillReport(highReport);
|
|
319
|
+
const mediumResult = renderSkillReport(mediumReport);
|
|
320
|
+
expect(criticalResult.review!.event).toBe('REQUEST_CHANGES');
|
|
321
|
+
expect(highResult.review!.event).toBe('REQUEST_CHANGES');
|
|
322
|
+
expect(mediumResult.review!.event).toBe('COMMENT');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('respects maxFindings option', () => {
|
|
326
|
+
const report: SkillReport = {
|
|
327
|
+
...baseReport,
|
|
328
|
+
findings: Array.from({ length: 10 }, (_, i) => ({
|
|
329
|
+
id: `f${i}`,
|
|
330
|
+
severity: 'info' as const,
|
|
331
|
+
title: `Finding ${i}`,
|
|
332
|
+
description: 'Details',
|
|
333
|
+
location: { path: 'src/a.ts', startLine: i + 1 },
|
|
334
|
+
})),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const result = renderSkillReport(report, { maxFindings: 3 });
|
|
338
|
+
|
|
339
|
+
expect(result.review!.comments).toHaveLength(3);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('handles findings without location', () => {
|
|
343
|
+
const report: SkillReport = {
|
|
344
|
+
...baseReport,
|
|
345
|
+
findings: [
|
|
346
|
+
{
|
|
347
|
+
id: 'f1',
|
|
348
|
+
severity: 'medium',
|
|
349
|
+
title: 'General Issue',
|
|
350
|
+
description: 'Applies to whole project',
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const result = renderSkillReport(report);
|
|
356
|
+
|
|
357
|
+
expect(result.review).toBeUndefined();
|
|
358
|
+
expect(result.summaryComment).toContain('General Issue');
|
|
359
|
+
expect(result.summaryComment).toContain('General');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe('commentOn filtering', () => {
|
|
363
|
+
it('filters findings by commentOn threshold', () => {
|
|
364
|
+
const report: SkillReport = {
|
|
365
|
+
...baseReport,
|
|
366
|
+
findings: [
|
|
367
|
+
{
|
|
368
|
+
id: 'f1',
|
|
369
|
+
severity: 'critical',
|
|
370
|
+
title: 'Critical Issue',
|
|
371
|
+
description: 'Critical details',
|
|
372
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 'f2',
|
|
376
|
+
severity: 'high',
|
|
377
|
+
title: 'High Issue',
|
|
378
|
+
description: 'High details',
|
|
379
|
+
location: { path: 'src/a.ts', startLine: 20 },
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: 'f3',
|
|
383
|
+
severity: 'medium',
|
|
384
|
+
title: 'Medium Issue',
|
|
385
|
+
description: 'Medium details',
|
|
386
|
+
location: { path: 'src/a.ts', startLine: 30 },
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: 'f4',
|
|
390
|
+
severity: 'low',
|
|
391
|
+
title: 'Low Issue',
|
|
392
|
+
description: 'Low details',
|
|
393
|
+
location: { path: 'src/a.ts', startLine: 40 },
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// commentOn='high' should only include critical and high
|
|
399
|
+
const result = renderSkillReport(report, { commentOn: 'high' });
|
|
400
|
+
|
|
401
|
+
expect(result.review).toBeDefined();
|
|
402
|
+
expect(result.review!.comments).toHaveLength(2);
|
|
403
|
+
expect(result.review!.comments.map((c) => c.body)).toEqual([
|
|
404
|
+
expect.stringContaining('Critical Issue'),
|
|
405
|
+
expect.stringContaining('High Issue'),
|
|
406
|
+
]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('shows all findings when commentOn is not specified', () => {
|
|
410
|
+
const report: SkillReport = {
|
|
411
|
+
...baseReport,
|
|
412
|
+
findings: [
|
|
413
|
+
{
|
|
414
|
+
id: 'f1',
|
|
415
|
+
severity: 'critical',
|
|
416
|
+
title: 'Critical Issue',
|
|
417
|
+
description: 'Details',
|
|
418
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: 'f2',
|
|
422
|
+
severity: 'info',
|
|
423
|
+
title: 'Info Issue',
|
|
424
|
+
description: 'Details',
|
|
425
|
+
location: { path: 'src/a.ts', startLine: 20 },
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const result = renderSkillReport(report);
|
|
431
|
+
|
|
432
|
+
expect(result.review!.comments).toHaveLength(2);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('returns empty review when all findings are filtered out', () => {
|
|
436
|
+
const report: SkillReport = {
|
|
437
|
+
...baseReport,
|
|
438
|
+
findings: [
|
|
439
|
+
{
|
|
440
|
+
id: 'f1',
|
|
441
|
+
severity: 'low',
|
|
442
|
+
title: 'Low Issue',
|
|
443
|
+
description: 'Details',
|
|
444
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
id: 'f2',
|
|
448
|
+
severity: 'info',
|
|
449
|
+
title: 'Info Issue',
|
|
450
|
+
description: 'Details',
|
|
451
|
+
location: { path: 'src/a.ts', startLine: 20 },
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const result = renderSkillReport(report, { commentOn: 'high' });
|
|
457
|
+
|
|
458
|
+
expect(result.review).toBeUndefined();
|
|
459
|
+
expect(result.summaryComment).toContain('No findings to report');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('applies commentOn filter before maxFindings limit', () => {
|
|
463
|
+
const report: SkillReport = {
|
|
464
|
+
...baseReport,
|
|
465
|
+
findings: [
|
|
466
|
+
{
|
|
467
|
+
id: 'f1',
|
|
468
|
+
severity: 'critical',
|
|
469
|
+
title: 'Critical Issue',
|
|
470
|
+
description: 'Details',
|
|
471
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: 'f2',
|
|
475
|
+
severity: 'low',
|
|
476
|
+
title: 'Low Issue 1',
|
|
477
|
+
description: 'Details',
|
|
478
|
+
location: { path: 'src/a.ts', startLine: 20 },
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
id: 'f3',
|
|
482
|
+
severity: 'low',
|
|
483
|
+
title: 'Low Issue 2',
|
|
484
|
+
description: 'Details',
|
|
485
|
+
location: { path: 'src/a.ts', startLine: 30 },
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// With commentOn='high' and maxFindings=2, should only show critical (1 finding)
|
|
491
|
+
// because low findings are filtered out first
|
|
492
|
+
const result = renderSkillReport(report, { commentOn: 'high', maxFindings: 2 });
|
|
493
|
+
|
|
494
|
+
expect(result.review!.comments).toHaveLength(1);
|
|
495
|
+
expect(result.review!.comments[0]!.body).toContain('Critical Issue');
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe('stats footer', () => {
|
|
500
|
+
it('includes stats footer in summary comment when stats are available', () => {
|
|
501
|
+
const report: SkillReport = {
|
|
502
|
+
...baseReport,
|
|
503
|
+
findings: [
|
|
504
|
+
{
|
|
505
|
+
id: 'f1',
|
|
506
|
+
severity: 'medium',
|
|
507
|
+
title: 'Issue',
|
|
508
|
+
description: 'Details',
|
|
509
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
durationMs: 15800,
|
|
513
|
+
usage: {
|
|
514
|
+
inputTokens: 3000,
|
|
515
|
+
outputTokens: 680,
|
|
516
|
+
costUSD: 0.0048,
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const result = renderSkillReport(report);
|
|
521
|
+
|
|
522
|
+
expect(result.summaryComment).toContain('⏱ 15.8s');
|
|
523
|
+
expect(result.summaryComment).toContain('3.0k in / 680 out');
|
|
524
|
+
expect(result.summaryComment).toContain('$0.0048');
|
|
525
|
+
expect(result.summaryComment).toContain('<sub>');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('review body is empty (stats only in summary comment)', () => {
|
|
529
|
+
const report: SkillReport = {
|
|
530
|
+
...baseReport,
|
|
531
|
+
findings: [
|
|
532
|
+
{
|
|
533
|
+
id: 'f1',
|
|
534
|
+
severity: 'medium',
|
|
535
|
+
title: 'Issue',
|
|
536
|
+
description: 'Details',
|
|
537
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
durationMs: 12300,
|
|
541
|
+
usage: {
|
|
542
|
+
inputTokens: 2500,
|
|
543
|
+
outputTokens: 450,
|
|
544
|
+
costUSD: 0.0032,
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const result = renderSkillReport(report);
|
|
549
|
+
|
|
550
|
+
expect(result.review).toBeDefined();
|
|
551
|
+
expect(result.review!.body).toBe('');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('includes stats footer in empty findings report', () => {
|
|
555
|
+
const report: SkillReport = {
|
|
556
|
+
...baseReport,
|
|
557
|
+
findings: [],
|
|
558
|
+
durationMs: 8200,
|
|
559
|
+
usage: {
|
|
560
|
+
inputTokens: 1800,
|
|
561
|
+
outputTokens: 320,
|
|
562
|
+
costUSD: 0.0021,
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const result = renderSkillReport(report);
|
|
567
|
+
|
|
568
|
+
expect(result.summaryComment).toContain('No findings to report');
|
|
569
|
+
expect(result.summaryComment).toContain('⏱ 8.2s');
|
|
570
|
+
expect(result.summaryComment).toContain('1.8k in / 320 out');
|
|
571
|
+
expect(result.summaryComment).toContain('$0.0021');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('omits stats footer when no stats available', () => {
|
|
575
|
+
const report: SkillReport = {
|
|
576
|
+
...baseReport,
|
|
577
|
+
findings: [],
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const result = renderSkillReport(report);
|
|
581
|
+
|
|
582
|
+
expect(result.summaryComment).not.toContain('⏱');
|
|
583
|
+
expect(result.summaryComment).not.toContain('<sub>');
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
describe('check run link', () => {
|
|
588
|
+
it('shows link to full report when findings are filtered out', () => {
|
|
589
|
+
const report: SkillReport = {
|
|
590
|
+
...baseReport,
|
|
591
|
+
findings: [
|
|
592
|
+
{
|
|
593
|
+
id: 'f1',
|
|
594
|
+
severity: 'high',
|
|
595
|
+
title: 'High Issue',
|
|
596
|
+
description: 'Details',
|
|
597
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
id: 'f2',
|
|
601
|
+
severity: 'low',
|
|
602
|
+
title: 'Low Issue',
|
|
603
|
+
description: 'Details',
|
|
604
|
+
location: { path: 'src/a.ts', startLine: 20 },
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
id: 'f3',
|
|
608
|
+
severity: 'info',
|
|
609
|
+
title: 'Info Issue',
|
|
610
|
+
description: 'Details',
|
|
611
|
+
location: { path: 'src/a.ts', startLine: 30 },
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const result = renderSkillReport(report, {
|
|
617
|
+
commentOn: 'high',
|
|
618
|
+
checkRunUrl: 'https://github.com/owner/repo/runs/123',
|
|
619
|
+
totalFindings: 3,
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
expect(result.summaryComment).toContain('View 2 additional findings in Checks');
|
|
623
|
+
expect(result.summaryComment).toContain('https://github.com/owner/repo/runs/123');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('shows singular "finding" when only one is hidden', () => {
|
|
627
|
+
const report: SkillReport = {
|
|
628
|
+
...baseReport,
|
|
629
|
+
findings: [
|
|
630
|
+
{
|
|
631
|
+
id: 'f1',
|
|
632
|
+
severity: 'high',
|
|
633
|
+
title: 'High Issue',
|
|
634
|
+
description: 'Details',
|
|
635
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: 'f2',
|
|
639
|
+
severity: 'low',
|
|
640
|
+
title: 'Low Issue',
|
|
641
|
+
description: 'Details',
|
|
642
|
+
location: { path: 'src/a.ts', startLine: 20 },
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const result = renderSkillReport(report, {
|
|
648
|
+
commentOn: 'high',
|
|
649
|
+
checkRunUrl: 'https://github.com/owner/repo/runs/123',
|
|
650
|
+
totalFindings: 2,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
expect(result.summaryComment).toContain('View 1 additional finding in Checks');
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('shows link when all findings filtered out', () => {
|
|
657
|
+
const report: SkillReport = {
|
|
658
|
+
...baseReport,
|
|
659
|
+
findings: [
|
|
660
|
+
{
|
|
661
|
+
id: 'f1',
|
|
662
|
+
severity: 'low',
|
|
663
|
+
title: 'Low Issue',
|
|
664
|
+
description: 'Details',
|
|
665
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const result = renderSkillReport(report, {
|
|
671
|
+
commentOn: 'high',
|
|
672
|
+
checkRunUrl: 'https://github.com/owner/repo/runs/123',
|
|
673
|
+
totalFindings: 1,
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
expect(result.summaryComment).toContain('No findings to report');
|
|
677
|
+
expect(result.summaryComment).toContain('View 1 additional finding in Checks');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('does not show link when no checkRunUrl provided', () => {
|
|
681
|
+
const report: SkillReport = {
|
|
682
|
+
...baseReport,
|
|
683
|
+
findings: [
|
|
684
|
+
{
|
|
685
|
+
id: 'f1',
|
|
686
|
+
severity: 'high',
|
|
687
|
+
title: 'High Issue',
|
|
688
|
+
description: 'Details',
|
|
689
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
690
|
+
},
|
|
691
|
+
],
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const result = renderSkillReport(report, {
|
|
695
|
+
commentOn: 'high',
|
|
696
|
+
totalFindings: 3,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
expect(result.summaryComment).not.toContain('View');
|
|
700
|
+
expect(result.summaryComment).not.toContain('additional finding');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('does not show link when no findings are hidden', () => {
|
|
704
|
+
const report: SkillReport = {
|
|
705
|
+
...baseReport,
|
|
706
|
+
findings: [
|
|
707
|
+
{
|
|
708
|
+
id: 'f1',
|
|
709
|
+
severity: 'high',
|
|
710
|
+
title: 'High Issue',
|
|
711
|
+
description: 'Details',
|
|
712
|
+
location: { path: 'src/a.ts', startLine: 10 },
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const result = renderSkillReport(report, {
|
|
718
|
+
commentOn: 'high',
|
|
719
|
+
checkRunUrl: 'https://github.com/owner/repo/runs/123',
|
|
720
|
+
totalFindings: 1,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
expect(result.summaryComment).not.toContain('View');
|
|
724
|
+
expect(result.summaryComment).not.toContain('additional finding');
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
});
|