mandrel-platform 0.17.0 → 0.17.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.
package/package.json
CHANGED
|
@@ -14,11 +14,21 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import assert from "node:assert/strict";
|
|
17
|
-
import {
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import {
|
|
19
|
+
mkdtempSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "node:fs";
|
|
18
25
|
import { tmpdir } from "node:os";
|
|
19
|
-
import { join } from "node:path";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
20
28
|
import { test } from "node:test";
|
|
21
29
|
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
|
|
22
32
|
import {
|
|
23
33
|
VALID_METRICS,
|
|
24
34
|
parseArgs,
|
|
@@ -33,11 +43,21 @@ import {
|
|
|
33
43
|
} from "./check-coverage-threshold.mjs";
|
|
34
44
|
|
|
35
45
|
// Build a minimal Istanbul/c8/vitest-shaped coverage-summary object.
|
|
36
|
-
function summary({
|
|
46
|
+
function summary({
|
|
47
|
+
lines = 0,
|
|
48
|
+
statements = 0,
|
|
49
|
+
functions = 0,
|
|
50
|
+
branches = 0,
|
|
51
|
+
} = {}) {
|
|
37
52
|
return {
|
|
38
53
|
total: {
|
|
39
54
|
lines: { total: 100, covered: lines, skipped: 0, pct: lines },
|
|
40
|
-
statements: {
|
|
55
|
+
statements: {
|
|
56
|
+
total: 100,
|
|
57
|
+
covered: statements,
|
|
58
|
+
skipped: 0,
|
|
59
|
+
pct: statements,
|
|
60
|
+
},
|
|
41
61
|
functions: { total: 100, covered: functions, skipped: 0, pct: functions },
|
|
42
62
|
branches: { total: 100, covered: branches, skipped: 0, pct: branches },
|
|
43
63
|
},
|
|
@@ -80,10 +100,14 @@ test("parseArgs defaults: threshold 0, metric lines", () => {
|
|
|
80
100
|
|
|
81
101
|
test("parseArgs reads --threshold/--metric/--coverage-dir", () => {
|
|
82
102
|
const opts = parseArgs([
|
|
83
|
-
"--threshold",
|
|
84
|
-
"
|
|
85
|
-
"--
|
|
86
|
-
"
|
|
103
|
+
"--threshold",
|
|
104
|
+
"85",
|
|
105
|
+
"--metric",
|
|
106
|
+
"Statements",
|
|
107
|
+
"--coverage-dir",
|
|
108
|
+
"packages/api/coverage",
|
|
109
|
+
"--coverage-dir",
|
|
110
|
+
"packages/web/coverage",
|
|
87
111
|
]);
|
|
88
112
|
assert.equal(opts.threshold, 85);
|
|
89
113
|
assert.equal(opts.metric, "statements"); // normalized lowercase
|
|
@@ -98,7 +122,12 @@ test("parseArgs rejects an unknown --metric", () => {
|
|
|
98
122
|
});
|
|
99
123
|
|
|
100
124
|
test("VALID_METRICS covers the four Istanbul totals", () => {
|
|
101
|
-
assert.deepEqual(VALID_METRICS, [
|
|
125
|
+
assert.deepEqual(VALID_METRICS, [
|
|
126
|
+
"lines",
|
|
127
|
+
"statements",
|
|
128
|
+
"functions",
|
|
129
|
+
"branches",
|
|
130
|
+
]);
|
|
102
131
|
});
|
|
103
132
|
|
|
104
133
|
// ---------------------------------------------------------------------------
|
|
@@ -141,7 +170,7 @@ test("evaluateGate: threshold 0 is a no-op pass (skipped)", () => {
|
|
|
141
170
|
read: () => {
|
|
142
171
|
throw new Error("read should not run when gate is disabled");
|
|
143
172
|
},
|
|
144
|
-
}
|
|
173
|
+
},
|
|
145
174
|
);
|
|
146
175
|
assert.equal(verdict.ok, true);
|
|
147
176
|
assert.equal(verdict.skipped, true);
|
|
@@ -158,7 +187,7 @@ test("evaluateGate: SET + measured above floor → pass", () => {
|
|
|
158
187
|
{
|
|
159
188
|
findSummaries: () => ["/x/coverage/coverage-summary.json"],
|
|
160
189
|
read: () => summary({ lines: 91 }),
|
|
161
|
-
}
|
|
190
|
+
},
|
|
162
191
|
);
|
|
163
192
|
assert.equal(verdict.ok, true);
|
|
164
193
|
assert.equal(verdict.skipped, false);
|
|
@@ -172,7 +201,7 @@ test("evaluateGate: SET + measured below floor → fail", () => {
|
|
|
172
201
|
{
|
|
173
202
|
findSummaries: () => ["/x/coverage/coverage-summary.json"],
|
|
174
203
|
read: () => summary({ lines: 73 }),
|
|
175
|
-
}
|
|
204
|
+
},
|
|
176
205
|
);
|
|
177
206
|
assert.equal(verdict.ok, false);
|
|
178
207
|
assert.equal(verdict.results[0].pct, 73);
|
|
@@ -185,7 +214,7 @@ test("evaluateGate: SET but no coverage summary found → fail (never silent-pas
|
|
|
185
214
|
{
|
|
186
215
|
findSummaries: () => [],
|
|
187
216
|
read: () => null,
|
|
188
|
-
}
|
|
217
|
+
},
|
|
189
218
|
);
|
|
190
219
|
assert.equal(verdict.ok, false);
|
|
191
220
|
assert.equal(verdict.skipped, false);
|
|
@@ -196,9 +225,15 @@ test("evaluateGate: SET, one of many packages below floor → fail", () => {
|
|
|
196
225
|
const verdict = evaluateGate(
|
|
197
226
|
{ threshold: 80, metric: "statements", cwd: ".", coverageDirs: [] },
|
|
198
227
|
{
|
|
199
|
-
findSummaries: () => [
|
|
200
|
-
|
|
201
|
-
|
|
228
|
+
findSummaries: () => [
|
|
229
|
+
"a/coverage/coverage-summary.json",
|
|
230
|
+
"b/coverage/coverage-summary.json",
|
|
231
|
+
],
|
|
232
|
+
read: (f) =>
|
|
233
|
+
f.startsWith("a")
|
|
234
|
+
? summary({ statements: 95 })
|
|
235
|
+
: summary({ statements: 40 }),
|
|
236
|
+
},
|
|
202
237
|
);
|
|
203
238
|
assert.equal(verdict.ok, false);
|
|
204
239
|
assert.equal(verdict.results.length, 2);
|
|
@@ -217,24 +252,29 @@ test("findCoverageSummaries auto-scans **/coverage/, pruning node_modules + dott
|
|
|
217
252
|
mkdirSync(join(root, "packages", "api", "coverage"), { recursive: true });
|
|
218
253
|
writeFileSync(
|
|
219
254
|
join(root, "packages", "api", "coverage", "coverage-summary.json"),
|
|
220
|
-
JSON.stringify(summary({ lines: 88 }))
|
|
255
|
+
JSON.stringify(summary({ lines: 88 })),
|
|
221
256
|
);
|
|
222
257
|
// A decoy under node_modules that MUST be pruned.
|
|
223
|
-
mkdirSync(join(root, "node_modules", "dep", "coverage"), {
|
|
258
|
+
mkdirSync(join(root, "node_modules", "dep", "coverage"), {
|
|
259
|
+
recursive: true,
|
|
260
|
+
});
|
|
224
261
|
writeFileSync(
|
|
225
262
|
join(root, "node_modules", "dep", "coverage", "coverage-summary.json"),
|
|
226
|
-
JSON.stringify(summary({ lines: 1 }))
|
|
263
|
+
JSON.stringify(summary({ lines: 1 })),
|
|
227
264
|
);
|
|
228
265
|
// A decoy under a dotted dir that MUST be pruned.
|
|
229
266
|
mkdirSync(join(root, ".agents", "coverage"), { recursive: true });
|
|
230
267
|
writeFileSync(
|
|
231
268
|
join(root, ".agents", "coverage", "coverage-summary.json"),
|
|
232
|
-
JSON.stringify(summary({ lines: 2 }))
|
|
269
|
+
JSON.stringify(summary({ lines: 2 })),
|
|
233
270
|
);
|
|
234
271
|
|
|
235
272
|
const files = findCoverageSummaries(root);
|
|
236
273
|
assert.equal(files.length, 1);
|
|
237
|
-
assert.match(
|
|
274
|
+
assert.match(
|
|
275
|
+
files[0],
|
|
276
|
+
/packages[/\\]api[/\\]coverage[/\\]coverage-summary\.json$/,
|
|
277
|
+
);
|
|
238
278
|
|
|
239
279
|
const parsed = readSummary(files[0]);
|
|
240
280
|
assert.equal(extractPct(parsed, "lines"), 88);
|
|
@@ -251,29 +291,37 @@ test("findCoverageSummaries discovers a per-workspace fan-out layout (coverage/<
|
|
|
251
291
|
mkdirSync(join(root, "coverage", "web"), { recursive: true });
|
|
252
292
|
writeFileSync(
|
|
253
293
|
join(root, "coverage", "web", "coverage-summary.json"),
|
|
254
|
-
JSON.stringify(summary({ lines: 77 }))
|
|
294
|
+
JSON.stringify(summary({ lines: 77 })),
|
|
255
295
|
);
|
|
256
296
|
mkdirSync(join(root, "coverage", "shared"), { recursive: true });
|
|
257
297
|
writeFileSync(
|
|
258
298
|
join(root, "coverage", "shared", "coverage-summary.json"),
|
|
259
|
-
JSON.stringify(summary({ lines: 93 }))
|
|
299
|
+
JSON.stringify(summary({ lines: 93 })),
|
|
260
300
|
);
|
|
261
301
|
// Decoys that must still be pruned.
|
|
262
|
-
mkdirSync(join(root, "node_modules", "dep", "coverage"), {
|
|
302
|
+
mkdirSync(join(root, "node_modules", "dep", "coverage"), {
|
|
303
|
+
recursive: true,
|
|
304
|
+
});
|
|
263
305
|
writeFileSync(
|
|
264
306
|
join(root, "node_modules", "dep", "coverage", "coverage-summary.json"),
|
|
265
|
-
JSON.stringify(summary({ lines: 1 }))
|
|
307
|
+
JSON.stringify(summary({ lines: 1 })),
|
|
266
308
|
);
|
|
267
309
|
mkdirSync(join(root, ".agents", "coverage"), { recursive: true });
|
|
268
310
|
writeFileSync(
|
|
269
311
|
join(root, ".agents", "coverage", "coverage-summary.json"),
|
|
270
|
-
JSON.stringify(summary({ lines: 2 }))
|
|
312
|
+
JSON.stringify(summary({ lines: 2 })),
|
|
271
313
|
);
|
|
272
314
|
|
|
273
315
|
const files = findCoverageSummaries(root).sort();
|
|
274
316
|
assert.equal(files.length, 2);
|
|
275
|
-
assert.ok(
|
|
276
|
-
|
|
317
|
+
assert.ok(
|
|
318
|
+
files.some((f) =>
|
|
319
|
+
/coverage[/\\]shared[/\\]coverage-summary\.json$/.test(f),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
assert.ok(
|
|
323
|
+
files.some((f) => /coverage[/\\]web[/\\]coverage-summary\.json$/.test(f)),
|
|
324
|
+
);
|
|
277
325
|
} finally {
|
|
278
326
|
rmSync(root, { recursive: true, force: true });
|
|
279
327
|
}
|
|
@@ -285,7 +333,7 @@ test("findCoverageSummaries honours explicit --coverage-dir roots", () => {
|
|
|
285
333
|
mkdirSync(join(root, "custom", "cov"), { recursive: true });
|
|
286
334
|
writeFileSync(
|
|
287
335
|
join(root, "custom", "cov", "coverage-summary.json"),
|
|
288
|
-
JSON.stringify(summary({ lines: 90 }))
|
|
336
|
+
JSON.stringify(summary({ lines: 90 })),
|
|
289
337
|
);
|
|
290
338
|
const files = findCoverageSummaries(root, ["custom/cov"]);
|
|
291
339
|
assert.equal(files.length, 1);
|
|
@@ -327,7 +375,7 @@ test("runCli: threshold set, real passing coverage tree → exit 0", () => {
|
|
|
327
375
|
mkdirSync(join(root, "coverage"), { recursive: true });
|
|
328
376
|
writeFileSync(
|
|
329
377
|
join(root, "coverage", "coverage-summary.json"),
|
|
330
|
-
JSON.stringify(summary({ lines: 95 }))
|
|
378
|
+
JSON.stringify(summary({ lines: 95 })),
|
|
331
379
|
);
|
|
332
380
|
const out = [];
|
|
333
381
|
const code = runCli(["--threshold", "80", "--cwd", root], {
|
|
@@ -347,7 +395,7 @@ test("runCli: threshold set, real failing coverage tree → exit 1", () => {
|
|
|
347
395
|
mkdirSync(join(root, "coverage"), { recursive: true });
|
|
348
396
|
writeFileSync(
|
|
349
397
|
join(root, "coverage", "coverage-summary.json"),
|
|
350
|
-
JSON.stringify(summary({ lines: 50 }))
|
|
398
|
+
JSON.stringify(summary({ lines: 50 })),
|
|
351
399
|
);
|
|
352
400
|
const out = [];
|
|
353
401
|
const code = runCli(["--threshold", "80", "--cwd", root], {
|
|
@@ -378,9 +426,179 @@ test("runCli: threshold set but no coverage data → exit 1", () => {
|
|
|
378
426
|
|
|
379
427
|
test("formatVerdict renders a skip line for the disabled gate", () => {
|
|
380
428
|
const lines = formatVerdict({
|
|
381
|
-
ok: true,
|
|
382
|
-
|
|
429
|
+
ok: true,
|
|
430
|
+
skipped: true,
|
|
431
|
+
reason: "threshold 0 — coverage gate disabled (no-op)",
|
|
432
|
+
threshold: 0,
|
|
433
|
+
metric: "lines",
|
|
434
|
+
results: [],
|
|
383
435
|
});
|
|
384
436
|
assert.equal(lines.length, 1);
|
|
385
437
|
assert.match(lines[0], /⏭️/);
|
|
386
438
|
});
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// pr-quality.yml Coverage threshold gate — inline step regression (#163)
|
|
442
|
+
//
|
|
443
|
+
// The workflow step embeds its OWN copy of the discovery logic (it must stay
|
|
444
|
+
// dependency-free for consumer checkouts that have not adopted
|
|
445
|
+
// check-coverage-threshold.mjs). #158 fixed only the standalone script; this
|
|
446
|
+
// suite's `findCoverageSummaries` tests above cannot catch a regression in the
|
|
447
|
+
// workflow file's embedded JS. These tests extract that embedded node script
|
|
448
|
+
// straight out of pr-quality.yml and run it against real fixture trees, so the
|
|
449
|
+
// exact #163 bug (matching a directory literally named `coverage`, missing
|
|
450
|
+
// `coverage/<workspace>/coverage-summary.json`) cannot survive a refactor.
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
// Pull the embedded `node --input-type=module - <<'NODE' … NODE` script body
|
|
454
|
+
// out of the "Coverage threshold gate" step, de-indenting the YAML block
|
|
455
|
+
// scalar so it runs as a standalone ES module.
|
|
456
|
+
function extractGateScript() {
|
|
457
|
+
const yaml = readFileSync(
|
|
458
|
+
join(__dirname, "..", ".github", "workflows", "pr-quality.yml"),
|
|
459
|
+
"utf8",
|
|
460
|
+
);
|
|
461
|
+
const lines = yaml.split(/\r?\n/);
|
|
462
|
+
const startIdx = lines.findIndex((l) =>
|
|
463
|
+
/node --input-type=module - <<'NODE'\s*$/.test(l),
|
|
464
|
+
);
|
|
465
|
+
assert.ok(
|
|
466
|
+
startIdx !== -1,
|
|
467
|
+
"expected an embedded `<<'NODE'` heredoc in pr-quality.yml",
|
|
468
|
+
);
|
|
469
|
+
// The heredoc opener carries the block-scalar indent; every body line shares
|
|
470
|
+
// at least that indent, and the closing `NODE` sentinel sits at the same
|
|
471
|
+
// indent. Strip it uniformly.
|
|
472
|
+
const indent = lines[startIdx].match(/^(\s*)/)[1];
|
|
473
|
+
const body = [];
|
|
474
|
+
for (let i = startIdx + 1; i < lines.length; i += 1) {
|
|
475
|
+
if (lines[i].trim() === "NODE") return body.join("\n");
|
|
476
|
+
body.push(
|
|
477
|
+
lines[i].startsWith(indent) ? lines[i].slice(indent.length) : lines[i],
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
throw new Error("unterminated `NODE` heredoc in pr-quality.yml gate step");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Run the extracted gate script with COVERAGE_THRESHOLD / COVERAGE_METRIC set
|
|
484
|
+
// and cwd pointed at `treeRoot`. Returns { status, stdout, stderr }.
|
|
485
|
+
function runGateStep(treeRoot, { threshold = "0", metric = "lines" } = {}) {
|
|
486
|
+
const scriptPath = join(treeRoot, "__gate-step.mjs");
|
|
487
|
+
writeFileSync(scriptPath, extractGateScript());
|
|
488
|
+
try {
|
|
489
|
+
const stdout = execFileSync("node", [scriptPath], {
|
|
490
|
+
cwd: treeRoot,
|
|
491
|
+
env: {
|
|
492
|
+
...process.env,
|
|
493
|
+
COVERAGE_THRESHOLD: threshold,
|
|
494
|
+
COVERAGE_METRIC: metric,
|
|
495
|
+
},
|
|
496
|
+
encoding: "utf8",
|
|
497
|
+
});
|
|
498
|
+
return { status: 0, stdout, stderr: "" };
|
|
499
|
+
} catch (err) {
|
|
500
|
+
return {
|
|
501
|
+
status: err.status ?? 1,
|
|
502
|
+
stdout: err.stdout?.toString() ?? "",
|
|
503
|
+
stderr: err.stderr?.toString() ?? "",
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
test("pr-quality gate step discovers a per-workspace coverage/<ws>/ layout (#163 regression)", () => {
|
|
509
|
+
const dir = mkdtempSync(join(tmpdir(), "gate-step-fanout-"));
|
|
510
|
+
try {
|
|
511
|
+
// Per-workspace fan-out: NO top-level coverage/coverage-summary.json,
|
|
512
|
+
// only coverage/web/ and coverage/shared/ — the exact shape that the
|
|
513
|
+
// pre-#158 literal-`coverage`-name match missed.
|
|
514
|
+
mkdirSync(join(dir, "coverage", "web"), { recursive: true });
|
|
515
|
+
mkdirSync(join(dir, "coverage", "shared"), { recursive: true });
|
|
516
|
+
writeFileSync(
|
|
517
|
+
join(dir, "coverage", "web", "coverage-summary.json"),
|
|
518
|
+
JSON.stringify(summary({ lines: 95 })),
|
|
519
|
+
);
|
|
520
|
+
writeFileSync(
|
|
521
|
+
join(dir, "coverage", "shared", "coverage-summary.json"),
|
|
522
|
+
JSON.stringify(summary({ lines: 90 })),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const res = runGateStep(dir, { threshold: "80", metric: "lines" });
|
|
526
|
+
assert.equal(
|
|
527
|
+
res.status,
|
|
528
|
+
0,
|
|
529
|
+
`gate step should PASS on the fan-out layout, got exit ${res.status}\n${res.stderr}`,
|
|
530
|
+
);
|
|
531
|
+
// Both per-workspace summaries were discovered and gated — not the
|
|
532
|
+
// "no coverage-summary.json was found" false failure.
|
|
533
|
+
assert.doesNotMatch(res.stderr, /no coverage-summary\.json/);
|
|
534
|
+
assert.match(res.stdout, /coverage[/\\]web[/\\]coverage-summary\.json/);
|
|
535
|
+
assert.match(res.stdout, /coverage[/\\]shared[/\\]coverage-summary\.json/);
|
|
536
|
+
assert.match(res.stdout, /coverage meets the 80% floor/);
|
|
537
|
+
} finally {
|
|
538
|
+
rmSync(dir, { recursive: true, force: true });
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("pr-quality gate step: per-workspace summary below floor fails (#163 regression)", () => {
|
|
543
|
+
const dir = mkdtempSync(join(tmpdir(), "gate-step-fanout-fail-"));
|
|
544
|
+
try {
|
|
545
|
+
mkdirSync(join(dir, "coverage", "web"), { recursive: true });
|
|
546
|
+
writeFileSync(
|
|
547
|
+
join(dir, "coverage", "web", "coverage-summary.json"),
|
|
548
|
+
JSON.stringify(summary({ lines: 40 })),
|
|
549
|
+
);
|
|
550
|
+
const res = runGateStep(dir, { threshold: "80", metric: "lines" });
|
|
551
|
+
assert.equal(
|
|
552
|
+
res.status,
|
|
553
|
+
1,
|
|
554
|
+
"below-floor per-workspace coverage must fail the gate",
|
|
555
|
+
);
|
|
556
|
+
assert.match(res.stderr, /coverage[/\\]web[/\\]coverage-summary\.json/);
|
|
557
|
+
assert.match(res.stderr, /below the 80% floor/);
|
|
558
|
+
} finally {
|
|
559
|
+
rmSync(dir, { recursive: true, force: true });
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("pr-quality gate step stays byte-for-byte compatible with the single coverage/ dir shape (#163)", () => {
|
|
564
|
+
const dir = mkdtempSync(join(tmpdir(), "gate-step-single-"));
|
|
565
|
+
try {
|
|
566
|
+
// Legacy single-workspace shape: coverage/coverage-summary.json directly.
|
|
567
|
+
mkdirSync(join(dir, "coverage"), { recursive: true });
|
|
568
|
+
writeFileSync(
|
|
569
|
+
join(dir, "coverage", "coverage-summary.json"),
|
|
570
|
+
JSON.stringify(summary({ lines: 88 })),
|
|
571
|
+
);
|
|
572
|
+
// node_modules + dotted dirs must still be pruned (never read a vendored
|
|
573
|
+
// coverage tree).
|
|
574
|
+
mkdirSync(join(dir, "node_modules", "pkg", "coverage"), {
|
|
575
|
+
recursive: true,
|
|
576
|
+
});
|
|
577
|
+
writeFileSync(
|
|
578
|
+
join(dir, "node_modules", "pkg", "coverage", "coverage-summary.json"),
|
|
579
|
+
JSON.stringify(summary({ lines: 1 })),
|
|
580
|
+
);
|
|
581
|
+
const res = runGateStep(dir, { threshold: "80", metric: "lines" });
|
|
582
|
+
assert.equal(
|
|
583
|
+
res.status,
|
|
584
|
+
0,
|
|
585
|
+
`single-dir shape should PASS, got exit ${res.status}\n${res.stderr}`,
|
|
586
|
+
);
|
|
587
|
+
assert.match(res.stdout, /coverage[/\\]coverage-summary\.json/);
|
|
588
|
+
assert.doesNotMatch(res.stdout, /node_modules/);
|
|
589
|
+
assert.doesNotMatch(res.stderr, /node_modules/);
|
|
590
|
+
} finally {
|
|
591
|
+
rmSync(dir, { recursive: true, force: true });
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("pr-quality gate step still hard-fails when threshold set but no summary exists (#163)", () => {
|
|
596
|
+
const dir = mkdtempSync(join(tmpdir(), "gate-step-empty-"));
|
|
597
|
+
try {
|
|
598
|
+
const res = runGateStep(dir, { threshold: "80", metric: "lines" });
|
|
599
|
+
assert.equal(res.status, 1, "a set floor must not pass on missing data");
|
|
600
|
+
assert.match(res.stderr, /no coverage-summary\.json/);
|
|
601
|
+
} finally {
|
|
602
|
+
rmSync(dir, { recursive: true, force: true });
|
|
603
|
+
}
|
|
604
|
+
});
|