@tested/cli 0.1.8 → 0.1.10
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/README.md +6 -6
- package/dist/td.js +266 -34
- package/dist/tested.js +266 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ CI uses the composite Action (`tested-hq/cli/action@main`). It installs `@tested
|
|
|
25
25
|
```yaml
|
|
26
26
|
- uses: tested-hq/cli/action@main
|
|
27
27
|
with:
|
|
28
|
-
version: 0.1.
|
|
28
|
+
version: 0.1.10
|
|
29
29
|
token: ${{ secrets.TESTED_TOKEN }}
|
|
30
30
|
```
|
|
31
31
|
|
|
@@ -132,7 +132,7 @@ If `thresholds` is missing from `.tested.yaml`, `tested check` prints a notice o
|
|
|
132
132
|
|
|
133
133
|
### Flags (per package)
|
|
134
134
|
|
|
135
|
-
Independent floors so a monorepo total cannot hide one package. Each flag is graded from **this run's** coverage files
|
|
135
|
+
Independent floors so a monorepo total cannot hide one package. Each flag is graded from **this run's** coverage files.
|
|
136
136
|
|
|
137
137
|
```yaml
|
|
138
138
|
flags:
|
|
@@ -144,11 +144,11 @@ flags:
|
|
|
144
144
|
paths: ["apps/api/**"]
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
`tested check` applies global `thresholds` plus each flag whose paths appear in the merged coverage. Per-flag patch is new executable lines in those paths. A flag with no files this run is **
|
|
147
|
+
`tested check` applies global `thresholds` plus each flag whose paths appear in the merged coverage. Per-flag patch is new executable lines in those paths. A flag with no files this run is **skipped** (not 0%) — an affected-graph or scoped job that did not collect that package is not a fail.
|
|
148
148
|
|
|
149
|
-
A job already scoped to one package: `tested check --flag frontend` (Action `flag:`). That coverage file **is** the flag.
|
|
149
|
+
A job already scoped to one package: `tested check --flag frontend` (Action `flag:`). That coverage file **is** the flag. Other packages are omitted from this upload.
|
|
150
150
|
|
|
151
|
-
`--json` lists per-flag results (`flags.frontend.patchCheck` → `tested.dev / patch / frontend`). `
|
|
151
|
+
`--json` lists per-flag results (`flags.frontend.patchCheck` → `tested.dev / patch / frontend`). Skipped flags have `status: missing` and `skipped: true` with no `executable` / `pct`. `tested push` omits skipped flags from ingest so the last successful upload for that package still stands. `tested diff --json` includes the same map as check.
|
|
152
152
|
|
|
153
153
|
## Coverage formats
|
|
154
154
|
|
|
@@ -195,7 +195,7 @@ Or pass `--file` repeatedly / Action `files`. A CI matrix must not conclude the
|
|
|
195
195
|
```yaml
|
|
196
196
|
- uses: tested-hq/cli/action@main
|
|
197
197
|
with:
|
|
198
|
-
version: 0.1.
|
|
198
|
+
version: 0.1.10
|
|
199
199
|
push: true
|
|
200
200
|
pr-number: ${{ github.event.pull_request.number }}
|
|
201
201
|
token: ${{ secrets.TESTED_TOKEN }}
|
package/dist/td.js
CHANGED
|
@@ -1106,6 +1106,9 @@ var FlagConfigSchema = z2.object({
|
|
|
1106
1106
|
paths: z2.array(z2.string().min(1)).min(1),
|
|
1107
1107
|
thresholds: FlagThresholdsSchema.optional()
|
|
1108
1108
|
});
|
|
1109
|
+
var PathThresholdSchema = FlagThresholdsSchema.extend({
|
|
1110
|
+
glob: z2.string().min(1)
|
|
1111
|
+
});
|
|
1109
1112
|
var TestedConfigSchema = z2.object({
|
|
1110
1113
|
ignores: z2.array(z2.string()).default([]),
|
|
1111
1114
|
coverage: z2.object({
|
|
@@ -1125,11 +1128,16 @@ var TestedConfigSchema = z2.object({
|
|
|
1125
1128
|
// doesn't silently drop the field.
|
|
1126
1129
|
thresholds: z2.object({
|
|
1127
1130
|
patch: z2.number().min(0).max(100),
|
|
1128
|
-
project: z2.number().min(0).max(100)
|
|
1131
|
+
project: z2.number().min(0).max(100),
|
|
1132
|
+
/**
|
|
1133
|
+
* Per-path glob floors. Graded from this run's matched files only.
|
|
1134
|
+
* A glob with no files this run is skipped (not 0%). Not flags.
|
|
1135
|
+
*/
|
|
1136
|
+
paths: z2.array(PathThresholdSchema).optional()
|
|
1129
1137
|
}).optional(),
|
|
1130
1138
|
/**
|
|
1131
1139
|
* Per-package gates. Each flag is graded from this run's coverage files
|
|
1132
|
-
* only — a missing path is
|
|
1140
|
+
* only — a missing path is skipped (not 0%), never another flag's totals.
|
|
1133
1141
|
*/
|
|
1134
1142
|
flags: z2.record(FlagNameSchema, FlagConfigSchema).optional()
|
|
1135
1143
|
});
|
|
@@ -1164,17 +1172,21 @@ var DiffOutputSchema = z2.object({
|
|
|
1164
1172
|
ignored: z2.array(z2.string())
|
|
1165
1173
|
});
|
|
1166
1174
|
var FlagMetricJsonSchema = z2.object({
|
|
1167
|
-
|
|
1175
|
+
/** Omitted when this flag had no files this run (`skipped: true`). */
|
|
1176
|
+
pct: z2.number().min(0).max(100).optional(),
|
|
1168
1177
|
threshold: z2.number().min(0).max(100),
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1178
|
+
/** Omitted when skipped — a missing flag is not a 0% fail. */
|
|
1179
|
+
pass: z2.boolean().optional(),
|
|
1180
|
+
executable: z2.number().int().nonnegative().optional(),
|
|
1181
|
+
covered: z2.number().int().nonnegative().optional(),
|
|
1172
1182
|
skipped: z2.literal(true).optional(),
|
|
1173
1183
|
reason: z2.string().optional()
|
|
1174
1184
|
});
|
|
1175
1185
|
var FlagResultJsonSchema = z2.object({
|
|
1176
1186
|
status: z2.enum(["pass", "fail", "missing"]),
|
|
1177
1187
|
present: z2.boolean(),
|
|
1188
|
+
/** True when this flag had no coverage files this run (not a 0% result). */
|
|
1189
|
+
skipped: z2.literal(true).optional(),
|
|
1178
1190
|
reason: z2.string().optional(),
|
|
1179
1191
|
patchCheck: z2.string(),
|
|
1180
1192
|
projectCheck: z2.string(),
|
|
@@ -1182,6 +1194,16 @@ var FlagResultJsonSchema = z2.object({
|
|
|
1182
1194
|
project: FlagMetricJsonSchema
|
|
1183
1195
|
});
|
|
1184
1196
|
var FlagsJsonMapSchema = z2.record(FlagNameSchema, FlagResultJsonSchema);
|
|
1197
|
+
var PathResultJsonSchema = z2.object({
|
|
1198
|
+
glob: z2.string().min(1),
|
|
1199
|
+
status: z2.enum(["pass", "fail", "missing"]),
|
|
1200
|
+
present: z2.boolean(),
|
|
1201
|
+
skipped: z2.literal(true).optional(),
|
|
1202
|
+
reason: z2.string().optional(),
|
|
1203
|
+
patch: FlagMetricJsonSchema,
|
|
1204
|
+
project: FlagMetricJsonSchema
|
|
1205
|
+
});
|
|
1206
|
+
var PathsJsonSchema = z2.array(PathResultJsonSchema);
|
|
1185
1207
|
|
|
1186
1208
|
// src/config.ts
|
|
1187
1209
|
var DEFAULT_IGNORES = [
|
|
@@ -1696,19 +1718,28 @@ async function computeDiffContext(opts) {
|
|
|
1696
1718
|
return { diff, files, addedByFile };
|
|
1697
1719
|
}
|
|
1698
1720
|
|
|
1699
|
-
// src/core/
|
|
1721
|
+
// src/core/globs.ts
|
|
1700
1722
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1701
|
-
var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
|
|
1702
|
-
var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
|
|
1703
1723
|
var MATCH_OPTS = { dot: true, matchBase: true };
|
|
1704
|
-
function
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
);
|
|
1724
|
+
function normalizeRepoPath(filePath) {
|
|
1725
|
+
return filePath.replace(/\\/g, "/");
|
|
1726
|
+
}
|
|
1727
|
+
function pathMatchesGlob(filePath, pattern) {
|
|
1728
|
+
const normalized = normalizeRepoPath(filePath);
|
|
1729
|
+
return minimatch2(normalized, pattern, MATCH_OPTS) || minimatch2(normalized, `**/${pattern}`, MATCH_OPTS);
|
|
1730
|
+
}
|
|
1731
|
+
function pathMatchesAnyGlob(filePath, patterns) {
|
|
1732
|
+
return patterns.some((p) => pathMatchesGlob(filePath, p));
|
|
1709
1733
|
}
|
|
1734
|
+
function filterFilesByGlobs(files, patterns) {
|
|
1735
|
+
return files.filter((f) => pathMatchesAnyGlob(f.path, patterns));
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/core/flags.ts
|
|
1739
|
+
var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
|
|
1740
|
+
var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
|
|
1710
1741
|
function filterFilesByFlag(files, patterns) {
|
|
1711
|
-
return files
|
|
1742
|
+
return filterFilesByGlobs(files, patterns);
|
|
1712
1743
|
}
|
|
1713
1744
|
function resolveFlagThresholds(flag, global) {
|
|
1714
1745
|
return {
|
|
@@ -1742,13 +1773,11 @@ function totalsToMetric(totals, threshold, kind) {
|
|
|
1742
1773
|
covered: totals.covered
|
|
1743
1774
|
};
|
|
1744
1775
|
}
|
|
1745
|
-
function missingMetric(threshold) {
|
|
1776
|
+
function missingMetric(threshold, reason) {
|
|
1746
1777
|
return {
|
|
1747
|
-
pct: 0,
|
|
1748
1778
|
threshold,
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
covered: 0
|
|
1779
|
+
skipped: true,
|
|
1780
|
+
reason
|
|
1752
1781
|
};
|
|
1753
1782
|
}
|
|
1754
1783
|
function checkSlug(kind, name) {
|
|
@@ -1778,8 +1807,8 @@ function missingFlag(name, thresholds, reason) {
|
|
|
1778
1807
|
reason,
|
|
1779
1808
|
patchCheck: checkSlug("patch", name),
|
|
1780
1809
|
projectCheck: checkSlug("project", name),
|
|
1781
|
-
patch: missingMetric(thresholds.patch),
|
|
1782
|
-
project: missingMetric(thresholds.project)
|
|
1810
|
+
patch: missingMetric(thresholds.patch, reason),
|
|
1811
|
+
project: missingMetric(thresholds.project, reason)
|
|
1783
1812
|
};
|
|
1784
1813
|
}
|
|
1785
1814
|
function evaluateFlags(input) {
|
|
@@ -1810,7 +1839,7 @@ function evaluateFlags(input) {
|
|
|
1810
1839
|
return results;
|
|
1811
1840
|
}
|
|
1812
1841
|
function flagsPass(results) {
|
|
1813
|
-
return results.every((f) => f.status
|
|
1842
|
+
return results.every((f) => f.status !== "fail");
|
|
1814
1843
|
}
|
|
1815
1844
|
function flagsToJson(results) {
|
|
1816
1845
|
const out = {};
|
|
@@ -1818,19 +1847,44 @@ function flagsToJson(results) {
|
|
|
1818
1847
|
out[flag.name] = {
|
|
1819
1848
|
status: flag.status,
|
|
1820
1849
|
present: flag.present,
|
|
1850
|
+
...flag.status === "missing" ? { skipped: true } : {},
|
|
1821
1851
|
...flag.reason ? { reason: flag.reason } : {},
|
|
1822
1852
|
patchCheck: flag.patchCheck,
|
|
1823
1853
|
projectCheck: flag.projectCheck,
|
|
1824
|
-
patch: flag.patch,
|
|
1825
|
-
project: flag.project
|
|
1854
|
+
patch: metricToJson(flag.patch),
|
|
1855
|
+
project: metricToJson(flag.project)
|
|
1826
1856
|
};
|
|
1827
1857
|
}
|
|
1828
1858
|
return out;
|
|
1829
1859
|
}
|
|
1860
|
+
function metricToJson(metric) {
|
|
1861
|
+
if (metric.skipped) {
|
|
1862
|
+
return {
|
|
1863
|
+
threshold: metric.threshold,
|
|
1864
|
+
skipped: true,
|
|
1865
|
+
...metric.reason ? { reason: metric.reason } : {}
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
return {
|
|
1869
|
+
pct: metric.pct ?? 0,
|
|
1870
|
+
threshold: metric.threshold,
|
|
1871
|
+
pass: metric.pass ?? false,
|
|
1872
|
+
executable: metric.executable ?? 0,
|
|
1873
|
+
covered: metric.covered ?? 0,
|
|
1874
|
+
...metric.reason ? { reason: metric.reason } : {}
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
function flagsToIngestJson(results) {
|
|
1878
|
+
const present = results.filter((f) => f.present);
|
|
1879
|
+
return present.length > 0 ? flagsToJson(present) : void 0;
|
|
1880
|
+
}
|
|
1830
1881
|
function resolveFlagsJson(input) {
|
|
1831
1882
|
const results = evaluateFlags(input);
|
|
1832
1883
|
return results.length > 0 ? flagsToJson(results) : void 0;
|
|
1833
1884
|
}
|
|
1885
|
+
function resolveIngestFlagsJson(input) {
|
|
1886
|
+
return flagsToIngestJson(evaluateFlags(input));
|
|
1887
|
+
}
|
|
1834
1888
|
|
|
1835
1889
|
// src/core/coverage-merge.ts
|
|
1836
1890
|
function emptyToUndef(value) {
|
|
@@ -2553,7 +2607,7 @@ async function executePush(cli, deps) {
|
|
|
2553
2607
|
let flags;
|
|
2554
2608
|
if (!handshakeOnly && !computeDiffFn) {
|
|
2555
2609
|
try {
|
|
2556
|
-
flags =
|
|
2610
|
+
flags = resolveIngestFlagsJson({
|
|
2557
2611
|
config,
|
|
2558
2612
|
files,
|
|
2559
2613
|
addedByFile,
|
|
@@ -3072,7 +3126,7 @@ import pc3 from "picocolors";
|
|
|
3072
3126
|
// package.json
|
|
3073
3127
|
var package_default = {
|
|
3074
3128
|
name: "@tested/cli",
|
|
3075
|
-
version: "0.1.
|
|
3129
|
+
version: "0.1.10",
|
|
3076
3130
|
description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
|
|
3077
3131
|
license: "MIT",
|
|
3078
3132
|
homepage: "https://tested.dev",
|
|
@@ -3482,6 +3536,116 @@ function registerRunCommand(program2) {
|
|
|
3482
3536
|
// src/commands/diff.ts
|
|
3483
3537
|
import "commander";
|
|
3484
3538
|
|
|
3539
|
+
// src/core/path-thresholds.ts
|
|
3540
|
+
var MISSING_PATH_REASON = "no coverage files matched this path glob in this run";
|
|
3541
|
+
function resolvePathThresholds(entry, global) {
|
|
3542
|
+
return {
|
|
3543
|
+
patch: entry.patch ?? global.patch,
|
|
3544
|
+
project: entry.project ?? global.project
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
function totalsToMetric2(totals, threshold, kind) {
|
|
3548
|
+
const empty = isEmptyPatch(totals);
|
|
3549
|
+
if (kind === "patch" && empty) {
|
|
3550
|
+
return {
|
|
3551
|
+
pct: totals.pct,
|
|
3552
|
+
threshold,
|
|
3553
|
+
pass: true,
|
|
3554
|
+
executable: totals.executable,
|
|
3555
|
+
covered: totals.covered,
|
|
3556
|
+
skipped: true,
|
|
3557
|
+
reason: EMPTY_PATCH_REASON
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
const pass = totals.pct >= threshold;
|
|
3561
|
+
return {
|
|
3562
|
+
pct: totals.pct,
|
|
3563
|
+
threshold,
|
|
3564
|
+
pass,
|
|
3565
|
+
executable: totals.executable,
|
|
3566
|
+
covered: totals.covered
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
function missingMetric2(threshold, reason) {
|
|
3570
|
+
return {
|
|
3571
|
+
threshold,
|
|
3572
|
+
skipped: true,
|
|
3573
|
+
reason
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
function evaluatePresentPath(glob, files, addedByFile, thresholds) {
|
|
3577
|
+
const patch = computePatchCoverage(files, addedByFile);
|
|
3578
|
+
const project = computeProjectCoverage(files);
|
|
3579
|
+
const patchMetric = totalsToMetric2(patch.totals, thresholds.patch, "patch");
|
|
3580
|
+
const projectMetric = totalsToMetric2(project.totals, thresholds.project, "project");
|
|
3581
|
+
const status = patchMetric.pass && projectMetric.pass ? "pass" : "fail";
|
|
3582
|
+
return {
|
|
3583
|
+
glob,
|
|
3584
|
+
present: true,
|
|
3585
|
+
status,
|
|
3586
|
+
patch: patchMetric,
|
|
3587
|
+
project: projectMetric
|
|
3588
|
+
};
|
|
3589
|
+
}
|
|
3590
|
+
function missingPath(glob, thresholds, reason) {
|
|
3591
|
+
return {
|
|
3592
|
+
glob,
|
|
3593
|
+
present: false,
|
|
3594
|
+
status: "missing",
|
|
3595
|
+
reason,
|
|
3596
|
+
patch: missingMetric2(thresholds.patch, reason),
|
|
3597
|
+
project: missingMetric2(thresholds.project, reason)
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
function evaluatePathThresholds(input) {
|
|
3601
|
+
const global = input.config.thresholds;
|
|
3602
|
+
const entries = global?.paths;
|
|
3603
|
+
if (!global || !entries || entries.length === 0) return [];
|
|
3604
|
+
return entries.map((entry) => {
|
|
3605
|
+
const thresholds = resolvePathThresholds(entry, global);
|
|
3606
|
+
const matched = filterFilesByGlobs(input.files, [entry.glob]);
|
|
3607
|
+
if (matched.length === 0) {
|
|
3608
|
+
return missingPath(entry.glob, thresholds, MISSING_PATH_REASON);
|
|
3609
|
+
}
|
|
3610
|
+
return evaluatePresentPath(entry.glob, matched, input.addedByFile, thresholds);
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
function pathThresholdsPass(results) {
|
|
3614
|
+
return results.every((p) => p.status !== "fail");
|
|
3615
|
+
}
|
|
3616
|
+
function pathThresholdsToJson(results) {
|
|
3617
|
+
return results.map((path) => ({
|
|
3618
|
+
glob: path.glob,
|
|
3619
|
+
status: path.status,
|
|
3620
|
+
present: path.present,
|
|
3621
|
+
...path.status === "missing" ? { skipped: true } : {},
|
|
3622
|
+
...path.reason ? { reason: path.reason } : {},
|
|
3623
|
+
patch: metricToJson2(path.patch),
|
|
3624
|
+
project: metricToJson2(path.project)
|
|
3625
|
+
}));
|
|
3626
|
+
}
|
|
3627
|
+
function metricToJson2(metric) {
|
|
3628
|
+
if (metric.skipped) {
|
|
3629
|
+
return {
|
|
3630
|
+
threshold: metric.threshold,
|
|
3631
|
+
skipped: true,
|
|
3632
|
+
...metric.reason ? { reason: metric.reason } : {}
|
|
3633
|
+
};
|
|
3634
|
+
}
|
|
3635
|
+
return {
|
|
3636
|
+
pct: metric.pct ?? 0,
|
|
3637
|
+
threshold: metric.threshold,
|
|
3638
|
+
pass: metric.pass ?? false,
|
|
3639
|
+
executable: metric.executable ?? 0,
|
|
3640
|
+
covered: metric.covered ?? 0,
|
|
3641
|
+
...metric.reason ? { reason: metric.reason } : {}
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
function resolvePathThresholdsJson(input) {
|
|
3645
|
+
const results = evaluatePathThresholds(input);
|
|
3646
|
+
return results.length > 0 ? pathThresholdsToJson(results) : void 0;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3485
3649
|
// src/output/human.ts
|
|
3486
3650
|
function formatRange(r) {
|
|
3487
3651
|
return r.start === r.end ? `${r.start}` : `${r.start}-${r.end}`;
|
|
@@ -3629,7 +3793,12 @@ function registerDiffCommand(program2) {
|
|
|
3629
3793
|
});
|
|
3630
3794
|
if (opts.json) {
|
|
3631
3795
|
const flags = resolveFlagsJson({ config, files, addedByFile });
|
|
3632
|
-
const
|
|
3796
|
+
const paths = resolvePathThresholdsJson({ config, files, addedByFile });
|
|
3797
|
+
const payload = {
|
|
3798
|
+
...diff,
|
|
3799
|
+
...flags ? { flags } : {},
|
|
3800
|
+
...paths ? { paths } : {}
|
|
3801
|
+
};
|
|
3633
3802
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
3634
3803
|
} else {
|
|
3635
3804
|
process.stdout.write(
|
|
@@ -3659,6 +3828,7 @@ function formatIncompleteCheck(state, json) {
|
|
|
3659
3828
|
projectPass: true,
|
|
3660
3829
|
overall: "pass",
|
|
3661
3830
|
flagResults: [],
|
|
3831
|
+
pathResults: [],
|
|
3662
3832
|
stdout: JSON.stringify({
|
|
3663
3833
|
overall: "pending",
|
|
3664
3834
|
complete: false,
|
|
@@ -3676,6 +3846,7 @@ function formatIncompleteCheck(state, json) {
|
|
|
3676
3846
|
projectPass: true,
|
|
3677
3847
|
overall: "pass",
|
|
3678
3848
|
flagResults: [],
|
|
3849
|
+
pathResults: [],
|
|
3679
3850
|
stdout: "",
|
|
3680
3851
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3681
3852
|
|
|
@@ -3694,6 +3865,7 @@ function formatCompleteHandshakeCheck(json) {
|
|
|
3694
3865
|
projectPass: true,
|
|
3695
3866
|
overall: "pass",
|
|
3696
3867
|
flagResults: [],
|
|
3868
|
+
pathResults: [],
|
|
3697
3869
|
stdout: JSON.stringify({ overall: "pending", complete: true, note }) + "\n",
|
|
3698
3870
|
stderr: "",
|
|
3699
3871
|
exitCode: 0
|
|
@@ -3705,6 +3877,7 @@ function formatCompleteHandshakeCheck(json) {
|
|
|
3705
3877
|
projectPass: true,
|
|
3706
3878
|
overall: "pass",
|
|
3707
3879
|
flagResults: [],
|
|
3880
|
+
pathResults: [],
|
|
3708
3881
|
stdout: "",
|
|
3709
3882
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3710
3883
|
|
|
@@ -3718,6 +3891,12 @@ function formatMetricLine(label, pct3, threshold, pass, indent = " ") {
|
|
|
3718
3891
|
const status = pass ? badge("pass") : badge("fail");
|
|
3719
3892
|
return `${indent}${label.padEnd(8)} ${pctStr}% (threshold ${threshold}) ${status}`;
|
|
3720
3893
|
}
|
|
3894
|
+
function presentPct(metric) {
|
|
3895
|
+
if (metric.pct === void 0 || metric.pass === void 0) {
|
|
3896
|
+
throw new Error("present metric missing pct/pass");
|
|
3897
|
+
}
|
|
3898
|
+
return { pct: metric.pct, pass: metric.pass };
|
|
3899
|
+
}
|
|
3721
3900
|
function formatFlagLines(results) {
|
|
3722
3901
|
if (results.length === 0) return [];
|
|
3723
3902
|
const lines = [""];
|
|
@@ -3734,16 +3913,52 @@ function formatFlagLines(results) {
|
|
|
3734
3913
|
` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
|
|
3735
3914
|
);
|
|
3736
3915
|
} else {
|
|
3916
|
+
const patch = presentPct(flag.patch);
|
|
3737
3917
|
lines.push(
|
|
3738
|
-
formatMetricLine("Patch",
|
|
3918
|
+
formatMetricLine("Patch", patch.pct, flag.patch.threshold, patch.pass, " ")
|
|
3739
3919
|
);
|
|
3740
3920
|
}
|
|
3921
|
+
const project = presentPct(flag.project);
|
|
3741
3922
|
lines.push(
|
|
3742
3923
|
formatMetricLine(
|
|
3743
3924
|
"Project",
|
|
3744
|
-
|
|
3925
|
+
project.pct,
|
|
3745
3926
|
flag.project.threshold,
|
|
3746
|
-
|
|
3927
|
+
project.pass,
|
|
3928
|
+
" "
|
|
3929
|
+
)
|
|
3930
|
+
);
|
|
3931
|
+
}
|
|
3932
|
+
return lines;
|
|
3933
|
+
}
|
|
3934
|
+
function formatPathLines(results) {
|
|
3935
|
+
if (results.length === 0) return [];
|
|
3936
|
+
const lines = [""];
|
|
3937
|
+
for (const path of results) {
|
|
3938
|
+
if (path.status === "missing") {
|
|
3939
|
+
lines.push(
|
|
3940
|
+
` ${path.glob} ${dim(path.reason ?? "missing this run")} ${badge("missing")}`
|
|
3941
|
+
);
|
|
3942
|
+
continue;
|
|
3943
|
+
}
|
|
3944
|
+
lines.push(` ${path.glob} ${path.status === "pass" ? badge("pass") : badge("fail")}`);
|
|
3945
|
+
if (path.patch.skipped) {
|
|
3946
|
+
lines.push(
|
|
3947
|
+
` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
|
|
3948
|
+
);
|
|
3949
|
+
} else {
|
|
3950
|
+
const patch = presentPct(path.patch);
|
|
3951
|
+
lines.push(
|
|
3952
|
+
formatMetricLine("Patch", patch.pct, path.patch.threshold, patch.pass, " ")
|
|
3953
|
+
);
|
|
3954
|
+
}
|
|
3955
|
+
const project = presentPct(path.project);
|
|
3956
|
+
lines.push(
|
|
3957
|
+
formatMetricLine(
|
|
3958
|
+
"Project",
|
|
3959
|
+
project.pct,
|
|
3960
|
+
path.project.threshold,
|
|
3961
|
+
project.pass,
|
|
3747
3962
|
" "
|
|
3748
3963
|
)
|
|
3749
3964
|
);
|
|
@@ -3759,6 +3974,7 @@ function runCheck(input) {
|
|
|
3759
3974
|
projectPass: true,
|
|
3760
3975
|
overall: "pass",
|
|
3761
3976
|
flagResults: [],
|
|
3977
|
+
pathResults: [],
|
|
3762
3978
|
stdout: "",
|
|
3763
3979
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3764
3980
|
|
|
@@ -3781,8 +3997,14 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3781
3997
|
addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map(),
|
|
3782
3998
|
...input.onlyFlag !== void 0 ? { onlyFlag: input.onlyFlag } : {}
|
|
3783
3999
|
});
|
|
4000
|
+
const pathResults = evaluatePathThresholds({
|
|
4001
|
+
config,
|
|
4002
|
+
files: input.files ?? [],
|
|
4003
|
+
addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map()
|
|
4004
|
+
});
|
|
3784
4005
|
const flagsOk = flagsPass(flagResults);
|
|
3785
|
-
const
|
|
4006
|
+
const pathsOk = pathThresholdsPass(pathResults);
|
|
4007
|
+
const overall = patchPass && projectPass && flagsOk && pathsOk ? "pass" : "fail";
|
|
3786
4008
|
const exitCode = overall === "pass" ? 0 : 1;
|
|
3787
4009
|
if (json) {
|
|
3788
4010
|
const payload = {
|
|
@@ -3794,6 +4016,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3794
4016
|
},
|
|
3795
4017
|
project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
|
|
3796
4018
|
...flagResults.length > 0 ? { flags: flagsToJson(flagResults) } : {},
|
|
4019
|
+
...pathResults.length > 0 ? { paths: pathThresholdsToJson(pathResults) } : {},
|
|
3797
4020
|
overall,
|
|
3798
4021
|
...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
|
|
3799
4022
|
};
|
|
@@ -3803,6 +4026,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3803
4026
|
projectPass,
|
|
3804
4027
|
overall,
|
|
3805
4028
|
flagResults,
|
|
4029
|
+
pathResults,
|
|
3806
4030
|
stdout: JSON.stringify(payload) + "\n",
|
|
3807
4031
|
stderr: "",
|
|
3808
4032
|
exitCode
|
|
@@ -3822,6 +4046,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3822
4046
|
}
|
|
3823
4047
|
lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
|
|
3824
4048
|
lines.push(...formatFlagLines(flagResults));
|
|
4049
|
+
lines.push(...formatPathLines(pathResults));
|
|
3825
4050
|
if (overall === "fail") {
|
|
3826
4051
|
lines.push("");
|
|
3827
4052
|
if (patchSkipped) {
|
|
@@ -3831,10 +4056,16 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3831
4056
|
if (missing.length > 0) {
|
|
3832
4057
|
lines.push(
|
|
3833
4058
|
dim(
|
|
3834
|
-
"
|
|
4059
|
+
"Flags with no files this run are skipped (not 0%). Scope the job with --flag when this coverage file is one package."
|
|
3835
4060
|
)
|
|
3836
4061
|
);
|
|
3837
4062
|
}
|
|
4063
|
+
const missingPaths = pathResults.filter((p) => p.status === "missing");
|
|
4064
|
+
if (missingPaths.length > 0) {
|
|
4065
|
+
lines.push(
|
|
4066
|
+
dim("Path globs with no files this run are skipped (not 0%).")
|
|
4067
|
+
);
|
|
4068
|
+
}
|
|
3838
4069
|
lines.push(tip("add tests for uncovered ranges: tested diff"));
|
|
3839
4070
|
} else {
|
|
3840
4071
|
lines.push("");
|
|
@@ -3851,6 +4082,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3851
4082
|
projectPass,
|
|
3852
4083
|
overall,
|
|
3853
4084
|
flagResults,
|
|
4085
|
+
pathResults,
|
|
3854
4086
|
stdout: lines.join("\n"),
|
|
3855
4087
|
stderr: "",
|
|
3856
4088
|
exitCode
|
|
@@ -3858,7 +4090,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3858
4090
|
}
|
|
3859
4091
|
function registerCheckCommand(program2) {
|
|
3860
4092
|
program2.command("check").description(
|
|
3861
|
-
"Exit non-zero if patch or
|
|
4093
|
+
"Exit non-zero if patch, project, or path coverage falls below configured thresholds."
|
|
3862
4094
|
).option("--json", "Emit machine-readable JSON to stdout (exit code unchanged).", false).option("--base <ref>", "Git base ref to diff against", void 0).option(
|
|
3863
4095
|
"--file <path>",
|
|
3864
4096
|
"Coverage file to merge (repeatable). Overrides coverage.path.",
|
package/dist/tested.js
CHANGED
|
@@ -1106,6 +1106,9 @@ var FlagConfigSchema = z2.object({
|
|
|
1106
1106
|
paths: z2.array(z2.string().min(1)).min(1),
|
|
1107
1107
|
thresholds: FlagThresholdsSchema.optional()
|
|
1108
1108
|
});
|
|
1109
|
+
var PathThresholdSchema = FlagThresholdsSchema.extend({
|
|
1110
|
+
glob: z2.string().min(1)
|
|
1111
|
+
});
|
|
1109
1112
|
var TestedConfigSchema = z2.object({
|
|
1110
1113
|
ignores: z2.array(z2.string()).default([]),
|
|
1111
1114
|
coverage: z2.object({
|
|
@@ -1125,11 +1128,16 @@ var TestedConfigSchema = z2.object({
|
|
|
1125
1128
|
// doesn't silently drop the field.
|
|
1126
1129
|
thresholds: z2.object({
|
|
1127
1130
|
patch: z2.number().min(0).max(100),
|
|
1128
|
-
project: z2.number().min(0).max(100)
|
|
1131
|
+
project: z2.number().min(0).max(100),
|
|
1132
|
+
/**
|
|
1133
|
+
* Per-path glob floors. Graded from this run's matched files only.
|
|
1134
|
+
* A glob with no files this run is skipped (not 0%). Not flags.
|
|
1135
|
+
*/
|
|
1136
|
+
paths: z2.array(PathThresholdSchema).optional()
|
|
1129
1137
|
}).optional(),
|
|
1130
1138
|
/**
|
|
1131
1139
|
* Per-package gates. Each flag is graded from this run's coverage files
|
|
1132
|
-
* only — a missing path is
|
|
1140
|
+
* only — a missing path is skipped (not 0%), never another flag's totals.
|
|
1133
1141
|
*/
|
|
1134
1142
|
flags: z2.record(FlagNameSchema, FlagConfigSchema).optional()
|
|
1135
1143
|
});
|
|
@@ -1164,17 +1172,21 @@ var DiffOutputSchema = z2.object({
|
|
|
1164
1172
|
ignored: z2.array(z2.string())
|
|
1165
1173
|
});
|
|
1166
1174
|
var FlagMetricJsonSchema = z2.object({
|
|
1167
|
-
|
|
1175
|
+
/** Omitted when this flag had no files this run (`skipped: true`). */
|
|
1176
|
+
pct: z2.number().min(0).max(100).optional(),
|
|
1168
1177
|
threshold: z2.number().min(0).max(100),
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1178
|
+
/** Omitted when skipped — a missing flag is not a 0% fail. */
|
|
1179
|
+
pass: z2.boolean().optional(),
|
|
1180
|
+
executable: z2.number().int().nonnegative().optional(),
|
|
1181
|
+
covered: z2.number().int().nonnegative().optional(),
|
|
1172
1182
|
skipped: z2.literal(true).optional(),
|
|
1173
1183
|
reason: z2.string().optional()
|
|
1174
1184
|
});
|
|
1175
1185
|
var FlagResultJsonSchema = z2.object({
|
|
1176
1186
|
status: z2.enum(["pass", "fail", "missing"]),
|
|
1177
1187
|
present: z2.boolean(),
|
|
1188
|
+
/** True when this flag had no coverage files this run (not a 0% result). */
|
|
1189
|
+
skipped: z2.literal(true).optional(),
|
|
1178
1190
|
reason: z2.string().optional(),
|
|
1179
1191
|
patchCheck: z2.string(),
|
|
1180
1192
|
projectCheck: z2.string(),
|
|
@@ -1182,6 +1194,16 @@ var FlagResultJsonSchema = z2.object({
|
|
|
1182
1194
|
project: FlagMetricJsonSchema
|
|
1183
1195
|
});
|
|
1184
1196
|
var FlagsJsonMapSchema = z2.record(FlagNameSchema, FlagResultJsonSchema);
|
|
1197
|
+
var PathResultJsonSchema = z2.object({
|
|
1198
|
+
glob: z2.string().min(1),
|
|
1199
|
+
status: z2.enum(["pass", "fail", "missing"]),
|
|
1200
|
+
present: z2.boolean(),
|
|
1201
|
+
skipped: z2.literal(true).optional(),
|
|
1202
|
+
reason: z2.string().optional(),
|
|
1203
|
+
patch: FlagMetricJsonSchema,
|
|
1204
|
+
project: FlagMetricJsonSchema
|
|
1205
|
+
});
|
|
1206
|
+
var PathsJsonSchema = z2.array(PathResultJsonSchema);
|
|
1185
1207
|
|
|
1186
1208
|
// src/config.ts
|
|
1187
1209
|
var DEFAULT_IGNORES = [
|
|
@@ -1696,19 +1718,28 @@ async function computeDiffContext(opts) {
|
|
|
1696
1718
|
return { diff, files, addedByFile };
|
|
1697
1719
|
}
|
|
1698
1720
|
|
|
1699
|
-
// src/core/
|
|
1721
|
+
// src/core/globs.ts
|
|
1700
1722
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1701
|
-
var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
|
|
1702
|
-
var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
|
|
1703
1723
|
var MATCH_OPTS = { dot: true, matchBase: true };
|
|
1704
|
-
function
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
);
|
|
1724
|
+
function normalizeRepoPath(filePath) {
|
|
1725
|
+
return filePath.replace(/\\/g, "/");
|
|
1726
|
+
}
|
|
1727
|
+
function pathMatchesGlob(filePath, pattern) {
|
|
1728
|
+
const normalized = normalizeRepoPath(filePath);
|
|
1729
|
+
return minimatch2(normalized, pattern, MATCH_OPTS) || minimatch2(normalized, `**/${pattern}`, MATCH_OPTS);
|
|
1730
|
+
}
|
|
1731
|
+
function pathMatchesAnyGlob(filePath, patterns) {
|
|
1732
|
+
return patterns.some((p) => pathMatchesGlob(filePath, p));
|
|
1709
1733
|
}
|
|
1734
|
+
function filterFilesByGlobs(files, patterns) {
|
|
1735
|
+
return files.filter((f) => pathMatchesAnyGlob(f.path, patterns));
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/core/flags.ts
|
|
1739
|
+
var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
|
|
1740
|
+
var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
|
|
1710
1741
|
function filterFilesByFlag(files, patterns) {
|
|
1711
|
-
return files
|
|
1742
|
+
return filterFilesByGlobs(files, patterns);
|
|
1712
1743
|
}
|
|
1713
1744
|
function resolveFlagThresholds(flag, global) {
|
|
1714
1745
|
return {
|
|
@@ -1742,13 +1773,11 @@ function totalsToMetric(totals, threshold, kind) {
|
|
|
1742
1773
|
covered: totals.covered
|
|
1743
1774
|
};
|
|
1744
1775
|
}
|
|
1745
|
-
function missingMetric(threshold) {
|
|
1776
|
+
function missingMetric(threshold, reason) {
|
|
1746
1777
|
return {
|
|
1747
|
-
pct: 0,
|
|
1748
1778
|
threshold,
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
covered: 0
|
|
1779
|
+
skipped: true,
|
|
1780
|
+
reason
|
|
1752
1781
|
};
|
|
1753
1782
|
}
|
|
1754
1783
|
function checkSlug(kind, name) {
|
|
@@ -1778,8 +1807,8 @@ function missingFlag(name, thresholds, reason) {
|
|
|
1778
1807
|
reason,
|
|
1779
1808
|
patchCheck: checkSlug("patch", name),
|
|
1780
1809
|
projectCheck: checkSlug("project", name),
|
|
1781
|
-
patch: missingMetric(thresholds.patch),
|
|
1782
|
-
project: missingMetric(thresholds.project)
|
|
1810
|
+
patch: missingMetric(thresholds.patch, reason),
|
|
1811
|
+
project: missingMetric(thresholds.project, reason)
|
|
1783
1812
|
};
|
|
1784
1813
|
}
|
|
1785
1814
|
function evaluateFlags(input) {
|
|
@@ -1810,7 +1839,7 @@ function evaluateFlags(input) {
|
|
|
1810
1839
|
return results;
|
|
1811
1840
|
}
|
|
1812
1841
|
function flagsPass(results) {
|
|
1813
|
-
return results.every((f) => f.status
|
|
1842
|
+
return results.every((f) => f.status !== "fail");
|
|
1814
1843
|
}
|
|
1815
1844
|
function flagsToJson(results) {
|
|
1816
1845
|
const out = {};
|
|
@@ -1818,19 +1847,44 @@ function flagsToJson(results) {
|
|
|
1818
1847
|
out[flag.name] = {
|
|
1819
1848
|
status: flag.status,
|
|
1820
1849
|
present: flag.present,
|
|
1850
|
+
...flag.status === "missing" ? { skipped: true } : {},
|
|
1821
1851
|
...flag.reason ? { reason: flag.reason } : {},
|
|
1822
1852
|
patchCheck: flag.patchCheck,
|
|
1823
1853
|
projectCheck: flag.projectCheck,
|
|
1824
|
-
patch: flag.patch,
|
|
1825
|
-
project: flag.project
|
|
1854
|
+
patch: metricToJson(flag.patch),
|
|
1855
|
+
project: metricToJson(flag.project)
|
|
1826
1856
|
};
|
|
1827
1857
|
}
|
|
1828
1858
|
return out;
|
|
1829
1859
|
}
|
|
1860
|
+
function metricToJson(metric) {
|
|
1861
|
+
if (metric.skipped) {
|
|
1862
|
+
return {
|
|
1863
|
+
threshold: metric.threshold,
|
|
1864
|
+
skipped: true,
|
|
1865
|
+
...metric.reason ? { reason: metric.reason } : {}
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
return {
|
|
1869
|
+
pct: metric.pct ?? 0,
|
|
1870
|
+
threshold: metric.threshold,
|
|
1871
|
+
pass: metric.pass ?? false,
|
|
1872
|
+
executable: metric.executable ?? 0,
|
|
1873
|
+
covered: metric.covered ?? 0,
|
|
1874
|
+
...metric.reason ? { reason: metric.reason } : {}
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
function flagsToIngestJson(results) {
|
|
1878
|
+
const present = results.filter((f) => f.present);
|
|
1879
|
+
return present.length > 0 ? flagsToJson(present) : void 0;
|
|
1880
|
+
}
|
|
1830
1881
|
function resolveFlagsJson(input) {
|
|
1831
1882
|
const results = evaluateFlags(input);
|
|
1832
1883
|
return results.length > 0 ? flagsToJson(results) : void 0;
|
|
1833
1884
|
}
|
|
1885
|
+
function resolveIngestFlagsJson(input) {
|
|
1886
|
+
return flagsToIngestJson(evaluateFlags(input));
|
|
1887
|
+
}
|
|
1834
1888
|
|
|
1835
1889
|
// src/core/coverage-merge.ts
|
|
1836
1890
|
function emptyToUndef(value) {
|
|
@@ -2553,7 +2607,7 @@ async function executePush(cli, deps) {
|
|
|
2553
2607
|
let flags;
|
|
2554
2608
|
if (!handshakeOnly && !computeDiffFn) {
|
|
2555
2609
|
try {
|
|
2556
|
-
flags =
|
|
2610
|
+
flags = resolveIngestFlagsJson({
|
|
2557
2611
|
config,
|
|
2558
2612
|
files,
|
|
2559
2613
|
addedByFile,
|
|
@@ -3072,7 +3126,7 @@ import pc3 from "picocolors";
|
|
|
3072
3126
|
// package.json
|
|
3073
3127
|
var package_default = {
|
|
3074
3128
|
name: "@tested/cli",
|
|
3075
|
-
version: "0.1.
|
|
3129
|
+
version: "0.1.10",
|
|
3076
3130
|
description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
|
|
3077
3131
|
license: "MIT",
|
|
3078
3132
|
homepage: "https://tested.dev",
|
|
@@ -3482,6 +3536,116 @@ function registerRunCommand(program2) {
|
|
|
3482
3536
|
// src/commands/diff.ts
|
|
3483
3537
|
import "commander";
|
|
3484
3538
|
|
|
3539
|
+
// src/core/path-thresholds.ts
|
|
3540
|
+
var MISSING_PATH_REASON = "no coverage files matched this path glob in this run";
|
|
3541
|
+
function resolvePathThresholds(entry, global) {
|
|
3542
|
+
return {
|
|
3543
|
+
patch: entry.patch ?? global.patch,
|
|
3544
|
+
project: entry.project ?? global.project
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
function totalsToMetric2(totals, threshold, kind) {
|
|
3548
|
+
const empty = isEmptyPatch(totals);
|
|
3549
|
+
if (kind === "patch" && empty) {
|
|
3550
|
+
return {
|
|
3551
|
+
pct: totals.pct,
|
|
3552
|
+
threshold,
|
|
3553
|
+
pass: true,
|
|
3554
|
+
executable: totals.executable,
|
|
3555
|
+
covered: totals.covered,
|
|
3556
|
+
skipped: true,
|
|
3557
|
+
reason: EMPTY_PATCH_REASON
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
const pass = totals.pct >= threshold;
|
|
3561
|
+
return {
|
|
3562
|
+
pct: totals.pct,
|
|
3563
|
+
threshold,
|
|
3564
|
+
pass,
|
|
3565
|
+
executable: totals.executable,
|
|
3566
|
+
covered: totals.covered
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
function missingMetric2(threshold, reason) {
|
|
3570
|
+
return {
|
|
3571
|
+
threshold,
|
|
3572
|
+
skipped: true,
|
|
3573
|
+
reason
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
function evaluatePresentPath(glob, files, addedByFile, thresholds) {
|
|
3577
|
+
const patch = computePatchCoverage(files, addedByFile);
|
|
3578
|
+
const project = computeProjectCoverage(files);
|
|
3579
|
+
const patchMetric = totalsToMetric2(patch.totals, thresholds.patch, "patch");
|
|
3580
|
+
const projectMetric = totalsToMetric2(project.totals, thresholds.project, "project");
|
|
3581
|
+
const status = patchMetric.pass && projectMetric.pass ? "pass" : "fail";
|
|
3582
|
+
return {
|
|
3583
|
+
glob,
|
|
3584
|
+
present: true,
|
|
3585
|
+
status,
|
|
3586
|
+
patch: patchMetric,
|
|
3587
|
+
project: projectMetric
|
|
3588
|
+
};
|
|
3589
|
+
}
|
|
3590
|
+
function missingPath(glob, thresholds, reason) {
|
|
3591
|
+
return {
|
|
3592
|
+
glob,
|
|
3593
|
+
present: false,
|
|
3594
|
+
status: "missing",
|
|
3595
|
+
reason,
|
|
3596
|
+
patch: missingMetric2(thresholds.patch, reason),
|
|
3597
|
+
project: missingMetric2(thresholds.project, reason)
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
function evaluatePathThresholds(input) {
|
|
3601
|
+
const global = input.config.thresholds;
|
|
3602
|
+
const entries = global?.paths;
|
|
3603
|
+
if (!global || !entries || entries.length === 0) return [];
|
|
3604
|
+
return entries.map((entry) => {
|
|
3605
|
+
const thresholds = resolvePathThresholds(entry, global);
|
|
3606
|
+
const matched = filterFilesByGlobs(input.files, [entry.glob]);
|
|
3607
|
+
if (matched.length === 0) {
|
|
3608
|
+
return missingPath(entry.glob, thresholds, MISSING_PATH_REASON);
|
|
3609
|
+
}
|
|
3610
|
+
return evaluatePresentPath(entry.glob, matched, input.addedByFile, thresholds);
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
function pathThresholdsPass(results) {
|
|
3614
|
+
return results.every((p) => p.status !== "fail");
|
|
3615
|
+
}
|
|
3616
|
+
function pathThresholdsToJson(results) {
|
|
3617
|
+
return results.map((path) => ({
|
|
3618
|
+
glob: path.glob,
|
|
3619
|
+
status: path.status,
|
|
3620
|
+
present: path.present,
|
|
3621
|
+
...path.status === "missing" ? { skipped: true } : {},
|
|
3622
|
+
...path.reason ? { reason: path.reason } : {},
|
|
3623
|
+
patch: metricToJson2(path.patch),
|
|
3624
|
+
project: metricToJson2(path.project)
|
|
3625
|
+
}));
|
|
3626
|
+
}
|
|
3627
|
+
function metricToJson2(metric) {
|
|
3628
|
+
if (metric.skipped) {
|
|
3629
|
+
return {
|
|
3630
|
+
threshold: metric.threshold,
|
|
3631
|
+
skipped: true,
|
|
3632
|
+
...metric.reason ? { reason: metric.reason } : {}
|
|
3633
|
+
};
|
|
3634
|
+
}
|
|
3635
|
+
return {
|
|
3636
|
+
pct: metric.pct ?? 0,
|
|
3637
|
+
threshold: metric.threshold,
|
|
3638
|
+
pass: metric.pass ?? false,
|
|
3639
|
+
executable: metric.executable ?? 0,
|
|
3640
|
+
covered: metric.covered ?? 0,
|
|
3641
|
+
...metric.reason ? { reason: metric.reason } : {}
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
function resolvePathThresholdsJson(input) {
|
|
3645
|
+
const results = evaluatePathThresholds(input);
|
|
3646
|
+
return results.length > 0 ? pathThresholdsToJson(results) : void 0;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3485
3649
|
// src/output/human.ts
|
|
3486
3650
|
function formatRange(r) {
|
|
3487
3651
|
return r.start === r.end ? `${r.start}` : `${r.start}-${r.end}`;
|
|
@@ -3629,7 +3793,12 @@ function registerDiffCommand(program2) {
|
|
|
3629
3793
|
});
|
|
3630
3794
|
if (opts.json) {
|
|
3631
3795
|
const flags = resolveFlagsJson({ config, files, addedByFile });
|
|
3632
|
-
const
|
|
3796
|
+
const paths = resolvePathThresholdsJson({ config, files, addedByFile });
|
|
3797
|
+
const payload = {
|
|
3798
|
+
...diff,
|
|
3799
|
+
...flags ? { flags } : {},
|
|
3800
|
+
...paths ? { paths } : {}
|
|
3801
|
+
};
|
|
3633
3802
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
3634
3803
|
} else {
|
|
3635
3804
|
process.stdout.write(
|
|
@@ -3659,6 +3828,7 @@ function formatIncompleteCheck(state, json) {
|
|
|
3659
3828
|
projectPass: true,
|
|
3660
3829
|
overall: "pass",
|
|
3661
3830
|
flagResults: [],
|
|
3831
|
+
pathResults: [],
|
|
3662
3832
|
stdout: JSON.stringify({
|
|
3663
3833
|
overall: "pending",
|
|
3664
3834
|
complete: false,
|
|
@@ -3676,6 +3846,7 @@ function formatIncompleteCheck(state, json) {
|
|
|
3676
3846
|
projectPass: true,
|
|
3677
3847
|
overall: "pass",
|
|
3678
3848
|
flagResults: [],
|
|
3849
|
+
pathResults: [],
|
|
3679
3850
|
stdout: "",
|
|
3680
3851
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3681
3852
|
|
|
@@ -3694,6 +3865,7 @@ function formatCompleteHandshakeCheck(json) {
|
|
|
3694
3865
|
projectPass: true,
|
|
3695
3866
|
overall: "pass",
|
|
3696
3867
|
flagResults: [],
|
|
3868
|
+
pathResults: [],
|
|
3697
3869
|
stdout: JSON.stringify({ overall: "pending", complete: true, note }) + "\n",
|
|
3698
3870
|
stderr: "",
|
|
3699
3871
|
exitCode: 0
|
|
@@ -3705,6 +3877,7 @@ function formatCompleteHandshakeCheck(json) {
|
|
|
3705
3877
|
projectPass: true,
|
|
3706
3878
|
overall: "pass",
|
|
3707
3879
|
flagResults: [],
|
|
3880
|
+
pathResults: [],
|
|
3708
3881
|
stdout: "",
|
|
3709
3882
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3710
3883
|
|
|
@@ -3718,6 +3891,12 @@ function formatMetricLine(label, pct3, threshold, pass, indent = " ") {
|
|
|
3718
3891
|
const status = pass ? badge("pass") : badge("fail");
|
|
3719
3892
|
return `${indent}${label.padEnd(8)} ${pctStr}% (threshold ${threshold}) ${status}`;
|
|
3720
3893
|
}
|
|
3894
|
+
function presentPct(metric) {
|
|
3895
|
+
if (metric.pct === void 0 || metric.pass === void 0) {
|
|
3896
|
+
throw new Error("present metric missing pct/pass");
|
|
3897
|
+
}
|
|
3898
|
+
return { pct: metric.pct, pass: metric.pass };
|
|
3899
|
+
}
|
|
3721
3900
|
function formatFlagLines(results) {
|
|
3722
3901
|
if (results.length === 0) return [];
|
|
3723
3902
|
const lines = [""];
|
|
@@ -3734,16 +3913,52 @@ function formatFlagLines(results) {
|
|
|
3734
3913
|
` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
|
|
3735
3914
|
);
|
|
3736
3915
|
} else {
|
|
3916
|
+
const patch = presentPct(flag.patch);
|
|
3737
3917
|
lines.push(
|
|
3738
|
-
formatMetricLine("Patch",
|
|
3918
|
+
formatMetricLine("Patch", patch.pct, flag.patch.threshold, patch.pass, " ")
|
|
3739
3919
|
);
|
|
3740
3920
|
}
|
|
3921
|
+
const project = presentPct(flag.project);
|
|
3741
3922
|
lines.push(
|
|
3742
3923
|
formatMetricLine(
|
|
3743
3924
|
"Project",
|
|
3744
|
-
|
|
3925
|
+
project.pct,
|
|
3745
3926
|
flag.project.threshold,
|
|
3746
|
-
|
|
3927
|
+
project.pass,
|
|
3928
|
+
" "
|
|
3929
|
+
)
|
|
3930
|
+
);
|
|
3931
|
+
}
|
|
3932
|
+
return lines;
|
|
3933
|
+
}
|
|
3934
|
+
function formatPathLines(results) {
|
|
3935
|
+
if (results.length === 0) return [];
|
|
3936
|
+
const lines = [""];
|
|
3937
|
+
for (const path of results) {
|
|
3938
|
+
if (path.status === "missing") {
|
|
3939
|
+
lines.push(
|
|
3940
|
+
` ${path.glob} ${dim(path.reason ?? "missing this run")} ${badge("missing")}`
|
|
3941
|
+
);
|
|
3942
|
+
continue;
|
|
3943
|
+
}
|
|
3944
|
+
lines.push(` ${path.glob} ${path.status === "pass" ? badge("pass") : badge("fail")}`);
|
|
3945
|
+
if (path.patch.skipped) {
|
|
3946
|
+
lines.push(
|
|
3947
|
+
` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
|
|
3948
|
+
);
|
|
3949
|
+
} else {
|
|
3950
|
+
const patch = presentPct(path.patch);
|
|
3951
|
+
lines.push(
|
|
3952
|
+
formatMetricLine("Patch", patch.pct, path.patch.threshold, patch.pass, " ")
|
|
3953
|
+
);
|
|
3954
|
+
}
|
|
3955
|
+
const project = presentPct(path.project);
|
|
3956
|
+
lines.push(
|
|
3957
|
+
formatMetricLine(
|
|
3958
|
+
"Project",
|
|
3959
|
+
project.pct,
|
|
3960
|
+
path.project.threshold,
|
|
3961
|
+
project.pass,
|
|
3747
3962
|
" "
|
|
3748
3963
|
)
|
|
3749
3964
|
);
|
|
@@ -3759,6 +3974,7 @@ function runCheck(input) {
|
|
|
3759
3974
|
projectPass: true,
|
|
3760
3975
|
overall: "pass",
|
|
3761
3976
|
flagResults: [],
|
|
3977
|
+
pathResults: [],
|
|
3762
3978
|
stdout: "",
|
|
3763
3979
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3764
3980
|
|
|
@@ -3781,8 +3997,14 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3781
3997
|
addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map(),
|
|
3782
3998
|
...input.onlyFlag !== void 0 ? { onlyFlag: input.onlyFlag } : {}
|
|
3783
3999
|
});
|
|
4000
|
+
const pathResults = evaluatePathThresholds({
|
|
4001
|
+
config,
|
|
4002
|
+
files: input.files ?? [],
|
|
4003
|
+
addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map()
|
|
4004
|
+
});
|
|
3784
4005
|
const flagsOk = flagsPass(flagResults);
|
|
3785
|
-
const
|
|
4006
|
+
const pathsOk = pathThresholdsPass(pathResults);
|
|
4007
|
+
const overall = patchPass && projectPass && flagsOk && pathsOk ? "pass" : "fail";
|
|
3786
4008
|
const exitCode = overall === "pass" ? 0 : 1;
|
|
3787
4009
|
if (json) {
|
|
3788
4010
|
const payload = {
|
|
@@ -3794,6 +4016,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3794
4016
|
},
|
|
3795
4017
|
project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
|
|
3796
4018
|
...flagResults.length > 0 ? { flags: flagsToJson(flagResults) } : {},
|
|
4019
|
+
...pathResults.length > 0 ? { paths: pathThresholdsToJson(pathResults) } : {},
|
|
3797
4020
|
overall,
|
|
3798
4021
|
...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
|
|
3799
4022
|
};
|
|
@@ -3803,6 +4026,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3803
4026
|
projectPass,
|
|
3804
4027
|
overall,
|
|
3805
4028
|
flagResults,
|
|
4029
|
+
pathResults,
|
|
3806
4030
|
stdout: JSON.stringify(payload) + "\n",
|
|
3807
4031
|
stderr: "",
|
|
3808
4032
|
exitCode
|
|
@@ -3822,6 +4046,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3822
4046
|
}
|
|
3823
4047
|
lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
|
|
3824
4048
|
lines.push(...formatFlagLines(flagResults));
|
|
4049
|
+
lines.push(...formatPathLines(pathResults));
|
|
3825
4050
|
if (overall === "fail") {
|
|
3826
4051
|
lines.push("");
|
|
3827
4052
|
if (patchSkipped) {
|
|
@@ -3831,10 +4056,16 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3831
4056
|
if (missing.length > 0) {
|
|
3832
4057
|
lines.push(
|
|
3833
4058
|
dim(
|
|
3834
|
-
"
|
|
4059
|
+
"Flags with no files this run are skipped (not 0%). Scope the job with --flag when this coverage file is one package."
|
|
3835
4060
|
)
|
|
3836
4061
|
);
|
|
3837
4062
|
}
|
|
4063
|
+
const missingPaths = pathResults.filter((p) => p.status === "missing");
|
|
4064
|
+
if (missingPaths.length > 0) {
|
|
4065
|
+
lines.push(
|
|
4066
|
+
dim("Path globs with no files this run are skipped (not 0%).")
|
|
4067
|
+
);
|
|
4068
|
+
}
|
|
3838
4069
|
lines.push(tip("add tests for uncovered ranges: tested diff"));
|
|
3839
4070
|
} else {
|
|
3840
4071
|
lines.push("");
|
|
@@ -3851,6 +4082,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3851
4082
|
projectPass,
|
|
3852
4083
|
overall,
|
|
3853
4084
|
flagResults,
|
|
4085
|
+
pathResults,
|
|
3854
4086
|
stdout: lines.join("\n"),
|
|
3855
4087
|
stderr: "",
|
|
3856
4088
|
exitCode
|
|
@@ -3858,7 +4090,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3858
4090
|
}
|
|
3859
4091
|
function registerCheckCommand(program2) {
|
|
3860
4092
|
program2.command("check").description(
|
|
3861
|
-
"Exit non-zero if patch or
|
|
4093
|
+
"Exit non-zero if patch, project, or path coverage falls below configured thresholds."
|
|
3862
4094
|
).option("--json", "Emit machine-readable JSON to stdout (exit code unchanged).", false).option("--base <ref>", "Git base ref to diff against", void 0).option(
|
|
3863
4095
|
"--file <path>",
|
|
3864
4096
|
"Coverage file to merge (repeatable). Overrides coverage.path.",
|
package/package.json
CHANGED