@fairfox/polly 0.79.0 → 0.80.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.
Files changed (26) hide show
  1. package/dist/cli/polly.js +31 -1
  2. package/dist/cli/polly.js.map +3 -3
  3. package/dist/tools/test/src/coverage-policy/cli.d.ts +19 -0
  4. package/dist/tools/test/src/coverage-policy/cli.js +339 -0
  5. package/dist/tools/test/src/coverage-policy/cli.js.map +13 -0
  6. package/dist/tools/test/src/coverage-policy/discover.d.ts +23 -0
  7. package/dist/tools/test/src/coverage-policy/enforce.d.ts +54 -0
  8. package/dist/tools/test/src/coverage-policy/index.d.ts +10 -0
  9. package/dist/tools/test/src/coverage-policy/index.js +242 -0
  10. package/dist/tools/test/src/coverage-policy/index.js.map +13 -0
  11. package/dist/tools/test/src/coverage-policy/mutate-targets.d.ts +30 -0
  12. package/dist/tools/test/src/coverage-policy/types.d.ts +35 -0
  13. package/dist/tools/test/src/e2e-mesh/index.js +18 -2
  14. package/dist/tools/test/src/e2e-mesh/index.js.map +5 -4
  15. package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +8 -0
  16. package/dist/tools/test/src/e2e-relay/index.d.ts +1 -1
  17. package/dist/tools/test/src/e2e-relay/index.js +1421 -0
  18. package/dist/tools/test/src/e2e-relay/index.js.map +30 -0
  19. package/dist/tools/test/src/e2e-relay/wait-for-relay-convergence.d.ts +8 -0
  20. package/dist/tools/test/src/e2e-relay/with-repo-server.d.ts +9 -0
  21. package/dist/tools/test/src/e2e-shared/index.d.ts +1 -0
  22. package/dist/tools/test/src/e2e-shared/timeout-context.d.ts +17 -0
  23. package/dist/tools/test/src/index.d.ts +1 -0
  24. package/dist/tools/test/src/index.js +16 -1
  25. package/dist/tools/test/src/index.js.map +5 -4
  26. package/package.json +11 -1
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @fairfox/polly/test/coverage — the coverage-policy engine.
3
+ *
4
+ * Parses a `bun test --coverage` table and applies a {@link CoverageConfig}.
5
+ * It fails on four conditions, not just low numbers: a non-exempt file below
6
+ * the floor, an exempt key whose source no longer exists, an exemption whose
7
+ * `claimedBy` test no longer exists, and an exempt file that has climbed back
8
+ * over the floor (promote it). It also reports orphans — source files no unit
9
+ * test imports, the blind spot a coverage table can't show.
10
+ *
11
+ * Everything here is parameterised by the project root, so the same engine
12
+ * backs Polly's own gate and the consumer-facing `polly coverage` command.
13
+ */
14
+ import type { CoverageConfig } from "./types";
15
+ export interface CoverageRow {
16
+ /** Project-relative, e.g. `src/shared/lib/state.ts`. */
17
+ file: string;
18
+ funcs: number;
19
+ lines: number;
20
+ }
21
+ export interface Violation {
22
+ file: string;
23
+ metric: "lines" | "funcs";
24
+ observed: number;
25
+ required: number;
26
+ }
27
+ export interface CoverageFindings {
28
+ rowCount: number;
29
+ violations: Violation[];
30
+ staleExempts: string[];
31
+ missingExemptFiles: string[];
32
+ missingClaimedBy: Array<{
33
+ file: string;
34
+ claimedBy: string;
35
+ }>;
36
+ orphans: string[];
37
+ /** True when a floor was configured; false means report-only. */
38
+ enforced: boolean;
39
+ }
40
+ /**
41
+ * Parse the `bun test --coverage` table. Only rows under `srcDir` are
42
+ * policy-bearing; the `All files` summary and test-infra rows are skipped.
43
+ * Column order is `File | % Funcs | % Lines | Uncovered`.
44
+ */
45
+ export declare function parseCoverageTable(text: string, srcDir: string): CoverageRow[];
46
+ /** Run `bun test --coverage` in the configured cwd and return combined output. */
47
+ export declare function runCoverage(root: string, testCwd: string): Promise<string>;
48
+ /** Apply the policy to a parsed table. Pure — no spawning. */
49
+ export declare function evaluateCoverage(root: string, rows: CoverageRow[], config: CoverageConfig): Promise<CoverageFindings>;
50
+ /** Run the suite under coverage and apply the policy. */
51
+ export declare function enforceCoverage(root: string, config: CoverageConfig, coverageText?: string): Promise<CoverageFindings>;
52
+ /** True when the findings represent a policy failure (orphans are advisory
53
+ * unless `strictOrphans`). */
54
+ export declare function hasFailure(findings: CoverageFindings, strictOrphans: boolean): boolean;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @fairfox/polly/test/coverage — programmatic API behind `polly coverage`.
3
+ *
4
+ * Use the CLI (`polly coverage`) for the common case; import these to script
5
+ * the policy yourself or to add a coverage tier to a custom runner.
6
+ */
7
+ export { type LoadedConfig, loadCoverageConfig } from "./discover";
8
+ export { type CoverageFindings, type CoverageRow, enforceCoverage, evaluateCoverage, hasFailure, parseCoverageTable, runCoverage, type Violation, } from "./enforce";
9
+ export { findStrykerConfigs, type MutateTargetIssue, type MutateTargetReport, validateMutateTargets, } from "./mutate-targets";
10
+ export type { CoverageConfig, ExemptEntry, FileThreshold } from "./types";
@@ -0,0 +1,242 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
+
19
+ // tools/test/src/coverage-policy/enforce.ts
20
+ var exports_enforce = {};
21
+ __export(exports_enforce, {
22
+ runCoverage: () => runCoverage,
23
+ parseCoverageTable: () => parseCoverageTable,
24
+ hasFailure: () => hasFailure,
25
+ evaluateCoverage: () => evaluateCoverage,
26
+ enforceCoverage: () => enforceCoverage
27
+ });
28
+ import { existsSync as existsSync2 } from "node:fs";
29
+ import { join as join2, resolve as resolve2 } from "node:path";
30
+ import { Glob } from "bun";
31
+ function normalizePath(raw) {
32
+ return raw.replace(/^(?:\.\.\/)+/, "");
33
+ }
34
+ function parseCoverageTable(text, srcDir) {
35
+ const prefix = `${srcDir}/`;
36
+ const rows = [];
37
+ for (const line of text.split(`
38
+ `)) {
39
+ if (!line.includes("|"))
40
+ continue;
41
+ if (line.includes("All files") || line.includes("% Funcs"))
42
+ continue;
43
+ if (line.trim().startsWith("---"))
44
+ continue;
45
+ const cells = line.split("|").map((c) => c.trim());
46
+ if (cells.length < 3)
47
+ continue;
48
+ const file = normalizePath(cells[0] ?? "");
49
+ const funcs = Number(cells[1]);
50
+ const lines = Number(cells[2]);
51
+ if (!file.startsWith(prefix) || Number.isNaN(funcs) || Number.isNaN(lines))
52
+ continue;
53
+ rows.push({ file, funcs, lines });
54
+ }
55
+ return rows;
56
+ }
57
+ async function runCoverage(root, testCwd) {
58
+ const proc = Bun.spawn(["bun", "test", "--coverage"], {
59
+ cwd: join2(root, testCwd),
60
+ stdout: "pipe",
61
+ stderr: "pipe"
62
+ });
63
+ const [out, err] = await Promise.all([
64
+ new Response(proc.stdout).text(),
65
+ new Response(proc.stderr).text()
66
+ ]);
67
+ await proc.exited;
68
+ if (proc.exitCode !== 0) {
69
+ throw new Error(`bun test --coverage exited ${proc.exitCode}
70
+ ${err}`);
71
+ }
72
+ return `${out}
73
+ ${err}`;
74
+ }
75
+ function evaluateRows(rows, config) {
76
+ const t = config.defaultThreshold;
77
+ const exempt = config.exempt ?? {};
78
+ const violations = [];
79
+ const staleExempts = [];
80
+ if (!t)
81
+ return { violations, staleExempts };
82
+ for (const row of rows) {
83
+ if (exempt[row.file]) {
84
+ if (row.lines >= t.lines && row.funcs >= t.funcs)
85
+ staleExempts.push(row.file);
86
+ continue;
87
+ }
88
+ if (row.lines < t.lines) {
89
+ violations.push({ file: row.file, metric: "lines", observed: row.lines, required: t.lines });
90
+ }
91
+ if (row.funcs < t.funcs) {
92
+ violations.push({ file: row.file, metric: "funcs", observed: row.funcs, required: t.funcs });
93
+ }
94
+ }
95
+ return { violations, staleExempts };
96
+ }
97
+ function validateExemptions(root, config) {
98
+ const missingExemptFiles = [];
99
+ const missingClaimedBy = [];
100
+ for (const [file, entry] of Object.entries(config.exempt ?? {})) {
101
+ if (!existsSync2(resolve2(root, file)))
102
+ missingExemptFiles.push(file);
103
+ const claimedBy = entry.claimedBy.trim();
104
+ if (!claimedBy.startsWith("n/a") && !existsSync2(resolve2(root, claimedBy))) {
105
+ missingClaimedBy.push({ file, claimedBy });
106
+ }
107
+ }
108
+ return { missingExemptFiles, missingClaimedBy };
109
+ }
110
+ async function findOrphans(root, srcDir, covered, config) {
111
+ const exempt = config.exempt ?? {};
112
+ const orphans = [];
113
+ const glob = new Glob(`${srcDir}/**/*.{ts,tsx}`);
114
+ for await (const file of glob.scan({ cwd: root, onlyFiles: true })) {
115
+ if (file.endsWith(".d.ts") || /\.test\.tsx?$/.test(file) || file.includes("/__tests__/")) {
116
+ continue;
117
+ }
118
+ if (covered.has(file) || exempt[file])
119
+ continue;
120
+ orphans.push(file);
121
+ }
122
+ return orphans.sort();
123
+ }
124
+ async function evaluateCoverage(root, rows, config) {
125
+ const srcDir = config.srcDir ?? DEFAULT_SRC;
126
+ const { violations, staleExempts } = evaluateRows(rows, config);
127
+ const { missingExemptFiles, missingClaimedBy } = validateExemptions(root, config);
128
+ const orphans = await findOrphans(root, srcDir, new Set(rows.map((r) => r.file)), config);
129
+ return {
130
+ rowCount: rows.length,
131
+ violations,
132
+ staleExempts,
133
+ missingExemptFiles,
134
+ missingClaimedBy,
135
+ orphans,
136
+ enforced: config.defaultThreshold !== undefined
137
+ };
138
+ }
139
+ async function enforceCoverage(root, config, coverageText) {
140
+ const srcDir = config.srcDir ?? DEFAULT_SRC;
141
+ const text = coverageText ?? await runCoverage(root, config.testCwd ?? ".");
142
+ const rows = parseCoverageTable(text, srcDir);
143
+ return evaluateCoverage(root, rows, config);
144
+ }
145
+ function hasFailure(findings, strictOrphans) {
146
+ return findings.violations.length > 0 || findings.staleExempts.length > 0 || findings.missingExemptFiles.length > 0 || findings.missingClaimedBy.length > 0 || strictOrphans && findings.orphans.length > 0;
147
+ }
148
+ var DEFAULT_SRC = "src";
149
+ var init_enforce = () => {};
150
+
151
+ // tools/test/src/coverage-policy/discover.ts
152
+ import { existsSync } from "node:fs";
153
+ import { isAbsolute, join, resolve } from "node:path";
154
+ function isCoverageConfig(value) {
155
+ return typeof value === "object" && value !== null;
156
+ }
157
+ async function importConfig(path) {
158
+ const mod = await import(path);
159
+ const candidate = mod["config"] ?? mod["default"];
160
+ if (!isCoverageConfig(candidate)) {
161
+ throw new Error(`${path} must export \`config\` (a CoverageConfig object)`);
162
+ }
163
+ return candidate;
164
+ }
165
+ async function loadCoverageConfig(root, explicitPath) {
166
+ if (explicitPath) {
167
+ const abs = isAbsolute(explicitPath) ? explicitPath : resolve(root, explicitPath);
168
+ if (!existsSync(abs))
169
+ throw new Error(`coverage config not found: ${abs}`);
170
+ return { config: await importConfig(abs), source: abs };
171
+ }
172
+ for (const name of ["coverage.config.ts", "coverage.config.js"]) {
173
+ const abs = join(root, name);
174
+ if (existsSync(abs))
175
+ return { config: await importConfig(abs), source: abs };
176
+ }
177
+ return { config: {}, source: null };
178
+ }
179
+
180
+ // tools/test/src/coverage-policy/index.ts
181
+ init_enforce();
182
+
183
+ // tools/test/src/coverage-policy/mutate-targets.ts
184
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
185
+ import { join as join3, resolve as resolve3 } from "node:path";
186
+ import { Glob as Glob2 } from "bun";
187
+ async function findStrykerConfigs(root) {
188
+ const found = [];
189
+ const single = join3(root, "stryker.conf.json");
190
+ if (existsSync3(single))
191
+ found.push(single);
192
+ const glob = new Glob2("stryker/*.{json,conf.json}");
193
+ for await (const rel of glob.scan({ cwd: root, onlyFiles: true })) {
194
+ found.push(join3(root, rel));
195
+ }
196
+ return found.sort();
197
+ }
198
+ function isGlob(pattern) {
199
+ return pattern.includes("*") || pattern.includes("?") || pattern.includes("[");
200
+ }
201
+ async function resolvesToFile(pattern, cwd) {
202
+ if (pattern.startsWith("!"))
203
+ return true;
204
+ if (!isGlob(pattern))
205
+ return existsSync3(resolve3(cwd, pattern));
206
+ const glob = new Glob2(pattern);
207
+ for await (const _ of glob.scan({ cwd, onlyFiles: true }))
208
+ return true;
209
+ return false;
210
+ }
211
+ async function checkField(configPath, field, patterns, cwd) {
212
+ const issues = [];
213
+ for (const pattern of patterns ?? []) {
214
+ if (!await resolvesToFile(pattern, cwd)) {
215
+ issues.push({ config: configPath, field, pattern });
216
+ }
217
+ }
218
+ return issues;
219
+ }
220
+ async function validateMutateTargets(root) {
221
+ const configs = await findStrykerConfigs(root);
222
+ const issues = [];
223
+ for (const configPath of configs) {
224
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
225
+ const testFiles = config.testFiles ?? config.bun?.testFiles;
226
+ issues.push(...await checkField(configPath, "mutate", config.mutate, root));
227
+ issues.push(...await checkField(configPath, "testFiles", testFiles, root));
228
+ }
229
+ return { configs, issues };
230
+ }
231
+ export {
232
+ validateMutateTargets,
233
+ runCoverage,
234
+ parseCoverageTable,
235
+ loadCoverageConfig,
236
+ hasFailure,
237
+ findStrykerConfigs,
238
+ evaluateCoverage,
239
+ enforceCoverage
240
+ };
241
+
242
+ //# debugId=046D33457FD03F6D64756E2164756E21
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../tools/test/src/coverage-policy/enforce.ts", "../tools/test/src/coverage-policy/discover.ts", "../tools/test/src/coverage-policy/index.ts", "../tools/test/src/coverage-policy/mutate-targets.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @fairfox/polly/test/coverage — the coverage-policy engine.\n *\n * Parses a `bun test --coverage` table and applies a {@link CoverageConfig}.\n * It fails on four conditions, not just low numbers: a non-exempt file below\n * the floor, an exempt key whose source no longer exists, an exemption whose\n * `claimedBy` test no longer exists, and an exempt file that has climbed back\n * over the floor (promote it). It also reports orphans — source files no unit\n * test imports, the blind spot a coverage table can't show.\n *\n * Everything here is parameterised by the project root, so the same engine\n * backs Polly's own gate and the consumer-facing `polly coverage` command.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { Glob } from \"bun\";\nimport type { CoverageConfig } from \"./types\";\n\nexport interface CoverageRow {\n /** Project-relative, e.g. `src/shared/lib/state.ts`. */\n file: string;\n funcs: number;\n lines: number;\n}\n\nexport interface Violation {\n file: string;\n metric: \"lines\" | \"funcs\";\n observed: number;\n required: number;\n}\n\nexport interface CoverageFindings {\n rowCount: number;\n violations: Violation[];\n staleExempts: string[];\n missingExemptFiles: string[];\n missingClaimedBy: Array<{ file: string; claimedBy: string }>;\n orphans: string[];\n /** True when a floor was configured; false means report-only. */\n enforced: boolean;\n}\n\nconst DEFAULT_SRC = \"src\";\n\n/** `../src/foo.ts` (run from a subdir) → `src/foo.ts` (project-relative). */\nfunction normalizePath(raw: string): string {\n return raw.replace(/^(?:\\.\\.\\/)+/, \"\");\n}\n\n/**\n * Parse the `bun test --coverage` table. Only rows under `srcDir` are\n * policy-bearing; the `All files` summary and test-infra rows are skipped.\n * Column order is `File | % Funcs | % Lines | Uncovered`.\n */\nexport function parseCoverageTable(text: string, srcDir: string): CoverageRow[] {\n const prefix = `${srcDir}/`;\n const rows: CoverageRow[] = [];\n for (const line of text.split(\"\\n\")) {\n if (!line.includes(\"|\")) continue;\n if (line.includes(\"All files\") || line.includes(\"% Funcs\")) continue;\n if (line.trim().startsWith(\"---\")) continue;\n\n const cells = line.split(\"|\").map((c) => c.trim());\n if (cells.length < 3) continue;\n\n const file = normalizePath(cells[0] ?? \"\");\n const funcs = Number(cells[1]);\n const lines = Number(cells[2]);\n if (!file.startsWith(prefix) || Number.isNaN(funcs) || Number.isNaN(lines)) continue;\n\n rows.push({ file, funcs, lines });\n }\n return rows;\n}\n\n/** Run `bun test --coverage` in the configured cwd and return combined output. */\nexport async function runCoverage(root: string, testCwd: string): Promise<string> {\n const proc = Bun.spawn([\"bun\", \"test\", \"--coverage\"], {\n cwd: join(root, testCwd),\n stdout: \"pipe\",\n stderr: \"pipe\",\n });\n const [out, err] = await Promise.all([\n new Response(proc.stdout).text(),\n new Response(proc.stderr).text(),\n ]);\n await proc.exited;\n if (proc.exitCode !== 0) {\n throw new Error(`bun test --coverage exited ${proc.exitCode}\\n${err}`);\n }\n return `${out}\\n${err}`;\n}\n\nfunction evaluateRows(\n rows: CoverageRow[],\n config: CoverageConfig\n): { violations: Violation[]; staleExempts: string[] } {\n const t = config.defaultThreshold;\n const exempt = config.exempt ?? {};\n const violations: Violation[] = [];\n const staleExempts: string[] = [];\n if (!t) return { violations, staleExempts };\n\n for (const row of rows) {\n if (exempt[row.file]) {\n if (row.lines >= t.lines && row.funcs >= t.funcs) staleExempts.push(row.file);\n continue;\n }\n if (row.lines < t.lines) {\n violations.push({ file: row.file, metric: \"lines\", observed: row.lines, required: t.lines });\n }\n if (row.funcs < t.funcs) {\n violations.push({ file: row.file, metric: \"funcs\", observed: row.funcs, required: t.funcs });\n }\n }\n return { violations, staleExempts };\n}\n\nfunction validateExemptions(\n root: string,\n config: CoverageConfig\n): { missingExemptFiles: string[]; missingClaimedBy: Array<{ file: string; claimedBy: string }> } {\n const missingExemptFiles: string[] = [];\n const missingClaimedBy: Array<{ file: string; claimedBy: string }> = [];\n for (const [file, entry] of Object.entries(config.exempt ?? {})) {\n if (!existsSync(resolve(root, file))) missingExemptFiles.push(file);\n const claimedBy = entry.claimedBy.trim();\n if (!claimedBy.startsWith(\"n/a\") && !existsSync(resolve(root, claimedBy))) {\n missingClaimedBy.push({ file, claimedBy });\n }\n }\n return { missingExemptFiles, missingClaimedBy };\n}\n\n/** Source files no row covers and no exemption names — the coverage blind spot. */\nasync function findOrphans(\n root: string,\n srcDir: string,\n covered: Set<string>,\n config: CoverageConfig\n): Promise<string[]> {\n const exempt = config.exempt ?? {};\n const orphans: string[] = [];\n const glob = new Glob(`${srcDir}/**/*.{ts,tsx}`);\n for await (const file of glob.scan({ cwd: root, onlyFiles: true })) {\n if (file.endsWith(\".d.ts\") || /\\.test\\.tsx?$/.test(file) || file.includes(\"/__tests__/\")) {\n continue;\n }\n if (covered.has(file) || exempt[file]) continue;\n orphans.push(file);\n }\n return orphans.sort();\n}\n\n/** Apply the policy to a parsed table. Pure — no spawning. */\nexport async function evaluateCoverage(\n root: string,\n rows: CoverageRow[],\n config: CoverageConfig\n): Promise<CoverageFindings> {\n const srcDir = config.srcDir ?? DEFAULT_SRC;\n const { violations, staleExempts } = evaluateRows(rows, config);\n const { missingExemptFiles, missingClaimedBy } = validateExemptions(root, config);\n const orphans = await findOrphans(root, srcDir, new Set(rows.map((r) => r.file)), config);\n return {\n rowCount: rows.length,\n violations,\n staleExempts,\n missingExemptFiles,\n missingClaimedBy,\n orphans,\n enforced: config.defaultThreshold !== undefined,\n };\n}\n\n/** Run the suite under coverage and apply the policy. */\nexport async function enforceCoverage(\n root: string,\n config: CoverageConfig,\n coverageText?: string\n): Promise<CoverageFindings> {\n const srcDir = config.srcDir ?? DEFAULT_SRC;\n const text = coverageText ?? (await runCoverage(root, config.testCwd ?? \".\"));\n const rows = parseCoverageTable(text, srcDir);\n return evaluateCoverage(root, rows, config);\n}\n\n/** True when the findings represent a policy failure (orphans are advisory\n * unless `strictOrphans`). */\nexport function hasFailure(findings: CoverageFindings, strictOrphans: boolean): boolean {\n return (\n findings.violations.length > 0 ||\n findings.staleExempts.length > 0 ||\n findings.missingExemptFiles.length > 0 ||\n findings.missingClaimedBy.length > 0 ||\n (strictOrphans && findings.orphans.length > 0)\n );\n}\n",
6
+ "/**\n * @fairfox/polly/test/coverage — zero-config loading.\n *\n * `polly coverage` runs without any config: it looks for a `coverage.config.ts`\n * (or `.js`) at the project root, and if there is none it returns an empty\n * config, which the engine treats as report-only — it prints the numbers and\n * the orphan count but never fails the build. A consumer opts into enforcement\n * by adding a `defaultThreshold`, and into legible tiering by adding `exempt`\n * entries. This mirrors how `polly test --tier` discovers tiers by convention.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport type { CoverageConfig } from \"./types\";\n\nexport interface LoadedConfig {\n config: CoverageConfig;\n /** Absolute path the config was loaded from, or null for the zero-config default. */\n source: string | null;\n}\n\nfunction isCoverageConfig(value: unknown): value is CoverageConfig {\n return typeof value === \"object\" && value !== null;\n}\n\nasync function importConfig(path: string): Promise<CoverageConfig> {\n const mod: Record<string, unknown> = await import(path);\n const candidate = mod[\"config\"] ?? mod[\"default\"];\n if (!isCoverageConfig(candidate)) {\n throw new Error(`${path} must export \\`config\\` (a CoverageConfig object)`);\n }\n return candidate;\n}\n\n/**\n * Resolve the coverage config. With `explicitPath` (Polly's own front-end\n * passes scripts/coverage.config.ts) that file must exist. Otherwise look for\n * coverage.config.{ts,js} at the root; absent → the zero-config report-only\n * default.\n */\nexport async function loadCoverageConfig(\n root: string,\n explicitPath?: string\n): Promise<LoadedConfig> {\n if (explicitPath) {\n const abs = isAbsolute(explicitPath) ? explicitPath : resolve(root, explicitPath);\n if (!existsSync(abs)) throw new Error(`coverage config not found: ${abs}`);\n return { config: await importConfig(abs), source: abs };\n }\n\n for (const name of [\"coverage.config.ts\", \"coverage.config.js\"]) {\n const abs = join(root, name);\n if (existsSync(abs)) return { config: await importConfig(abs), source: abs };\n }\n return { config: {}, source: null };\n}\n",
7
+ "/**\n * @fairfox/polly/test/coverage — programmatic API behind `polly coverage`.\n *\n * Use the CLI (`polly coverage`) for the common case; import these to script\n * the policy yourself or to add a coverage tier to a custom runner.\n */\n\nexport { type LoadedConfig, loadCoverageConfig } from \"./discover\";\nexport {\n type CoverageFindings,\n type CoverageRow,\n enforceCoverage,\n evaluateCoverage,\n hasFailure,\n parseCoverageTable,\n runCoverage,\n type Violation,\n} from \"./enforce\";\nexport {\n findStrykerConfigs,\n type MutateTargetIssue,\n type MutateTargetReport,\n validateMutateTargets,\n} from \"./mutate-targets\";\nexport type { CoverageConfig, ExemptEntry, FileThreshold } from \"./types\";\n",
8
+ "/**\n * @fairfox/polly/test/coverage — Stryker mutate-/test-target validation.\n *\n * A Stryker config's `mutate` and `testFiles` lists are hand-curated. A path\n * that is renamed, or a glob whose directory moves, silently resolves to\n * nothing — Stryker mutates fewer files (or none) and the only signal is a\n * long mutation run whose score quietly drops. This asserts, in milliseconds,\n * that every entry still resolves to at least one file.\n *\n * Discovers configs two ways, covering both layouts in the wild: a single\n * `stryker.conf.json` at the root, and a `stryker/` directory of per-package\n * shards.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { Glob } from \"bun\";\n\ninterface StrykerConfig {\n mutate?: string[];\n testFiles?: string[];\n bun?: { testFiles?: string[] };\n}\n\nexport interface MutateTargetIssue {\n config: string;\n field: \"mutate\" | \"testFiles\";\n pattern: string;\n}\n\nexport interface MutateTargetReport {\n configs: string[];\n issues: MutateTargetIssue[];\n}\n\n/** Locate every Stryker config under the project root. */\nexport async function findStrykerConfigs(root: string): Promise<string[]> {\n const found: string[] = [];\n const single = join(root, \"stryker.conf.json\");\n if (existsSync(single)) found.push(single);\n\n const glob = new Glob(\"stryker/*.{json,conf.json}\");\n for await (const rel of glob.scan({ cwd: root, onlyFiles: true })) {\n found.push(join(root, rel));\n }\n return found.sort();\n}\n\nfunction isGlob(pattern: string): boolean {\n return pattern.includes(\"*\") || pattern.includes(\"?\") || pattern.includes(\"[\");\n}\n\nasync function resolvesToFile(pattern: string, cwd: string): Promise<boolean> {\n if (pattern.startsWith(\"!\")) return true; // negations are filters, not targets\n if (!isGlob(pattern)) return existsSync(resolve(cwd, pattern));\n const glob = new Glob(pattern);\n for await (const _ of glob.scan({ cwd, onlyFiles: true })) return true;\n return false;\n}\n\nasync function checkField(\n configPath: string,\n field: \"mutate\" | \"testFiles\",\n patterns: string[] | undefined,\n cwd: string\n): Promise<MutateTargetIssue[]> {\n const issues: MutateTargetIssue[] = [];\n for (const pattern of patterns ?? []) {\n if (!(await resolvesToFile(pattern, cwd))) {\n issues.push({ config: configPath, field, pattern });\n }\n }\n return issues;\n}\n\n/**\n * Validate every Stryker config under `root`. Paths in a Stryker config are\n * relative to that config's directory; for the monorepo-root configs that is\n * the root, so we resolve globs against `root`.\n */\nexport async function validateMutateTargets(root: string): Promise<MutateTargetReport> {\n const configs = await findStrykerConfigs(root);\n const issues: MutateTargetIssue[] = [];\n for (const configPath of configs) {\n const config = JSON.parse(readFileSync(configPath, \"utf8\")) as unknown as StrykerConfig;\n const testFiles = config.testFiles ?? config.bun?.testFiles;\n issues.push(...(await checkField(configPath, \"mutate\", config.mutate, root)));\n issues.push(...(await checkField(configPath, \"testFiles\", testFiles, root)));\n }\n return { configs, issues };\n}\n"
9
+ ],
10
+ "mappings": "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",
11
+ "debugId": "046D33457FD03F6D64756E2164756E21",
12
+ "names": []
13
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @fairfox/polly/test/coverage — Stryker mutate-/test-target validation.
3
+ *
4
+ * A Stryker config's `mutate` and `testFiles` lists are hand-curated. A path
5
+ * that is renamed, or a glob whose directory moves, silently resolves to
6
+ * nothing — Stryker mutates fewer files (or none) and the only signal is a
7
+ * long mutation run whose score quietly drops. This asserts, in milliseconds,
8
+ * that every entry still resolves to at least one file.
9
+ *
10
+ * Discovers configs two ways, covering both layouts in the wild: a single
11
+ * `stryker.conf.json` at the root, and a `stryker/` directory of per-package
12
+ * shards.
13
+ */
14
+ export interface MutateTargetIssue {
15
+ config: string;
16
+ field: "mutate" | "testFiles";
17
+ pattern: string;
18
+ }
19
+ export interface MutateTargetReport {
20
+ configs: string[];
21
+ issues: MutateTargetIssue[];
22
+ }
23
+ /** Locate every Stryker config under the project root. */
24
+ export declare function findStrykerConfigs(root: string): Promise<string[]>;
25
+ /**
26
+ * Validate every Stryker config under `root`. Paths in a Stryker config are
27
+ * relative to that config's directory; for the monorepo-root configs that is
28
+ * the root, so we resolve globs against `root`.
29
+ */
30
+ export declare function validateMutateTargets(root: string): Promise<MutateTargetReport>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @fairfox/polly/test/coverage — the per-file coverage policy a consumer can
3
+ * declare. Every field is optional so the tool runs zero-config: with no
4
+ * `coverage.config.ts` the enforcer reports numbers and orphans without
5
+ * failing; add a `defaultThreshold` to start enforcing, and `exempt` entries
6
+ * to record which higher-tier test covers a unit-thin file.
7
+ */
8
+ export interface FileThreshold {
9
+ /** Minimum `% Lines` from the `bun test --coverage` table. */
10
+ lines: number;
11
+ /** Minimum `% Funcs` from the `bun test --coverage` table. */
12
+ funcs: number;
13
+ }
14
+ export interface ExemptEntry {
15
+ /** Why this file is thin at the unit tier. */
16
+ reason: string;
17
+ /**
18
+ * Package-relative path to the test or script that exercises this file at a
19
+ * higher tier. Verified to exist by the enforcer. Use the `'n/a — <reason>'`
20
+ * form for genuine waivers (extension-only shims, browser-only geometry).
21
+ */
22
+ claimedBy: string;
23
+ }
24
+ export interface CoverageConfig {
25
+ /** Per-file floor. Omit to run report-only (numbers + orphans, never fails). */
26
+ defaultThreshold?: FileThreshold;
27
+ /** Files below the floor that are covered at a higher tier, keyed by
28
+ * source path relative to the project root (e.g. `src/shared/lib/x.ts`). */
29
+ exempt?: Record<string, ExemptEntry>;
30
+ /** Source directory to police, relative to the project root. Default `src`. */
31
+ srcDir?: string;
32
+ /** Directory to run `bun test --coverage` in, relative to the project root.
33
+ * Default `.` (the root). Polly itself runs its suite from `tests`. */
34
+ testCwd?: string;
35
+ }
@@ -1010,6 +1010,21 @@ ${logs}`);
1010
1010
  }
1011
1011
  };
1012
1012
  }
1013
+ // tools/test/src/e2e-shared/timeout-context.ts
1014
+ async function resolveContext(context) {
1015
+ if (!context)
1016
+ return "";
1017
+ try {
1018
+ return `
1019
+
1020
+ transport: ${await context()}`;
1021
+ } catch (err) {
1022
+ return `
1023
+
1024
+ transport: <context threw: ${err instanceof Error ? err.message : String(err)}>`;
1025
+ }
1026
+ }
1027
+
1013
1028
  // tools/test/src/e2e-mesh/wait-for-convergence.ts
1014
1029
  async function readPeerSnapshot(peer) {
1015
1030
  return peer.page.evaluate(() => {
@@ -1033,8 +1048,9 @@ async function waitForConvergence(peers, predicate, options = {}) {
1033
1048
  }
1034
1049
  const summary = lastSnapshots.map((s) => ` ${s.peerId}: status="${s.status}" peerCount=${s.peerCount} items=${JSON.stringify(s.items)}`).join(`
1035
1050
  `);
1051
+ const transport = await resolveContext(options.context);
1036
1052
  throw new Error(`waitForConvergence: predicate did not hold for every peer within ${timeoutMs}ms.
1037
- ${summary}`);
1053
+ ${summary}${transport}`);
1038
1054
  }
1039
1055
  async function waitForMeshConnected(peers, options = {}) {
1040
1056
  const minPeers = peers.length - 1;
@@ -1209,4 +1225,4 @@ export {
1209
1225
  MESH_CONSOLE_ALLOWLIST
1210
1226
  };
1211
1227
 
1212
- //# debugId=DBF39E19E602FCC464756E2164756E21
1228
+ //# debugId=3188CA6C80D67D2E64756E2164756E21