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,112 +0,0 @@
1
- import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import type { SuiteMeta } from "./types.mts";
4
-
5
- export function assertSafePathComponent(value: string, label: string): void {
6
- if (
7
- typeof value !== "string" ||
8
- value.length === 0 ||
9
- value === "." ||
10
- value === ".." ||
11
- value.includes("/") ||
12
- value.includes("\\")
13
- ) {
14
- throw new Error(`invalid ${label}: ${JSON.stringify(value)}`);
15
- }
16
- }
17
-
18
- export type { SuiteMeta };
19
-
20
- export interface SuiteStore {
21
- /** Returns all suite names currently in the store. */
22
- list(): Promise<string[]>;
23
- /**
24
- * Returns the merged LCOV bytes for a suite, or null if absent.
25
- * Resolves by sha if opts.sha is set; otherwise follows the branch pointer
26
- * (opts.branch, defaulting to "main").
27
- */
28
- get(suite: string, opts?: { sha?: string; branch?: string }): Promise<Buffer | null>;
29
- /** Stores the merged LCOV bytes for a suite. sha and branch are required. */
30
- put(
31
- suite: string,
32
- lcov: Buffer,
33
- meta: SuiteMeta & { sha: string; branch: string },
34
- ): Promise<void>;
35
- }
36
-
37
- /**
38
- * Filesystem-backed SuiteStore.
39
- *
40
- * Layout:
41
- * <root>/<suite>/sha/<sha>/lcov.info — LCOV payload
42
- * <root>/<suite>/branch/<branch>/latest.json — { sha, timestamp }
43
- *
44
- * Transport is the caller's responsibility (e.g. S3 sync, git orphan branch).
45
- */
46
- export class FileSystemSuiteStore implements SuiteStore {
47
- private readonly root: string;
48
- constructor(root: string) {
49
- this.root = root;
50
- }
51
-
52
- async list(): Promise<string[]> {
53
- try {
54
- return readdirSync(this.root, { withFileTypes: true })
55
- .filter((e) => e.isDirectory())
56
- .map((e) => e.name);
57
- } catch (err) {
58
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
59
- throw err;
60
- }
61
- }
62
-
63
- async get(suite: string, opts?: { sha?: string; branch?: string }): Promise<Buffer | null> {
64
- assertSafePathComponent(suite, "suite");
65
- if (opts?.sha !== undefined) assertSafePathComponent(opts.sha, "sha");
66
- let sha = opts?.sha;
67
- if (!sha) {
68
- const branch = opts?.branch ?? "main";
69
- assertSafePathComponent(branch, "branch");
70
- const pointerPath = join(this.root, suite, "branch", branch, "latest.json");
71
- try {
72
- const pointer = JSON.parse(readFileSync(pointerPath, "utf8")) as { sha: string };
73
- assertSafePathComponent(pointer.sha, "sha");
74
- sha = pointer.sha;
75
- } catch (err) {
76
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
77
- throw err;
78
- }
79
- }
80
- const lcovPath = join(this.root, suite, "sha", sha, "lcov.info");
81
- try {
82
- return readFileSync(lcovPath);
83
- } catch (err) {
84
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
85
- throw err;
86
- }
87
- }
88
-
89
- async put(
90
- suite: string,
91
- lcov: Buffer,
92
- meta: SuiteMeta & { sha: string; branch: string },
93
- ): Promise<void> {
94
- assertSafePathComponent(suite, "suite");
95
- assertSafePathComponent(meta.sha, "sha");
96
- assertSafePathComponent(meta.branch, "branch");
97
- const shaDir = join(this.root, suite, "sha", meta.sha);
98
- mkdirSync(shaDir, { recursive: true });
99
- writeFileSync(join(shaDir, "lcov.info"), lcov);
100
-
101
- const branchDir = join(this.root, suite, "branch", meta.branch);
102
- mkdirSync(branchDir, { recursive: true });
103
- writeFileSync(
104
- join(branchDir, "latest.json"),
105
- JSON.stringify(
106
- { sha: meta.sha, timestamp: meta.timestamp ?? new Date().toISOString() },
107
- null,
108
- 2,
109
- ),
110
- );
111
- }
112
- }
@@ -1,209 +0,0 @@
1
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { tmpdir } from "node:os";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { assertSafePathComponent, FileSystemSuiteStore } from "./suite-store.mts";
6
-
7
- describe("FileSystemSuiteStore", () => {
8
- let tmpDir: string;
9
- let store: FileSystemSuiteStore;
10
-
11
- beforeEach(() => {
12
- tmpDir = mkdtempSync(join(tmpdir(), "suite-store-"));
13
- store = new FileSystemSuiteStore(tmpDir);
14
- });
15
-
16
- afterEach(() => {
17
- rmSync(tmpDir, { recursive: true, force: true });
18
- });
19
-
20
- describe("list()", () => {
21
- it("returns empty array when store root does not exist", async () => {
22
- const missing = new FileSystemSuiteStore(join(tmpDir, "nonexistent"));
23
- expect(await missing.list()).toEqual([]);
24
- });
25
-
26
- it("returns empty array for an empty store", async () => {
27
- expect(await store.list()).toEqual([]);
28
- });
29
-
30
- it("returns suite names after putting suites", async () => {
31
- await store.put("backend", Buffer.from("SF:foo\nend_of_record\n"), {
32
- sha: "abc",
33
- branch: "main",
34
- });
35
- await store.put("frontend", Buffer.from("SF:bar\nend_of_record\n"), {
36
- sha: "def",
37
- branch: "main",
38
- });
39
- const suites = await store.list();
40
- expect(suites).toContain("backend");
41
- expect(suites).toContain("frontend");
42
- expect(suites).toHaveLength(2);
43
- });
44
-
45
- it("ignores non-directory entries in the root", async () => {
46
- await store.put("backend", Buffer.from("SF:foo\nend_of_record\n"), {
47
- sha: "abc",
48
- branch: "main",
49
- });
50
- const suites = await store.list();
51
- expect(suites).not.toContain("lcov.info");
52
- expect(suites).not.toContain("meta.json");
53
- });
54
-
55
- it("rethrows non-ENOENT errors", async () => {
56
- const badStore = new FileSystemSuiteStore("\0invalid");
57
- await expect(badStore.list()).rejects.toThrow();
58
- });
59
- });
60
-
61
- describe("get()", () => {
62
- it("returns null for a missing suite (no pointer file)", async () => {
63
- expect(await store.get("nonexistent")).toBeNull();
64
- });
65
-
66
- it("returns null when get() is called with explicit sha that has no lcov file", async () => {
67
- // Pointer exists (from a different sha), but requested sha is absent
68
- await store.put("backend", Buffer.from("SF:backend/foo.mts\nDA:1,1\nend_of_record\n"), {
69
- sha: "abc",
70
- branch: "main",
71
- });
72
- expect(await store.get("backend", { sha: "nonexistent-sha" })).toBeNull();
73
- });
74
-
75
- it("returns the LCOV buffer after put() via branch pointer", async () => {
76
- const lcov = Buffer.from("SF:backend/foo.mts\nDA:1,1\nend_of_record\n");
77
- await store.put("backend", lcov, { sha: "abc123", branch: "main" });
78
- const result = await store.get("backend");
79
- expect(result).not.toBeNull();
80
- expect(result!.toString()).toBe(lcov.toString());
81
- });
82
-
83
- it("returns the LCOV buffer when get() is called with explicit sha", async () => {
84
- const lcov = Buffer.from("SF:backend/foo.mts\nDA:1,1\nend_of_record\n");
85
- await store.put("backend", lcov, { sha: "abc123", branch: "main" });
86
- const result = await store.get("backend", { sha: "abc123" });
87
- expect(result).not.toBeNull();
88
- expect(result!.toString()).toBe(lcov.toString());
89
- });
90
-
91
- it("resolves the correct sha via the branch pointer", async () => {
92
- const lcovV1 = Buffer.from("SF:backend/v1.mts\nDA:1,1\nend_of_record\n");
93
- const lcovV2 = Buffer.from("SF:backend/v2.mts\nDA:2,1\nend_of_record\n");
94
- await store.put("backend", lcovV1, { sha: "sha1", branch: "main" });
95
- await store.put("backend", lcovV2, { sha: "sha2", branch: "main" });
96
- // Branch pointer now points to sha2; sha1 still exists on disk
97
- const result = await store.get("backend", { branch: "main" });
98
- expect(result!.toString()).toBe(lcovV2.toString());
99
- });
100
-
101
- it("rethrows non-ENOENT errors from pointer readFileSync", async () => {
102
- const badStore = new FileSystemSuiteStore("\0invalid");
103
- await expect(badStore.get("suite")).rejects.toThrow();
104
- });
105
-
106
- it("rethrows non-ENOENT errors from lcov readFileSync when sha is explicit", async () => {
107
- const badStore = new FileSystemSuiteStore("\0invalid");
108
- await expect(badStore.get("suite", { sha: "abc" })).rejects.toThrow();
109
- });
110
- });
111
-
112
- describe("put()", () => {
113
- it("writes lcov.info under sha/ and latest.json under branch/", async () => {
114
- const lcov = Buffer.from("SF:foo.mts\nDA:1,5\nend_of_record\n");
115
- await store.put("backend", lcov, { sha: "abc123", branch: "main" });
116
-
117
- const lcovPath = join(tmpDir, "backend", "sha", "abc123", "lcov.info");
118
- const pointerPath = join(tmpDir, "backend", "branch", "main", "latest.json");
119
- expect(readFileSync(lcovPath).toString()).toBe(lcov.toString());
120
-
121
- const pointer = JSON.parse(readFileSync(pointerPath, "utf8"));
122
- expect(pointer.sha).toBe("abc123");
123
- expect(typeof pointer.timestamp).toBe("string");
124
- });
125
-
126
- it("uses the provided timestamp", async () => {
127
- await store.put("backend", Buffer.from(""), {
128
- sha: "abc",
129
- branch: "main",
130
- timestamp: "2026-01-01T00:00:00.000Z",
131
- });
132
- const pointer = JSON.parse(
133
- readFileSync(join(tmpDir, "backend", "branch", "main", "latest.json"), "utf8"),
134
- );
135
- expect(pointer.timestamp).toBe("2026-01-01T00:00:00.000Z");
136
- });
137
-
138
- it("creates a default timestamp when none is provided", async () => {
139
- const before = new Date().toISOString();
140
- await store.put("backend", Buffer.from(""), { sha: "abc", branch: "main" });
141
- const after = new Date().toISOString();
142
- const pointer = JSON.parse(
143
- readFileSync(join(tmpDir, "backend", "branch", "main", "latest.json"), "utf8"),
144
- );
145
- expect(pointer.timestamp >= before).toBe(true);
146
- expect(pointer.timestamp <= after).toBe(true);
147
- });
148
-
149
- it("creates parent directories recursively", async () => {
150
- const nested = new FileSystemSuiteStore(join(tmpDir, "deep", "nested", "store"));
151
- await nested.put("backend", Buffer.from("SF:foo\nend_of_record\n"), {
152
- sha: "abc",
153
- branch: "main",
154
- });
155
- expect(await nested.list()).toContain("backend");
156
- });
157
-
158
- it("overwrites an existing suite when same sha is used", async () => {
159
- await store.put("backend", Buffer.from("v1"), { sha: "abc", branch: "main" });
160
- await store.put("backend", Buffer.from("v2"), { sha: "abc", branch: "main" });
161
- const result = await store.get("backend", { sha: "abc" });
162
- expect(result!.toString()).toBe("v2");
163
- });
164
-
165
- it("keeps multiple sha entries independently", async () => {
166
- await store.put("backend", Buffer.from("v1"), { sha: "sha1", branch: "main" });
167
- await store.put("backend", Buffer.from("v2"), { sha: "sha2", branch: "main" });
168
- expect((await store.get("backend", { sha: "sha1" }))!.toString()).toBe("v1");
169
- expect((await store.get("backend", { sha: "sha2" }))!.toString()).toBe("v2");
170
- });
171
- });
172
-
173
- describe("path traversal protection", () => {
174
- const invalid = ["", ".", "..", "a/b", "a\\b"];
175
- for (const val of invalid) {
176
- it(`get() rejects suite=${JSON.stringify(val)}`, async () => {
177
- await expect(store.get(val)).rejects.toThrow("invalid suite");
178
- });
179
- it(`get() rejects branch=${JSON.stringify(val)}`, async () => {
180
- await expect(store.get("backend", { branch: val })).rejects.toThrow("invalid branch");
181
- });
182
- it(`get() rejects sha=${JSON.stringify(val)}`, async () => {
183
- await expect(store.get("backend", { sha: val })).rejects.toThrow("invalid sha");
184
- });
185
- it(`put() rejects suite=${JSON.stringify(val)}`, async () => {
186
- await expect(
187
- store.put(val, Buffer.from(""), { sha: "abc", branch: "main" }),
188
- ).rejects.toThrow("invalid suite");
189
- });
190
- it(`put() rejects sha=${JSON.stringify(val)}`, async () => {
191
- await expect(
192
- store.put("backend", Buffer.from(""), { sha: val, branch: "main" }),
193
- ).rejects.toThrow("invalid sha");
194
- });
195
- it(`put() rejects branch=${JSON.stringify(val)}`, async () => {
196
- await expect(
197
- store.put("backend", Buffer.from(""), { sha: "abc", branch: val }),
198
- ).rejects.toThrow("invalid branch");
199
- });
200
- }
201
- });
202
- });
203
-
204
- describe("assertSafePathComponent", () => {
205
- it("rejects non-string values at runtime (e.g. from JSON.parse)", () => {
206
- expect(() => assertSafePathComponent(123 as unknown as string, "sha")).toThrow("invalid sha");
207
- expect(() => assertSafePathComponent(null as unknown as string, "sha")).toThrow("invalid sha");
208
- });
209
- });
package/src/types.mts DELETED
@@ -1,43 +0,0 @@
1
- export type CoverageRule = {
2
- paths: string;
3
- patch_coverage_min: number;
4
- };
5
-
6
- export type CoverageRules = {
7
- rules: CoverageRule[];
8
- };
9
-
10
- /** Map from repo-root-relative file path to map of line number → hit count. */
11
- export type LcovData = Map<string, Map<number, number>>;
12
-
13
- /** Map from repo-root-relative file path to set of added/modified line numbers. */
14
- export type DiffLines = Map<string, Set<number>>;
15
-
16
- export type FileCoverageResult = {
17
- file: string;
18
- coverable: number;
19
- hit: number;
20
- uncoveredLines: number[];
21
- rule: string | null;
22
- };
23
-
24
- export type BucketResult = {
25
- rule: string;
26
- threshold: number;
27
- coverable: number;
28
- hit: number;
29
- files: FileCoverageResult[];
30
- passed: boolean;
31
- };
32
-
33
- export type CoverageCheckResult = {
34
- buckets: BucketResult[];
35
- informational: FileCoverageResult[];
36
- passed: boolean;
37
- };
38
-
39
- export type SuiteMeta = {
40
- sha?: string;
41
- branch?: string;
42
- timestamp?: string;
43
- };