pr-prism 1.1.3 → 1.1.6
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/.github/dependabot.yml +26 -0
- package/.github/workflows/ci.yml +1 -0
- package/.github/workflows/dependabot-auto-merge.yml +4 -10
- package/package.json +12 -6
- package/scripts/lib/sanitize.test.ts +545 -0
- package/scripts/lib/sanitize.ts +152 -0
- package/scripts/lib/scrape-issues.ts +204 -0
- package/scripts/lib/shared.test.ts +20 -0
- package/scripts/lib/shared.ts +74 -0
- package/scripts/pr-prism.ts +260 -0
- package/scripts/resolve-pr-threads.ts +59 -12
- package/scripts/scrape-pr-reviews.ts +47 -42
- package/.github/workflows/release.yml +0 -123
- package/pr-reviews/.gitkeep +0 -0
- package/pr-reviews/.scraped-ids.json +0 -34
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "npm"
|
|
4
|
+
directory: "/"
|
|
5
|
+
target-branch: "main"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "weekly"
|
|
8
|
+
day: "monday"
|
|
9
|
+
time: "09:00"
|
|
10
|
+
timezone: "Etc/UTC"
|
|
11
|
+
open-pull-requests-limit: 10
|
|
12
|
+
groups:
|
|
13
|
+
all-dependencies:
|
|
14
|
+
update-types:
|
|
15
|
+
- "minor"
|
|
16
|
+
- "patch"
|
|
17
|
+
|
|
18
|
+
- package-ecosystem: "github-actions"
|
|
19
|
+
directory: "/"
|
|
20
|
+
target-branch: "main"
|
|
21
|
+
schedule:
|
|
22
|
+
interval: "weekly"
|
|
23
|
+
day: "monday"
|
|
24
|
+
time: "09:00"
|
|
25
|
+
timezone: "Etc/UTC"
|
|
26
|
+
open-pull-requests-limit: 5
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -10,7 +10,7 @@ permissions:
|
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
12
|
auto-merge:
|
|
13
|
-
if: github.
|
|
13
|
+
if: github.actor == 'dependabot[bot]'
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
15
|
|
|
16
16
|
steps:
|
|
@@ -20,19 +20,13 @@ jobs:
|
|
|
20
20
|
with:
|
|
21
21
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
22
22
|
|
|
23
|
-
- name: Auto-approve
|
|
24
|
-
if: >
|
|
25
|
-
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
|
|
26
|
-
steps.metadata.outputs.update-type == 'version-update:semver-patch'
|
|
23
|
+
- name: Auto-approve
|
|
27
24
|
env:
|
|
28
25
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
29
26
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
30
|
-
run: gh pr review "$PR_URL" --approve
|
|
27
|
+
run: 'gh pr review "$PR_URL" --approve --body "Auto-approved: Dependabot ${{ steps.metadata.outputs.update-type }} update"'
|
|
31
28
|
|
|
32
|
-
- name: Auto-merge
|
|
33
|
-
if: >
|
|
34
|
-
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
|
|
35
|
-
steps.metadata.outputs.update-type == 'version-update:semver-patch'
|
|
29
|
+
- name: Auto-merge
|
|
36
30
|
env:
|
|
37
31
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
38
32
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-prism",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.6",
|
|
4
|
+
"description": "Strip noise from GitHub PRs and issues for LLM agents. Scrape reviews, resolve threads, save ~40%+ tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"pr-prism": "scripts/pr-prism.ts",
|
|
7
8
|
"pr-review": "scripts/scrape-pr-reviews.ts",
|
|
8
9
|
"pr-resolve": "scripts/resolve-pr-threads.ts"
|
|
9
10
|
},
|
|
@@ -13,10 +14,11 @@
|
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
15
16
|
"@types/node": "^20.10.6",
|
|
16
|
-
"typescript": "^5.3.3"
|
|
17
|
+
"typescript": "^5.3.3",
|
|
18
|
+
"vitest": "^4.1.0"
|
|
17
19
|
},
|
|
18
20
|
"engines": {
|
|
19
|
-
"node": "
|
|
21
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
20
22
|
},
|
|
21
23
|
"license": "MIT",
|
|
22
24
|
"repository": {
|
|
@@ -28,8 +30,12 @@
|
|
|
28
30
|
"url": "https://github.com/YosefHayim/pr-prism/issues"
|
|
29
31
|
},
|
|
30
32
|
"scripts": {
|
|
31
|
-
"pr-
|
|
32
|
-
"pr-
|
|
33
|
+
"pr-prism": "tsx scripts/pr-prism.ts",
|
|
34
|
+
"pr-review": "tsx scripts/pr-prism.ts pr",
|
|
35
|
+
"pr-resolve": "tsx scripts/pr-prism.ts resolve",
|
|
36
|
+
"issue-review": "tsx scripts/pr-prism.ts issue",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
33
39
|
"typecheck": "tsc --noEmit"
|
|
34
40
|
}
|
|
35
41
|
}
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { stripNoise, renderSuggestions, isBot, estimateTokens, formatTokenSummary, KNOWN_BOTS, NOISE_DOMAINS } from "./sanitize.js";
|
|
3
|
+
|
|
4
|
+
describe("isBot", () => {
|
|
5
|
+
it("detects [bot] suffix", () => {
|
|
6
|
+
expect(isBot("renovate[bot]")).toBe(true);
|
|
7
|
+
expect(isBot("SomeBot[bot]")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("detects known bot names", () => {
|
|
11
|
+
for (const bot of KNOWN_BOTS) {
|
|
12
|
+
expect(isBot(bot)).toBe(true);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("is case-insensitive", () => {
|
|
17
|
+
expect(isBot("CodeRabbitAI")).toBe(true);
|
|
18
|
+
expect(isBot("DEPENDABOT")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects human logins", () => {
|
|
22
|
+
expect(isBot("octocat")).toBe(false);
|
|
23
|
+
expect(isBot("YosefHayim")).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("renderSuggestions", () => {
|
|
28
|
+
it("converts suggestion blocks to diff format", () => {
|
|
29
|
+
const input = "before\n```suggestion\nconst x = 1;\nconst y = 2;\n```\nafter";
|
|
30
|
+
const result = renderSuggestions(input);
|
|
31
|
+
expect(result).toContain("**SUGGESTED CHANGE:**");
|
|
32
|
+
expect(result).toContain("```diff");
|
|
33
|
+
expect(result).toContain("+ const x = 1;");
|
|
34
|
+
expect(result).toContain("+ const y = 2;");
|
|
35
|
+
expect(result).toContain("before");
|
|
36
|
+
expect(result).toContain("after");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("handles multiple suggestion blocks", () => {
|
|
40
|
+
const input = "```suggestion\nfoo\n```\ntext\n```suggestion\nbar\n```";
|
|
41
|
+
const result = renderSuggestions(input);
|
|
42
|
+
expect(result).toContain("+ foo");
|
|
43
|
+
expect(result).toContain("+ bar");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles CRLF line endings in suggestion blocks", () => {
|
|
47
|
+
const input = "before\r\n```suggestion\r\nconst x = 1;\r\n```\r\nafter";
|
|
48
|
+
const result = renderSuggestions(input);
|
|
49
|
+
expect(result).toContain("**SUGGESTED CHANGE:**");
|
|
50
|
+
expect(result).toContain("+ const x = 1;");
|
|
51
|
+
const diffBlock = result.match(/```diff\n([\s\S]*?)```/)?.[1] ?? "";
|
|
52
|
+
expect(diffBlock).not.toContain("\r");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("leaves non-suggestion code blocks untouched", () => {
|
|
56
|
+
const input = "```typescript\nconst x = 1;\n```";
|
|
57
|
+
expect(renderSuggestions(input)).toBe(input);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("stripNoise", () => {
|
|
62
|
+
describe("HTML comments", () => {
|
|
63
|
+
it("strips metadata comments", () => {
|
|
64
|
+
const input = '<!-- metadata:{"confidence":9} -->\nP1: some issue';
|
|
65
|
+
expect(stripNoise(input)).toBe("P1: some issue");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("strips multi-line attribution comments", () => {
|
|
69
|
+
const input = "text\n<!-- cubic:attribution IMPORTANT: This code review was authored by cubic -->\nmore text";
|
|
70
|
+
const result = stripNoise(input);
|
|
71
|
+
expect(result).not.toContain("cubic:attribution");
|
|
72
|
+
expect(result).toContain("text");
|
|
73
|
+
expect(result).toContain("more text");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("<img> tags", () => {
|
|
78
|
+
it("strips self-closing img", () => {
|
|
79
|
+
const input = '<img src="https://example.com/icon.png" height="20" alt="icon" />';
|
|
80
|
+
expect(stripNoise(input)).toBe("");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("strips non-self-closing img", () => {
|
|
84
|
+
const input = '<img src="https://qodo.ai/wp-content/uploads/action-required.png" height="20" alt="Action required">';
|
|
85
|
+
expect(stripNoise(input)).toBe("");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("strips img from surrounding text", () => {
|
|
89
|
+
const input = 'before\n<img src="https://example.com/divider.svg" height="10%" alt="Divider">\nafter';
|
|
90
|
+
const result = stripNoise(input);
|
|
91
|
+
expect(result).not.toContain("<img");
|
|
92
|
+
expect(result).toContain("before");
|
|
93
|
+
expect(result).toContain("after");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("<b>, <i>, <strong>, <em>, <ins> tags", () => {
|
|
98
|
+
it("unwraps <b><i> tags", () => {
|
|
99
|
+
expect(stripNoise("<b><i>logger.ts</i></b>")).toBe("logger.ts");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("unwraps <strong> tags", () => {
|
|
103
|
+
expect(stripNoise("<strong>important text</strong>")).toBe("important text");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("unwraps <em> tags", () => {
|
|
107
|
+
expect(stripNoise("<em>emphasis</em>")).toBe("emphasis");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("unwraps <ins> tags", () => {
|
|
111
|
+
expect(stripNoise("<ins><strong>active content</strong></ins>")).toBe("active content");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("<details> and <summary> tags", () => {
|
|
116
|
+
it("unwraps details/summary", () => {
|
|
117
|
+
const input = "<details>\n<summary>Click to expand</summary>\ncontent here\n</details>";
|
|
118
|
+
const result = stripNoise(input);
|
|
119
|
+
expect(result).not.toContain("<details");
|
|
120
|
+
expect(result).not.toContain("<summary");
|
|
121
|
+
expect(result).not.toContain("</details");
|
|
122
|
+
expect(result).not.toContain("</summary");
|
|
123
|
+
expect(result).toContain("Click to expand");
|
|
124
|
+
expect(result).toContain("content here");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("handles <details open>", () => {
|
|
128
|
+
const input = "<details open>\n<summary>Click to expand</summary>\ntext\n</details>";
|
|
129
|
+
const result = stripNoise(input);
|
|
130
|
+
expect(result).not.toContain("<details");
|
|
131
|
+
expect(result).toContain("Click to expand");
|
|
132
|
+
expect(result).toContain("text");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("<pre> tags", () => {
|
|
137
|
+
it("unwraps <pre> blocks", () => {
|
|
138
|
+
const input = "<pre>\nSome preformatted content\n</pre>";
|
|
139
|
+
const result = stripNoise(input);
|
|
140
|
+
expect(result).not.toContain("<pre>");
|
|
141
|
+
expect(result).not.toContain("</pre>");
|
|
142
|
+
expect(result).toContain("Some preformatted content");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("<code> tags", () => {
|
|
147
|
+
it("converts <code> to backticks", () => {
|
|
148
|
+
expect(stripNoise("<code>Rule violation</code>")).toBe("`Rule violation`");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("converts multiple <code> tags", () => {
|
|
152
|
+
const input = '<code>Rule violation</code> <code>Reliability</code>';
|
|
153
|
+
expect(stripNoise(input)).toBe("`Rule violation` `Reliability`");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("<picture> blocks", () => {
|
|
158
|
+
it("strips entire picture blocks", () => {
|
|
159
|
+
const input = `<picture>
|
|
160
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cubic.dev/buttons/dark.svg">
|
|
161
|
+
<source media="(prefers-color-scheme: light)" srcset="https://cubic.dev/buttons/light.svg">
|
|
162
|
+
<img alt="Fix" src="https://cubic.dev/buttons/dark.svg">
|
|
163
|
+
</picture>`;
|
|
164
|
+
expect(stripNoise(input)).toBe("");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("<sub> and <sup> blocks", () => {
|
|
169
|
+
it("strips <sub> promo text", () => {
|
|
170
|
+
const input = '<sub>You\'re on the free plan. <a href="https://cubic.dev/settings">Upgrade</a></sub>';
|
|
171
|
+
expect(stripNoise(input)).toBe("");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("strips <sup> text", () => {
|
|
175
|
+
const input = '<sup>GitGuardian detects secrets</sup>';
|
|
176
|
+
expect(stripNoise(input)).toBe("");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("<table> tags", () => {
|
|
181
|
+
it("unwraps table markup preserving cell boundaries", () => {
|
|
182
|
+
const input = "<table>\n<tr><td>Name</td><td>Status</td></tr>\n</table>";
|
|
183
|
+
const result = stripNoise(input);
|
|
184
|
+
expect(result).not.toContain("<table");
|
|
185
|
+
expect(result).not.toContain("<tr");
|
|
186
|
+
expect(result).not.toContain("<td");
|
|
187
|
+
expect(result).toContain("Name");
|
|
188
|
+
expect(result).toContain("Status");
|
|
189
|
+
expect(result).not.toContain("NameStatus");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("heading tags <h1>-<h6>", () => {
|
|
194
|
+
it("unwraps <h3> tags", () => {
|
|
195
|
+
expect(stripNoise("<h3>Custom heading text</h3>")).toBe("Custom heading text");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("strips bot-specific header content after unwrap", () => {
|
|
199
|
+
expect(stripNoise("<h3>Review Summary by Qodo</h3>")).toBe("");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("<br> and <hr> tags", () => {
|
|
204
|
+
it("converts <br> to newline", () => {
|
|
205
|
+
expect(stripNoise("line1<br>line2")).toBe("line1\nline2");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("converts <br/> to newline", () => {
|
|
209
|
+
expect(stripNoise("line1<br/>line2")).toBe("line1\nline2");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("removes <hr/>", () => {
|
|
213
|
+
expect(stripNoise("before<hr/>after")).toBe("beforeafter");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("copy-prompt instruction lines (ⓘ)", () => {
|
|
218
|
+
it("strips lines containing ⓘ", () => {
|
|
219
|
+
const input = "useful content\n\u24D8 Copy this prompt and use it to remediate\nmore useful content";
|
|
220
|
+
const result = stripNoise(input);
|
|
221
|
+
expect(result).not.toContain("\u24D8");
|
|
222
|
+
expect(result).not.toContain("Copy this prompt");
|
|
223
|
+
expect(result).toContain("useful content");
|
|
224
|
+
expect(result).toContain("more useful content");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("review-bot custom XML tags", () => {
|
|
229
|
+
it("strips <file context> tags, keeps content", () => {
|
|
230
|
+
const input = '<file context>\n@@ -22,21 +22,10 @@\n-old\n+new\n</file context>';
|
|
231
|
+
const result = stripNoise(input);
|
|
232
|
+
expect(result).not.toContain("<file context>");
|
|
233
|
+
expect(result).not.toContain("</file context>");
|
|
234
|
+
expect(result).toContain("@@ -22,21 +22,10 @@");
|
|
235
|
+
expect(result).toContain("-old");
|
|
236
|
+
expect(result).toContain("+new");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("strips <comment> tags, keeps content", () => {
|
|
240
|
+
const input = "<comment>CORS is misconfigured</comment>";
|
|
241
|
+
const result = stripNoise(input);
|
|
242
|
+
expect(result).not.toContain("<comment>");
|
|
243
|
+
expect(result).toContain("CORS is misconfigured");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("strips <violation> tags, keeps content", () => {
|
|
247
|
+
const input = '<violation number="1" location="src/index.ts:25">\nP0: CORS is open</violation>';
|
|
248
|
+
const result = stripNoise(input);
|
|
249
|
+
expect(result).not.toContain("<violation");
|
|
250
|
+
expect(result).not.toContain("</violation>");
|
|
251
|
+
expect(result).toContain("P0: CORS is open");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("strips <file name> tags, keeps content", () => {
|
|
255
|
+
const input = '<file name="backend/src/index.ts">\ncontent\n</file>';
|
|
256
|
+
const result = stripNoise(input);
|
|
257
|
+
expect(result).not.toContain('<file name=');
|
|
258
|
+
expect(result).not.toContain("</file>");
|
|
259
|
+
expect(result).toContain("content");
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("noise-domain links", () => {
|
|
264
|
+
it("strips HTML links to noise domains", () => {
|
|
265
|
+
const input = '<a href="https://www.cubic.dev/action/fix/abc123">Fix it</a>';
|
|
266
|
+
expect(stripNoise(input)).toBe("");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("strips markdown links to noise domains", () => {
|
|
270
|
+
for (const domain of NOISE_DOMAINS) {
|
|
271
|
+
const input = `[click](https://${domain}/something)`;
|
|
272
|
+
expect(stripNoise(input)).toBe("");
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("unwraps non-noise links to plain text", () => {
|
|
277
|
+
const input = '<a href="https://github.com/user/repo">repo link</a>';
|
|
278
|
+
const result = stripNoise(input);
|
|
279
|
+
expect(result).toBe("repo link");
|
|
280
|
+
expect(result).not.toContain("<a");
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("empty <a> tags after image removal", () => {
|
|
285
|
+
it("strips <a> wrapping only images", () => {
|
|
286
|
+
const input = '<a href="https://example.com">\n <img src="pic.png">\n</a>';
|
|
287
|
+
const result = stripNoise(input);
|
|
288
|
+
expect(result).toBe("");
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("whitespace cleanup", () => {
|
|
293
|
+
it("collapses 3+ newlines to 2", () => {
|
|
294
|
+
expect(stripNoise("a\n\n\n\nb")).toBe("a\n\nb");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("strips separator-only lines", () => {
|
|
298
|
+
expect(stripNoise("a\n---\nb")).toBe("a\n\nb");
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe("emoji stripping", () => {
|
|
303
|
+
it("strips all emoji from text", () => {
|
|
304
|
+
const input = "📄 File: `test.ts` 🐞 Bug ✨ Enhancement ⚠️ Warning";
|
|
305
|
+
const result = stripNoise(input);
|
|
306
|
+
expect(result).not.toMatch(/[\p{Extended_Pictographic}]/u);
|
|
307
|
+
expect(result).toContain("File:");
|
|
308
|
+
expect(result).toContain("Bug");
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe("mermaid diagrams", () => {
|
|
313
|
+
it("strips mermaid blocks entirely", () => {
|
|
314
|
+
const input = "before\n```mermaid\nflowchart LR\n A --> B\n```\nafter";
|
|
315
|
+
const result = stripNoise(input);
|
|
316
|
+
expect(result).not.toContain("mermaid");
|
|
317
|
+
expect(result).not.toContain("flowchart");
|
|
318
|
+
expect(result).toContain("before");
|
|
319
|
+
expect(result).toContain("after");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe("block quote markers", () => {
|
|
324
|
+
it("strips > at line start", () => {
|
|
325
|
+
const input = "> quoted text\n> more quoted\nnormal text";
|
|
326
|
+
const result = stripNoise(input);
|
|
327
|
+
expect(result).toContain("quoted text");
|
|
328
|
+
expect(result).toContain("normal text");
|
|
329
|
+
expect(result).not.toMatch(/^>/m);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("promotional noise", () => {
|
|
334
|
+
it("strips CodeAnt promo text", () => {
|
|
335
|
+
const input = "Thanks for using CodeAnt! 🎉\n\nWe're free for open-source projects. if you're enjoying it, help us grow by sharing.";
|
|
336
|
+
const result = stripNoise(input);
|
|
337
|
+
expect(result).toBe("");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("strips bot status messages", () => {
|
|
341
|
+
expect(stripNoise("CodeAnt AI is reviewing your PR.")).toBe("");
|
|
342
|
+
expect(stripNoise("CodeAnt AI finished reviewing your PR.")).toBe("");
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe("strikethrough resolved items", () => {
|
|
347
|
+
it("strips <s>...</s> content", () => {
|
|
348
|
+
const input = "1. <s>CRLF not rendered</s> fixed\n2. Active issue";
|
|
349
|
+
const result = stripNoise(input);
|
|
350
|
+
expect(result).not.toContain("CRLF not rendered");
|
|
351
|
+
expect(result).toContain("Active issue");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("GitHub blob/diff links", () => {
|
|
356
|
+
it("keeps link text from GitHub file reference links", () => {
|
|
357
|
+
const input = "[sanitize.ts](https://github.com/user/repo/blob/abc123/scripts/lib/sanitize.ts/#L28-L36)";
|
|
358
|
+
expect(stripNoise(input)).toBe("sanitize.ts");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("keeps link text from GitHub diff links", () => {
|
|
362
|
+
const input = "[sanitize.ts](https://github.com/user/repo/pull/6/files#diff-ff1b9dfe)";
|
|
363
|
+
expect(stripNoise(input)).toBe("sanitize.ts");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("HTML entities", () => {
|
|
368
|
+
it("decodes common entities", () => {
|
|
369
|
+
expect(stripNoise("& < > "")).toBe('& < > "');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("section headers", () => {
|
|
374
|
+
it("strips standalone section headers", () => {
|
|
375
|
+
const input = "Agent prompt\n\nActual content here\n\nReview Summary by Qodo\n\nMore content";
|
|
376
|
+
const result = stripNoise(input);
|
|
377
|
+
expect(result).not.toMatch(/^Agent prompt$/m);
|
|
378
|
+
expect(result).not.toMatch(/^Review Summary by Qodo$/m);
|
|
379
|
+
expect(result).toContain("Actual content here");
|
|
380
|
+
expect(result).toContain("More content");
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("real-world Qodo comment body", () => {
|
|
385
|
+
it("strips all HTML noise from a full Qodo review comment", () => {
|
|
386
|
+
const qodoBody = [
|
|
387
|
+
'<img src="https://www.qodo.ai/wp-content/uploads/2026/01/action-required.png" height="20" alt="Action required">',
|
|
388
|
+
"",
|
|
389
|
+
'1\\. <b><i>i18n/index.ts</i></b> contains dynamic import <code>Rule violation</code> <code>Reliability</code>',
|
|
390
|
+
"",
|
|
391
|
+
"<pre>",
|
|
392
|
+
'The new <b><i>backend/.../i18n/index.ts</i></b> contains executable logic.',
|
|
393
|
+
"</pre>",
|
|
394
|
+
"",
|
|
395
|
+
"<details>",
|
|
396
|
+
"<summary><strong>Agent Prompt</strong></summary>",
|
|
397
|
+
"",
|
|
398
|
+
"```",
|
|
399
|
+
"## Issue description",
|
|
400
|
+
"`index.ts` is an index file but contains executable logic.",
|
|
401
|
+
"```",
|
|
402
|
+
"",
|
|
403
|
+
'<code>\u24D8 Copy this prompt and use it to remediate the issue with your preferred AI generation tools</code>',
|
|
404
|
+
"</details>",
|
|
405
|
+
].join("\n");
|
|
406
|
+
|
|
407
|
+
const result = stripNoise(qodoBody);
|
|
408
|
+
|
|
409
|
+
expect(result).not.toContain("<img");
|
|
410
|
+
expect(result).not.toContain("<b>");
|
|
411
|
+
expect(result).not.toContain("<i>");
|
|
412
|
+
expect(result).not.toContain("<pre>");
|
|
413
|
+
expect(result).not.toContain("<details");
|
|
414
|
+
expect(result).not.toContain("<summary");
|
|
415
|
+
expect(result).not.toContain("<strong");
|
|
416
|
+
expect(result).not.toContain("<code>");
|
|
417
|
+
expect(result).not.toContain("\u24D8");
|
|
418
|
+
|
|
419
|
+
expect(result).toContain("i18n/index.ts");
|
|
420
|
+
expect(result).toContain("contains executable logic");
|
|
421
|
+
expect(result).toContain("## Issue description");
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe("real-world Cubic comment body", () => {
|
|
426
|
+
it("strips all HTML noise from a full Cubic review comment", () => {
|
|
427
|
+
const cubicBody = [
|
|
428
|
+
'<!-- metadata:{"confidence":10} -->',
|
|
429
|
+
'P0: `origin: true` with `credentials: true` allows any website to make authenticated requests.',
|
|
430
|
+
"",
|
|
431
|
+
"<details>",
|
|
432
|
+
"<summary>Prompt for AI agents</summary>",
|
|
433
|
+
"",
|
|
434
|
+
"```text",
|
|
435
|
+
"Check if this issue is valid. At backend/src/index.ts, line 25:",
|
|
436
|
+
"",
|
|
437
|
+
"<comment>Restore the origin validation.</comment>",
|
|
438
|
+
"",
|
|
439
|
+
"<file context>",
|
|
440
|
+
"@@ -22,21 +22,10 @@",
|
|
441
|
+
"+ origin: true,",
|
|
442
|
+
"</file context>",
|
|
443
|
+
"```",
|
|
444
|
+
"",
|
|
445
|
+
"</details>",
|
|
446
|
+
"",
|
|
447
|
+
'<a href="https://www.cubic.dev/action/fix/violation/abc123" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true">',
|
|
448
|
+
" <picture>",
|
|
449
|
+
' <source media="(prefers-color-scheme: dark)" srcset="https://cubic.dev/buttons/fix-with-cubic-dark.svg">',
|
|
450
|
+
' <img alt="Fix with Cubic" src="https://cubic.dev/buttons/fix-with-cubic-dark.svg">',
|
|
451
|
+
" </picture>",
|
|
452
|
+
"</a>",
|
|
453
|
+
].join("\n");
|
|
454
|
+
|
|
455
|
+
const result = stripNoise(cubicBody);
|
|
456
|
+
|
|
457
|
+
expect(result).not.toContain("<!-- metadata");
|
|
458
|
+
expect(result).not.toContain("<details");
|
|
459
|
+
expect(result).not.toContain("<summary");
|
|
460
|
+
expect(result).not.toContain("<comment>");
|
|
461
|
+
expect(result).not.toContain("<file context>");
|
|
462
|
+
expect(result).not.toContain("<picture>");
|
|
463
|
+
expect(result).not.toContain("<img");
|
|
464
|
+
expect(result).not.toContain("<a href");
|
|
465
|
+
expect(result).not.toContain("cubic.dev");
|
|
466
|
+
|
|
467
|
+
expect(result).toContain("P0:");
|
|
468
|
+
expect(result).toContain("origin: true");
|
|
469
|
+
expect(result).toContain("Restore the origin validation.");
|
|
470
|
+
expect(result).toContain("@@ -22,21 +22,10 @@");
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("token estimation", () => {
|
|
475
|
+
it("estimateTokens returns ~chars/4", () => {
|
|
476
|
+
expect(estimateTokens("")).toBe(0);
|
|
477
|
+
expect(estimateTokens("abcd")).toBe(1);
|
|
478
|
+
expect(estimateTokens("abcde")).toBe(2);
|
|
479
|
+
expect(estimateTokens("a".repeat(100))).toBe(25);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("formatTokenSummary shows compact stats", () => {
|
|
483
|
+
const summary = formatTokenSummary(8000, 1400);
|
|
484
|
+
expect(summary).toMatch(/Tokens: 2[,.\s\u00A0]?000 raw > 350 clean/);
|
|
485
|
+
expect(summary).toContain("83% saved");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("formatTokenSummary handles zero raw", () => {
|
|
489
|
+
const summary = formatTokenSummary(0, 0);
|
|
490
|
+
expect(summary).toContain("0% saved");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("formatTokenSummary clamps negative savings to zero", () => {
|
|
494
|
+
const summary = formatTokenSummary(100, 200);
|
|
495
|
+
expect(summary).toContain("0% saved");
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe("real-world Cubic summary block", () => {
|
|
500
|
+
it("strips the full unresolved-issues summary", () => {
|
|
501
|
+
const summaryBody = [
|
|
502
|
+
"**22 issues found** across 978 files",
|
|
503
|
+
"",
|
|
504
|
+
"Confidence score: **1/5**",
|
|
505
|
+
"- Merge risk is critical.",
|
|
506
|
+
"",
|
|
507
|
+
"<details>",
|
|
508
|
+
"<summary>Prompt for AI agents (unresolved issues)</summary>",
|
|
509
|
+
"",
|
|
510
|
+
"```text",
|
|
511
|
+
"Check if these issues are valid.",
|
|
512
|
+
"",
|
|
513
|
+
'<file name=".github/workflows/release.yml">',
|
|
514
|
+
"",
|
|
515
|
+
'<violation number="1" location=".github/workflows/release.yml:54">',
|
|
516
|
+
"P1: AI Conversation Navigator is deployed later but never packaged.</violation>",
|
|
517
|
+
"</file>",
|
|
518
|
+
"```",
|
|
519
|
+
"",
|
|
520
|
+
"</details>",
|
|
521
|
+
"",
|
|
522
|
+
'<sub>You\'re on the cubic free plan with 6 free PR reviews remaining. <a href="https://cubic.dev/settings?tab=subscription">Upgrade</a></sub>',
|
|
523
|
+
"",
|
|
524
|
+
'<sub>Reply with feedback. Tag `@cubic-dev-ai` to re-run.</sub>',
|
|
525
|
+
"",
|
|
526
|
+
'<!-- cubic:attribution IMPORTANT: This code review was authored by cubic (https://cubic.dev). -->',
|
|
527
|
+
].join("\n");
|
|
528
|
+
|
|
529
|
+
const result = stripNoise(summaryBody);
|
|
530
|
+
|
|
531
|
+
expect(result).not.toContain("<details");
|
|
532
|
+
expect(result).not.toContain("<summary");
|
|
533
|
+
expect(result).not.toContain("<violation");
|
|
534
|
+
expect(result).not.toContain("<file name=");
|
|
535
|
+
expect(result).not.toContain("<sub>");
|
|
536
|
+
expect(result).not.toContain("cubic:attribution");
|
|
537
|
+
expect(result).not.toContain("free plan");
|
|
538
|
+
expect(result).not.toContain("Upgrade");
|
|
539
|
+
|
|
540
|
+
expect(result).toContain("**22 issues found**");
|
|
541
|
+
expect(result).toContain("P1:");
|
|
542
|
+
expect(result).toContain("never packaged");
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
});
|