coverage-check 0.2.1 → 0.2.2

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 (74) hide show
  1. package/README.md +9 -5
  2. package/bin/coverage-check.mjs +4 -0
  3. package/dist/src/cli.d.mts +1 -0
  4. package/dist/src/cli.mjs +14 -0
  5. package/dist/src/commands/check-args.d.mts +20 -0
  6. package/dist/src/commands/check-args.mjs +89 -0
  7. package/dist/src/commands/check.d.mts +4 -0
  8. package/dist/src/commands/check.mjs +128 -0
  9. package/dist/src/commands/store-put.d.mts +11 -0
  10. package/dist/src/commands/store-put.mjs +104 -0
  11. package/{src/coverage-check.mts → dist/src/coverage-check.d.mts} +1 -9
  12. package/dist/src/coverage-check.mjs +4 -0
  13. package/dist/src/diff-parser.d.mts +17 -0
  14. package/dist/src/diff-parser.mjs +127 -0
  15. package/dist/src/github-comment.d.mts +9 -0
  16. package/dist/src/github-comment.mjs +66 -0
  17. package/dist/src/lcov-merge.d.mts +5 -0
  18. package/dist/src/lcov-merge.mjs +29 -0
  19. package/dist/src/lcov-parser.d.mts +8 -0
  20. package/dist/src/lcov-parser.mjs +44 -0
  21. package/dist/src/load-artifacts.d.mts +9 -0
  22. package/dist/src/load-artifacts.mjs +41 -0
  23. package/dist/src/patch-coverage.d.mts +5 -0
  24. package/dist/src/patch-coverage.mjs +65 -0
  25. package/dist/src/report.d.mts +4 -0
  26. package/dist/src/report.mjs +65 -0
  27. package/dist/src/rules.d.mts +4 -0
  28. package/dist/src/rules.mjs +30 -0
  29. package/dist/src/s3-suite-store.d.mts +28 -0
  30. package/dist/src/s3-suite-store.mjs +147 -0
  31. package/dist/src/s3-utils.d.mts +2 -0
  32. package/dist/src/s3-utils.mjs +14 -0
  33. package/dist/src/step-summary.d.mts +9 -0
  34. package/dist/src/step-summary.mjs +70 -0
  35. package/dist/src/store-factory.d.mts +11 -0
  36. package/dist/src/store-factory.mjs +23 -0
  37. package/dist/src/suite-store.d.mts +51 -0
  38. package/dist/src/suite-store.mjs +154 -0
  39. package/dist/src/types.d.mts +36 -0
  40. package/dist/src/types.mjs +1 -0
  41. package/package.json +19 -5
  42. package/bin/coverage-check.mts +0 -6
  43. package/src/cli.mts +0 -15
  44. package/src/cli.test.mts +0 -45
  45. package/src/commands/check-args.mts +0 -110
  46. package/src/commands/check.mts +0 -147
  47. package/src/commands/check.test.mts +0 -870
  48. package/src/commands/store-put.mts +0 -115
  49. package/src/commands/store-put.test.mts +0 -248
  50. package/src/diff-parser.mts +0 -127
  51. package/src/diff-parser.test.mts +0 -178
  52. package/src/github-comment.mts +0 -79
  53. package/src/github-comment.test.mts +0 -63
  54. package/src/lcov-merge.mts +0 -34
  55. package/src/lcov-merge.test.mts +0 -57
  56. package/src/lcov-parser.mts +0 -46
  57. package/src/lcov-parser.test.mts +0 -86
  58. package/src/load-artifacts.mts +0 -42
  59. package/src/load-artifacts.test.mts +0 -115
  60. package/src/patch-coverage.mts +0 -82
  61. package/src/patch-coverage.test.mts +0 -91
  62. package/src/report.mts +0 -78
  63. package/src/report.test.mts +0 -142
  64. package/src/rules.mts +0 -34
  65. package/src/rules.test.mts +0 -98
  66. package/src/s3-suite-store.mts +0 -138
  67. package/src/s3-suite-store.test.mts +0 -308
  68. package/src/step-summary.mts +0 -89
  69. package/src/step-summary.test.mts +0 -189
  70. package/src/store-factory.mts +0 -23
  71. package/src/store-factory.test.mts +0 -67
  72. package/src/suite-store.mts +0 -112
  73. package/src/suite-store.test.mts +0 -209
  74. package/src/types.mts +0 -43
@@ -1,870 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
- import { main, runCheck } from "./check.mts";
7
- import { FileSystemSuiteStore } from "../suite-store.mts";
8
-
9
- describe("main argument validation", () => {
10
- it("returns exit code 2 on unknown flags", async () => {
11
- expect(await main(["--unknown-flag"])).toBe(2);
12
- });
13
-
14
- it("returns exit code 2 when --pr is not a number", async () => {
15
- expect(await main(["--pr", "abc"])).toBe(2);
16
- });
17
-
18
- it("returns exit code 2 when --pr is zero", async () => {
19
- expect(await main(["--pr", "0"])).toBe(2);
20
- });
21
-
22
- it("returns exit code 2 when --pr has trailing non-digit chars", async () => {
23
- expect(await main(["--pr", "42abc"])).toBe(2);
24
- });
25
-
26
- it("returns exit code 2 when --pr is a decimal", async () => {
27
- expect(await main(["--pr", "42.5"])).toBe(2);
28
- });
29
-
30
- it("returns exit code 2 when a flag is missing its value", async () => {
31
- expect(await main(["--rules"])).toBe(2);
32
- });
33
-
34
- it("returns exit code 2 when a flag token follows as the value (e.g. --rules --pr)", async () => {
35
- expect(await main(["--rules", "--pr"])).toBe(2);
36
- });
37
-
38
- it("returns exit code 2 when --pr is set but repo is empty", async () => {
39
- const saved = process.env["GITHUB_REPOSITORY"];
40
- delete process.env["GITHUB_REPOSITORY"];
41
- try {
42
- expect(await main(["--pr", "42"])).toBe(2);
43
- } finally {
44
- if (saved !== undefined) process.env["GITHUB_REPOSITORY"] = saved;
45
- else delete process.env["GITHUB_REPOSITORY"];
46
- }
47
- });
48
-
49
- it("uses fallback defaults when GITHUB_REPOSITORY/REF_NAME/STEP_SUMMARY are unset", async () => {
50
- const saved: Record<string, string | undefined> = {};
51
- for (const key of ["GITHUB_REPOSITORY", "GITHUB_REF_NAME", "GITHUB_STEP_SUMMARY"]) {
52
- saved[key] = process.env[key];
53
- delete process.env[key];
54
- }
55
- try {
56
- // Any call exercises the default-init lines; unknown flag triggers parse error
57
- expect(await main(["--unknown-flag"])).toBe(2);
58
- } finally {
59
- for (const [k, v] of Object.entries(saved)) {
60
- if (v !== undefined) process.env[k] = v;
61
- else delete process.env[k];
62
- }
63
- }
64
- });
65
- });
66
-
67
- describe("main integration", () => {
68
- let tmpDir: string;
69
- let rulesPath: string;
70
- let artifactsDir: string;
71
-
72
- beforeEach(() => {
73
- tmpDir = mkdtempSync(join(tmpdir(), "coverage-check-main-"));
74
- rulesPath = join(tmpDir, "rules.yml");
75
- artifactsDir = join(tmpDir, "artifacts");
76
- mkdirSync(artifactsDir);
77
- writeFileSync(rulesPath, "rules:\n - paths: backend/**\n patch_coverage_min: 90\n");
78
- });
79
-
80
- afterEach(() => {
81
- rmSync(tmpDir, { recursive: true, force: true });
82
- });
83
-
84
- it("returns 2 when rules file is missing", async () => {
85
- expect(
86
- await main(["--rules", join(tmpDir, "nonexistent.yml"), "--artifacts", artifactsDir]),
87
- ).toBe(2);
88
- });
89
-
90
- it("accepts --pr and --repo flags (parse succeeds, fails on missing rules)", async () => {
91
- expect(
92
- await main([
93
- "--rules",
94
- join(tmpDir, "nonexistent.yml"),
95
- "--pr",
96
- "42",
97
- "--repo",
98
- "owner/repo",
99
- "--artifacts",
100
- artifactsDir,
101
- ]),
102
- ).toBe(2);
103
- });
104
-
105
- it("accepts --strip-prefix flag", async () => {
106
- expect(
107
- await main([
108
- "--rules",
109
- join(tmpDir, "nonexistent.yml"),
110
- "--strip-prefix",
111
- "/some/path",
112
- "--artifacts",
113
- artifactsDir,
114
- ]),
115
- ).toBe(2);
116
- });
117
-
118
- it("accepts --suite flag", async () => {
119
- expect(
120
- await main([
121
- "--rules",
122
- join(tmpDir, "nonexistent.yml"),
123
- "--suite",
124
- "backend",
125
- "--artifacts",
126
- artifactsDir,
127
- ]),
128
- ).toBe(2);
129
- });
130
-
131
- it("accepts --branch flag", async () => {
132
- expect(
133
- await main([
134
- "--rules",
135
- join(tmpDir, "nonexistent.yml"),
136
- "--branch",
137
- "main",
138
- "--artifacts",
139
- artifactsDir,
140
- ]),
141
- ).toBe(2);
142
- });
143
-
144
- it("returns 2 when both --store-fs and --store-s3 are provided", async () => {
145
- expect(
146
- await main([
147
- "--rules",
148
- join(tmpDir, "nonexistent.yml"),
149
- "--artifacts",
150
- artifactsDir,
151
- "--store-fs",
152
- "/tmp/store",
153
- "--store-s3",
154
- "my-bucket",
155
- ]),
156
- ).toBe(2);
157
- });
158
-
159
- it("accepts --store-s3 flag (parse succeeds, fails on missing rules)", async () => {
160
- expect(
161
- await main([
162
- "--rules",
163
- join(tmpDir, "nonexistent.yml"),
164
- "--artifacts",
165
- artifactsDir,
166
- "--store-s3",
167
- "my-bucket/prefix",
168
- ]),
169
- ).toBe(2);
170
- });
171
-
172
- it("returns 0 when no coverage data found — skips git entirely", async () => {
173
- expect(await main(["--rules", rulesPath, "--artifacts", artifactsDir])).toBe(0);
174
- });
175
-
176
- it("returns 0 when artifacts directory does not exist (ENOENT silenced)", async () => {
177
- expect(await main(["--rules", rulesPath, "--artifacts", join(tmpDir, "does-not-exist")])).toBe(
178
- 0,
179
- );
180
- });
181
-
182
- it("returns 2 when git diff fails due to invalid refs", async () => {
183
- writeFileSync(join(artifactsDir, "lcov.info"), "SF:backend/foo.mts\nDA:1,1\nend_of_record\n");
184
- expect(
185
- await main([
186
- "--rules",
187
- rulesPath,
188
- "--artifacts",
189
- artifactsDir,
190
- "--base",
191
- "INVALID_SHA_XXXXXX",
192
- "--head",
193
- "INVALID_SHA_YYYYYY",
194
- ]),
195
- ).toBe(2);
196
- });
197
-
198
- it("collects lcov files from nested subdirectories before git diff", async () => {
199
- const subDir = join(artifactsDir, "subdir");
200
- mkdirSync(subDir);
201
- writeFileSync(join(subDir, "lcov.info"), "SF:backend/foo.mts\nDA:1,1\nend_of_record\n");
202
- expect(
203
- await main([
204
- "--rules",
205
- rulesPath,
206
- "--artifacts",
207
- artifactsDir,
208
- "--base",
209
- "INVALID_SHA_XXXXXX",
210
- "--head",
211
- "INVALID_SHA_YYYYYY",
212
- ]),
213
- ).toBe(2);
214
- });
215
- });
216
-
217
- describe("runCheck with suite store", () => {
218
- let tmpDir: string;
219
- let rulesPath: string;
220
- let artifactsDir: string;
221
- let storeDir: string;
222
- let store: FileSystemSuiteStore;
223
- let origCwd: string;
224
- let savedGitEnv: Record<string, string | undefined>;
225
-
226
- beforeEach(() => {
227
- tmpDir = mkdtempSync(join(tmpdir(), "coverage-check-store-"));
228
- rulesPath = join(tmpDir, "rules.yml");
229
- artifactsDir = join(tmpDir, "artifacts");
230
- storeDir = join(tmpDir, "store");
231
- mkdirSync(artifactsDir);
232
- mkdirSync(storeDir);
233
- writeFileSync(rulesPath, "rules:\n - paths: backend/**\n patch_coverage_min: 90\n");
234
- store = new FileSystemSuiteStore(storeDir);
235
-
236
- const repoDir = join(tmpDir, "repo");
237
- mkdirSync(join(repoDir, "backend"), { recursive: true });
238
-
239
- savedGitEnv = {};
240
- for (const key of Object.keys(process.env)) {
241
- if (key.startsWith("GIT_")) {
242
- savedGitEnv[key] = process.env[key];
243
- delete process.env[key];
244
- }
245
- }
246
-
247
- const git = (cmd: string) =>
248
- execSync(cmd, {
249
- cwd: repoDir,
250
- shell: "/bin/sh",
251
- encoding: "utf8",
252
- env: {
253
- ...process.env,
254
- GIT_AUTHOR_NAME: "T",
255
- GIT_AUTHOR_EMAIL: "t@t.com",
256
- GIT_COMMITTER_NAME: "T",
257
- GIT_COMMITTER_EMAIL: "t@t.com",
258
- },
259
- });
260
- git("git init");
261
- writeFileSync(join(repoDir, "backend", "foo.mts"), "const a = 1\n");
262
- git('git add . && git commit -m "init"');
263
-
264
- origCwd = process.cwd();
265
- process.chdir(repoDir);
266
- });
267
-
268
- afterEach(() => {
269
- process.chdir(origCwd);
270
- for (const [key, val] of Object.entries(savedGitEnv)) {
271
- if (val !== undefined) process.env[key] = val;
272
- }
273
- rmSync(tmpDir, { recursive: true, force: true });
274
- });
275
-
276
- it("returns 0 when store has no suites and artifacts is empty", async () => {
277
- expect(
278
- await runCheck({
279
- rules: rulesPath,
280
- artifacts: artifactsDir,
281
- base: "HEAD",
282
- head: "HEAD",
283
- pr: null,
284
- repo: "",
285
- json: null,
286
- stripPrefixes: [],
287
- store,
288
- suite: null,
289
- }),
290
- ).toBe(0);
291
- });
292
-
293
- it("returns 0 when store has suites but diff is empty (base=head)", async () => {
294
- await store.put("frontend", Buffer.from("SF:web/app.tsx\nDA:1,1\nend_of_record\n"), {
295
- sha: "test-sha",
296
- branch: "main",
297
- });
298
- expect(
299
- await runCheck({
300
- rules: rulesPath,
301
- artifacts: artifactsDir,
302
- base: "HEAD",
303
- head: "HEAD",
304
- pr: null,
305
- repo: "",
306
- json: null,
307
- stripPrefixes: [],
308
- store,
309
- suite: null,
310
- }),
311
- ).toBe(0);
312
- });
313
-
314
- it("excludes the current suite from the store during check", async () => {
315
- await store.put("backend", Buffer.from("SF:backend/foo.mts\nDA:1,1\nend_of_record\n"), {
316
- sha: "test-sha",
317
- branch: "main",
318
- });
319
- await store.put("frontend", Buffer.from("SF:web/app.tsx\nDA:1,1\nend_of_record\n"), {
320
- sha: "test-sha",
321
- branch: "main",
322
- });
323
- expect(
324
- await runCheck({
325
- rules: rulesPath,
326
- artifacts: artifactsDir,
327
- base: "HEAD",
328
- head: "HEAD",
329
- pr: null,
330
- repo: "",
331
- json: null,
332
- stripPrefixes: [],
333
- store,
334
- suite: "backend",
335
- }),
336
- ).toBe(0);
337
- });
338
-
339
- it("includes non-current suites from the store", async () => {
340
- await store.put("frontend", Buffer.from("SF:web/app.tsx\nDA:1,1\nend_of_record\n"), {
341
- sha: "test-sha",
342
- branch: "main",
343
- });
344
- writeFileSync(join(artifactsDir, "lcov.info"), "SF:backend/foo.mts\nDA:1,1\nend_of_record\n");
345
- expect(
346
- await runCheck({
347
- rules: rulesPath,
348
- artifacts: artifactsDir,
349
- base: "HEAD",
350
- head: "HEAD",
351
- pr: null,
352
- repo: "",
353
- json: null,
354
- stripPrefixes: [],
355
- store,
356
- suite: "backend",
357
- }),
358
- ).toBe(0);
359
- });
360
-
361
- it("handles a store that returns null from get() gracefully", async () => {
362
- const nullStore = {
363
- async list() {
364
- return ["backend"];
365
- },
366
- async get(_suite: string, _opts?: { sha?: string; branch?: string }) {
367
- return null;
368
- },
369
- async put(
370
- _suite: string,
371
- _lcov: Buffer,
372
- _meta: { sha: string; branch: string },
373
- ): Promise<void> {},
374
- };
375
- expect(
376
- await runCheck({
377
- rules: rulesPath,
378
- artifacts: artifactsDir,
379
- base: "HEAD",
380
- head: "HEAD",
381
- pr: null,
382
- repo: "",
383
- json: null,
384
- stripPrefixes: [],
385
- store: nullStore,
386
- suite: null,
387
- }),
388
- ).toBe(0);
389
- });
390
-
391
- it("constructs a real runUrl when GITHUB_SERVER_URL and GITHUB_RUN_ID are set", async () => {
392
- await store.put("frontend", Buffer.from("SF:web/app.tsx\nDA:1,1\nend_of_record\n"), {
393
- sha: "test-sha",
394
- branch: "main",
395
- });
396
-
397
- const calls: string[][] = [];
398
- const gh = async (args: string[]) => {
399
- calls.push(args);
400
- return "";
401
- };
402
-
403
- const origServer = process.env["GITHUB_SERVER_URL"];
404
- const origRunId = process.env["GITHUB_RUN_ID"];
405
- process.env["GITHUB_SERVER_URL"] = "https://github.com";
406
- process.env["GITHUB_RUN_ID"] = "12345";
407
-
408
- try {
409
- await runCheck({
410
- rules: rulesPath,
411
- artifacts: artifactsDir,
412
- base: "HEAD",
413
- head: "HEAD",
414
- pr: 1,
415
- repo: "owner/repo",
416
- json: null,
417
- stripPrefixes: [],
418
- store,
419
- suite: null,
420
- gh,
421
- });
422
- expect(calls.length).toBeGreaterThanOrEqual(1);
423
- } finally {
424
- if (origServer === undefined) delete process.env["GITHUB_SERVER_URL"];
425
- else process.env["GITHUB_SERVER_URL"] = origServer;
426
- if (origRunId === undefined) delete process.env["GITHUB_RUN_ID"];
427
- else process.env["GITHUB_RUN_ID"] = origRunId;
428
- }
429
- });
430
-
431
- it("accepts --store and --suite flags via main()", async () => {
432
- expect(
433
- await main([
434
- "--rules",
435
- rulesPath,
436
- "--artifacts",
437
- artifactsDir,
438
- "--store",
439
- storeDir,
440
- "--suite",
441
- "backend",
442
- ]),
443
- ).toBe(0);
444
- });
445
-
446
- it("accepts --store-fs flag as alias for --store", async () => {
447
- expect(
448
- await main([
449
- "--rules",
450
- rulesPath,
451
- "--artifacts",
452
- artifactsDir,
453
- "--store-fs",
454
- storeDir,
455
- "--suite",
456
- "backend",
457
- ]),
458
- ).toBe(0);
459
- });
460
- });
461
-
462
- describe("with a real git repo and a known diff", () => {
463
- let tmpDir: string;
464
- let rulesPath: string;
465
- let artifactsDir: string;
466
- let repoDir: string;
467
- let origCwd: string;
468
- let baseSha: string;
469
- let headSha: string;
470
- let savedGitEnv: Record<string, string | undefined>;
471
-
472
- beforeEach(() => {
473
- tmpDir = mkdtempSync(join(tmpdir(), "coverage-check-git-"));
474
- rulesPath = join(tmpDir, "rules.yml");
475
- artifactsDir = join(tmpDir, "artifacts");
476
- mkdirSync(artifactsDir);
477
- writeFileSync(rulesPath, "rules:\n - paths: backend/**\n patch_coverage_min: 90\n");
478
-
479
- repoDir = join(tmpDir, "repo");
480
- mkdirSync(join(repoDir, "backend"), { recursive: true });
481
-
482
- savedGitEnv = {};
483
- for (const key of Object.keys(process.env)) {
484
- if (key.startsWith("GIT_")) {
485
- savedGitEnv[key] = process.env[key];
486
- delete process.env[key];
487
- }
488
- }
489
-
490
- const git = (cmd: string) =>
491
- execSync(cmd, {
492
- cwd: repoDir,
493
- shell: "/bin/sh",
494
- encoding: "utf8",
495
- env: {
496
- ...process.env,
497
- GIT_AUTHOR_NAME: "T",
498
- GIT_AUTHOR_EMAIL: "t@t.com",
499
- GIT_COMMITTER_NAME: "T",
500
- GIT_COMMITTER_EMAIL: "t@t.com",
501
- },
502
- });
503
-
504
- git("git init");
505
- writeFileSync(join(repoDir, "backend/foo.mts"), "const a = 1\n");
506
- git('git add . && git commit -m "base"');
507
- baseSha = git("git rev-parse HEAD").trim();
508
-
509
- writeFileSync(join(repoDir, "backend/foo.mts"), "const a = 1\nconst b = 2\n");
510
- git('git add . && git commit -m "head"');
511
- headSha = git("git rev-parse HEAD").trim();
512
-
513
- origCwd = process.cwd();
514
- process.chdir(repoDir);
515
- });
516
-
517
- afterEach(() => {
518
- process.chdir(origCwd);
519
- for (const [key, val] of Object.entries(savedGitEnv)) {
520
- if (val !== undefined) process.env[key] = val;
521
- }
522
- rmSync(tmpDir, { recursive: true, force: true });
523
- });
524
-
525
- it("returns 0 when diff is empty (base equals head)", async () => {
526
- writeFileSync(join(artifactsDir, "lcov.info"), "SF:backend/foo.mts\nDA:1,1\nend_of_record\n");
527
- expect(
528
- await main([
529
- "--rules",
530
- rulesPath,
531
- "--artifacts",
532
- artifactsDir,
533
- "--base",
534
- "HEAD",
535
- "--head",
536
- "HEAD",
537
- ]),
538
- ).toBe(0);
539
- });
540
-
541
- it("returns 1 when new lines are uncovered and below threshold", async () => {
542
- writeFileSync(
543
- join(artifactsDir, "lcov.info"),
544
- "SF:backend/foo.mts\nDA:1,1\nDA:2,0\nend_of_record\n",
545
- );
546
- expect(
547
- await main([
548
- "--rules",
549
- rulesPath,
550
- "--artifacts",
551
- artifactsDir,
552
- "--base",
553
- baseSha,
554
- "--head",
555
- headSha,
556
- ]),
557
- ).toBe(1);
558
- });
559
-
560
- it("returns 0 when all new lines are covered and above threshold", async () => {
561
- writeFileSync(
562
- join(artifactsDir, "lcov.info"),
563
- "SF:backend/foo.mts\nDA:1,1\nDA:2,1\nend_of_record\n",
564
- );
565
- expect(
566
- await main([
567
- "--rules",
568
- rulesPath,
569
- "--artifacts",
570
- artifactsDir,
571
- "--base",
572
- baseSha,
573
- "--head",
574
- headSha,
575
- ]),
576
- ).toBe(0);
577
- });
578
-
579
- it("writes json output to the path specified by --json", async () => {
580
- writeFileSync(
581
- join(artifactsDir, "lcov.info"),
582
- "SF:backend/foo.mts\nDA:1,1\nDA:2,1\nend_of_record\n",
583
- );
584
- const jsonPath = join(tmpDir, "result.json");
585
- await main([
586
- "--rules",
587
- rulesPath,
588
- "--artifacts",
589
- artifactsDir,
590
- "--base",
591
- baseSha,
592
- "--head",
593
- headSha,
594
- "--json",
595
- jsonPath,
596
- ]);
597
- const result = JSON.parse(readFileSync(jsonPath, "utf8"));
598
- expect(result.passed).toBe(true);
599
- expect(Array.isArray(result.buckets)).toBe(true);
600
- });
601
-
602
- it("posts PR comment via injectable gh runner on failure", async () => {
603
- writeFileSync(
604
- join(artifactsDir, "lcov.info"),
605
- "SF:backend/foo.mts\nDA:1,1\nDA:2,0\nend_of_record\n",
606
- );
607
- const calls: string[][] = [];
608
- const gh = async (args: string[]) => {
609
- calls.push(args);
610
- return "";
611
- };
612
- const result = await runCheck({
613
- rules: rulesPath,
614
- artifacts: artifactsDir,
615
- base: baseSha,
616
- head: headSha,
617
- pr: 42,
618
- repo: "owner/repo",
619
- json: null,
620
- stripPrefixes: [],
621
- store: null,
622
- suite: null,
623
- gh,
624
- });
625
- expect(result).toBe(1);
626
- expect(calls.length).toBeGreaterThanOrEqual(1);
627
- });
628
-
629
- it("posts PR comment via injectable gh runner on pass", async () => {
630
- writeFileSync(
631
- join(artifactsDir, "lcov.info"),
632
- "SF:backend/foo.mts\nDA:1,1\nDA:2,1\nend_of_record\n",
633
- );
634
- const calls: string[][] = [];
635
- const gh = async (args: string[]) => {
636
- calls.push(args);
637
- return "";
638
- };
639
- const result = await runCheck({
640
- rules: rulesPath,
641
- artifacts: artifactsDir,
642
- base: baseSha,
643
- head: headSha,
644
- pr: 42,
645
- repo: "owner/repo",
646
- json: null,
647
- stripPrefixes: [],
648
- store: null,
649
- suite: null,
650
- gh,
651
- });
652
- expect(result).toBe(0);
653
- // On pass with no existing comment, gh is called once (lookup only)
654
- expect(calls.length).toBe(1);
655
- });
656
-
657
- it("handles gh error gracefully (stderr write, still returns correct exit code)", async () => {
658
- writeFileSync(
659
- join(artifactsDir, "lcov.info"),
660
- "SF:backend/foo.mts\nDA:1,1\nDA:2,0\nend_of_record\n",
661
- );
662
- const gh = async (_args: string[]): Promise<string> => {
663
- throw new Error("network error");
664
- };
665
- const result = await runCheck({
666
- rules: rulesPath,
667
- artifacts: artifactsDir,
668
- base: baseSha,
669
- head: headSha,
670
- pr: 42,
671
- repo: "owner/repo",
672
- json: null,
673
- stripPrefixes: [],
674
- store: null,
675
- suite: null,
676
- gh,
677
- });
678
- expect(result).toBe(1);
679
- });
680
-
681
- it.skipIf(
682
- process.env.PR_AUTHOR === "dependabot[bot]" ||
683
- (!process.env.GH_TOKEN && !process.env.GITHUB_TOKEN),
684
- )("attempts pr comment on failure and handles gh error gracefully", async () => {
685
- writeFileSync(
686
- join(artifactsDir, "lcov.info"),
687
- "SF:backend/foo.mts\nDA:1,1\nDA:2,0\nend_of_record\n",
688
- );
689
- expect(
690
- await main([
691
- "--rules",
692
- rulesPath,
693
- "--artifacts",
694
- artifactsDir,
695
- "--base",
696
- baseSha,
697
- "--head",
698
- headSha,
699
- "--pr",
700
- "1",
701
- "--repo",
702
- "owner/NONEXISTENT_REPO_FOR_TEST",
703
- ]),
704
- ).toBe(1);
705
- });
706
-
707
- it("combines store suites with current artifacts for coverage check", async () => {
708
- const storeDir = join(tmpDir, "store");
709
- mkdirSync(storeDir);
710
- const store = new FileSystemSuiteStore(storeDir);
711
-
712
- await store.put("frontend", Buffer.from("SF:web/app.tsx\nDA:1,1\nend_of_record\n"), {
713
- sha: "test-sha",
714
- branch: "main",
715
- });
716
-
717
- writeFileSync(
718
- join(artifactsDir, "lcov.info"),
719
- "SF:backend/foo.mts\nDA:1,1\nDA:2,1\nend_of_record\n",
720
- );
721
-
722
- expect(
723
- await runCheck({
724
- rules: rulesPath,
725
- artifacts: artifactsDir,
726
- base: baseSha,
727
- head: headSha,
728
- pr: null,
729
- repo: "",
730
- json: null,
731
- stripPrefixes: [],
732
- store,
733
- suite: "backend",
734
- }),
735
- ).toBe(0);
736
- });
737
-
738
- it("writes step summary when summaryFile is provided", async () => {
739
- const summaryFile = join(tmpDir, "summary.md");
740
- writeFileSync(summaryFile, "");
741
- writeFileSync(
742
- join(artifactsDir, "lcov.info"),
743
- "SF:backend/foo.mts\nDA:1,1\nDA:2,1\nend_of_record\n",
744
- );
745
- await runCheck({
746
- rules: rulesPath,
747
- artifacts: artifactsDir,
748
- base: baseSha,
749
- head: headSha,
750
- pr: null,
751
- repo: "",
752
- json: null,
753
- stripPrefixes: [],
754
- store: null,
755
- suite: "backend",
756
- summaryFile,
757
- });
758
- const content = readFileSync(summaryFile, "utf8");
759
- expect(content).toContain("Coverage summary");
760
- });
761
-
762
- it("does not write step summary when summaryFile is null even if env var is set", async () => {
763
- const summaryFile = join(tmpDir, "should-not-exist.md");
764
- writeFileSync(
765
- join(artifactsDir, "lcov.info"),
766
- "SF:backend/foo.mts\nDA:1,1\nDA:2,1\nend_of_record\n",
767
- );
768
- const origEnv = process.env["GITHUB_STEP_SUMMARY"];
769
- process.env["GITHUB_STEP_SUMMARY"] = summaryFile;
770
- try {
771
- await runCheck({
772
- rules: rulesPath,
773
- artifacts: artifactsDir,
774
- base: baseSha,
775
- head: headSha,
776
- pr: null,
777
- repo: "",
778
- json: null,
779
- stripPrefixes: [],
780
- store: null,
781
- suite: "backend",
782
- summaryFile: null,
783
- });
784
- } finally {
785
- if (origEnv === undefined) delete process.env["GITHUB_STEP_SUMMARY"];
786
- else process.env["GITHUB_STEP_SUMMARY"] = origEnv;
787
- }
788
- expect(() => readFileSync(summaryFile, "utf8")).toThrow();
789
- });
790
-
791
- it("uses N/A runUrl when GITHUB_SERVER_URL and GITHUB_RUN_ID are unset", async () => {
792
- writeFileSync(
793
- join(artifactsDir, "lcov.info"),
794
- "SF:backend/foo.mts\nDA:1,1\nDA:2,1\nend_of_record\n",
795
- );
796
- const savedServer = process.env["GITHUB_SERVER_URL"];
797
- const savedRunId = process.env["GITHUB_RUN_ID"];
798
- delete process.env["GITHUB_SERVER_URL"];
799
- delete process.env["GITHUB_RUN_ID"];
800
- try {
801
- await runCheck({
802
- rules: rulesPath,
803
- artifacts: artifactsDir,
804
- base: baseSha,
805
- head: headSha,
806
- pr: null,
807
- repo: "",
808
- json: null,
809
- stripPrefixes: [],
810
- store: null,
811
- suite: "backend",
812
- });
813
- } finally {
814
- if (savedServer !== undefined) process.env["GITHUB_SERVER_URL"] = savedServer;
815
- else delete process.env["GITHUB_SERVER_URL"];
816
- if (savedRunId !== undefined) process.env["GITHUB_RUN_ID"] = savedRunId;
817
- else delete process.env["GITHUB_RUN_ID"];
818
- }
819
- });
820
-
821
- it("returns 2 when writeSummary throws (unwritable summaryFile path)", async () => {
822
- writeFileSync(
823
- join(artifactsDir, "lcov.info"),
824
- "SF:backend/foo.mts\nDA:1,1\nDA:2,1\nend_of_record\n",
825
- );
826
- // Pass the tmp directory itself as summaryFile — appendFileSync on a dir throws EISDIR
827
- expect(
828
- await runCheck({
829
- rules: rulesPath,
830
- artifacts: artifactsDir,
831
- base: baseSha,
832
- head: headSha,
833
- pr: null,
834
- repo: "",
835
- json: null,
836
- stripPrefixes: [],
837
- store: null,
838
- suite: "backend",
839
- summaryFile: tmpDir,
840
- }),
841
- ).toBe(2);
842
- });
843
-
844
- it("does not write summary when summaryFile is undefined and GITHUB_STEP_SUMMARY is unset", async () => {
845
- writeFileSync(
846
- join(artifactsDir, "lcov.info"),
847
- "SF:backend/foo.mts\nDA:1,1\nDA:2,1\nend_of_record\n",
848
- );
849
- const savedSummary = process.env["GITHUB_STEP_SUMMARY"];
850
- delete process.env["GITHUB_STEP_SUMMARY"];
851
- try {
852
- await runCheck({
853
- rules: rulesPath,
854
- artifacts: artifactsDir,
855
- base: baseSha,
856
- head: headSha,
857
- pr: null,
858
- repo: "",
859
- json: null,
860
- stripPrefixes: [],
861
- store: null,
862
- suite: "backend",
863
- summaryFile: undefined,
864
- });
865
- } finally {
866
- if (savedSummary !== undefined) process.env["GITHUB_STEP_SUMMARY"] = savedSummary;
867
- else delete process.env["GITHUB_STEP_SUMMARY"];
868
- }
869
- });
870
- });