@tested/cli 0.1.9 → 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 +2 -2
- package/dist/td.js +212 -15
- package/dist/tested.js +212 -15
- 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
|
|
|
@@ -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,7 +1128,12 @@ 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
|
|
@@ -1186,6 +1194,16 @@ var FlagResultJsonSchema = z2.object({
|
|
|
1186
1194
|
project: FlagMetricJsonSchema
|
|
1187
1195
|
});
|
|
1188
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);
|
|
1189
1207
|
|
|
1190
1208
|
// src/config.ts
|
|
1191
1209
|
var DEFAULT_IGNORES = [
|
|
@@ -1700,19 +1718,28 @@ async function computeDiffContext(opts) {
|
|
|
1700
1718
|
return { diff, files, addedByFile };
|
|
1701
1719
|
}
|
|
1702
1720
|
|
|
1703
|
-
// src/core/
|
|
1721
|
+
// src/core/globs.ts
|
|
1704
1722
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1705
|
-
var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
|
|
1706
|
-
var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
|
|
1707
1723
|
var MATCH_OPTS = { dot: true, matchBase: true };
|
|
1708
|
-
function
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
);
|
|
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));
|
|
1713
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";
|
|
1714
1741
|
function filterFilesByFlag(files, patterns) {
|
|
1715
|
-
return files
|
|
1742
|
+
return filterFilesByGlobs(files, patterns);
|
|
1716
1743
|
}
|
|
1717
1744
|
function resolveFlagThresholds(flag, global) {
|
|
1718
1745
|
return {
|
|
@@ -3099,7 +3126,7 @@ import pc3 from "picocolors";
|
|
|
3099
3126
|
// package.json
|
|
3100
3127
|
var package_default = {
|
|
3101
3128
|
name: "@tested/cli",
|
|
3102
|
-
version: "0.1.
|
|
3129
|
+
version: "0.1.10",
|
|
3103
3130
|
description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
|
|
3104
3131
|
license: "MIT",
|
|
3105
3132
|
homepage: "https://tested.dev",
|
|
@@ -3509,6 +3536,116 @@ function registerRunCommand(program2) {
|
|
|
3509
3536
|
// src/commands/diff.ts
|
|
3510
3537
|
import "commander";
|
|
3511
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
|
+
|
|
3512
3649
|
// src/output/human.ts
|
|
3513
3650
|
function formatRange(r) {
|
|
3514
3651
|
return r.start === r.end ? `${r.start}` : `${r.start}-${r.end}`;
|
|
@@ -3656,7 +3793,12 @@ function registerDiffCommand(program2) {
|
|
|
3656
3793
|
});
|
|
3657
3794
|
if (opts.json) {
|
|
3658
3795
|
const flags = resolveFlagsJson({ config, files, addedByFile });
|
|
3659
|
-
const
|
|
3796
|
+
const paths = resolvePathThresholdsJson({ config, files, addedByFile });
|
|
3797
|
+
const payload = {
|
|
3798
|
+
...diff,
|
|
3799
|
+
...flags ? { flags } : {},
|
|
3800
|
+
...paths ? { paths } : {}
|
|
3801
|
+
};
|
|
3660
3802
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
3661
3803
|
} else {
|
|
3662
3804
|
process.stdout.write(
|
|
@@ -3686,6 +3828,7 @@ function formatIncompleteCheck(state, json) {
|
|
|
3686
3828
|
projectPass: true,
|
|
3687
3829
|
overall: "pass",
|
|
3688
3830
|
flagResults: [],
|
|
3831
|
+
pathResults: [],
|
|
3689
3832
|
stdout: JSON.stringify({
|
|
3690
3833
|
overall: "pending",
|
|
3691
3834
|
complete: false,
|
|
@@ -3703,6 +3846,7 @@ function formatIncompleteCheck(state, json) {
|
|
|
3703
3846
|
projectPass: true,
|
|
3704
3847
|
overall: "pass",
|
|
3705
3848
|
flagResults: [],
|
|
3849
|
+
pathResults: [],
|
|
3706
3850
|
stdout: "",
|
|
3707
3851
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3708
3852
|
|
|
@@ -3721,6 +3865,7 @@ function formatCompleteHandshakeCheck(json) {
|
|
|
3721
3865
|
projectPass: true,
|
|
3722
3866
|
overall: "pass",
|
|
3723
3867
|
flagResults: [],
|
|
3868
|
+
pathResults: [],
|
|
3724
3869
|
stdout: JSON.stringify({ overall: "pending", complete: true, note }) + "\n",
|
|
3725
3870
|
stderr: "",
|
|
3726
3871
|
exitCode: 0
|
|
@@ -3732,6 +3877,7 @@ function formatCompleteHandshakeCheck(json) {
|
|
|
3732
3877
|
projectPass: true,
|
|
3733
3878
|
overall: "pass",
|
|
3734
3879
|
flagResults: [],
|
|
3880
|
+
pathResults: [],
|
|
3735
3881
|
stdout: "",
|
|
3736
3882
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3737
3883
|
|
|
@@ -3747,7 +3893,7 @@ function formatMetricLine(label, pct3, threshold, pass, indent = " ") {
|
|
|
3747
3893
|
}
|
|
3748
3894
|
function presentPct(metric) {
|
|
3749
3895
|
if (metric.pct === void 0 || metric.pass === void 0) {
|
|
3750
|
-
throw new Error("present
|
|
3896
|
+
throw new Error("present metric missing pct/pass");
|
|
3751
3897
|
}
|
|
3752
3898
|
return { pct: metric.pct, pass: metric.pass };
|
|
3753
3899
|
}
|
|
@@ -3785,6 +3931,40 @@ function formatFlagLines(results) {
|
|
|
3785
3931
|
}
|
|
3786
3932
|
return lines;
|
|
3787
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,
|
|
3962
|
+
" "
|
|
3963
|
+
)
|
|
3964
|
+
);
|
|
3965
|
+
}
|
|
3966
|
+
return lines;
|
|
3967
|
+
}
|
|
3788
3968
|
function runCheck(input) {
|
|
3789
3969
|
const { config, diff, json } = input;
|
|
3790
3970
|
if (!config.thresholds) {
|
|
@@ -3794,6 +3974,7 @@ function runCheck(input) {
|
|
|
3794
3974
|
projectPass: true,
|
|
3795
3975
|
overall: "pass",
|
|
3796
3976
|
flagResults: [],
|
|
3977
|
+
pathResults: [],
|
|
3797
3978
|
stdout: "",
|
|
3798
3979
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3799
3980
|
|
|
@@ -3816,8 +3997,14 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3816
3997
|
addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map(),
|
|
3817
3998
|
...input.onlyFlag !== void 0 ? { onlyFlag: input.onlyFlag } : {}
|
|
3818
3999
|
});
|
|
4000
|
+
const pathResults = evaluatePathThresholds({
|
|
4001
|
+
config,
|
|
4002
|
+
files: input.files ?? [],
|
|
4003
|
+
addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map()
|
|
4004
|
+
});
|
|
3819
4005
|
const flagsOk = flagsPass(flagResults);
|
|
3820
|
-
const
|
|
4006
|
+
const pathsOk = pathThresholdsPass(pathResults);
|
|
4007
|
+
const overall = patchPass && projectPass && flagsOk && pathsOk ? "pass" : "fail";
|
|
3821
4008
|
const exitCode = overall === "pass" ? 0 : 1;
|
|
3822
4009
|
if (json) {
|
|
3823
4010
|
const payload = {
|
|
@@ -3829,6 +4016,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3829
4016
|
},
|
|
3830
4017
|
project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
|
|
3831
4018
|
...flagResults.length > 0 ? { flags: flagsToJson(flagResults) } : {},
|
|
4019
|
+
...pathResults.length > 0 ? { paths: pathThresholdsToJson(pathResults) } : {},
|
|
3832
4020
|
overall,
|
|
3833
4021
|
...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
|
|
3834
4022
|
};
|
|
@@ -3838,6 +4026,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3838
4026
|
projectPass,
|
|
3839
4027
|
overall,
|
|
3840
4028
|
flagResults,
|
|
4029
|
+
pathResults,
|
|
3841
4030
|
stdout: JSON.stringify(payload) + "\n",
|
|
3842
4031
|
stderr: "",
|
|
3843
4032
|
exitCode
|
|
@@ -3857,6 +4046,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3857
4046
|
}
|
|
3858
4047
|
lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
|
|
3859
4048
|
lines.push(...formatFlagLines(flagResults));
|
|
4049
|
+
lines.push(...formatPathLines(pathResults));
|
|
3860
4050
|
if (overall === "fail") {
|
|
3861
4051
|
lines.push("");
|
|
3862
4052
|
if (patchSkipped) {
|
|
@@ -3870,6 +4060,12 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3870
4060
|
)
|
|
3871
4061
|
);
|
|
3872
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
|
+
}
|
|
3873
4069
|
lines.push(tip("add tests for uncovered ranges: tested diff"));
|
|
3874
4070
|
} else {
|
|
3875
4071
|
lines.push("");
|
|
@@ -3886,6 +4082,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3886
4082
|
projectPass,
|
|
3887
4083
|
overall,
|
|
3888
4084
|
flagResults,
|
|
4085
|
+
pathResults,
|
|
3889
4086
|
stdout: lines.join("\n"),
|
|
3890
4087
|
stderr: "",
|
|
3891
4088
|
exitCode
|
|
@@ -3893,7 +4090,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3893
4090
|
}
|
|
3894
4091
|
function registerCheckCommand(program2) {
|
|
3895
4092
|
program2.command("check").description(
|
|
3896
|
-
"Exit non-zero if patch or
|
|
4093
|
+
"Exit non-zero if patch, project, or path coverage falls below configured thresholds."
|
|
3897
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(
|
|
3898
4095
|
"--file <path>",
|
|
3899
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,7 +1128,12 @@ 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
|
|
@@ -1186,6 +1194,16 @@ var FlagResultJsonSchema = z2.object({
|
|
|
1186
1194
|
project: FlagMetricJsonSchema
|
|
1187
1195
|
});
|
|
1188
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);
|
|
1189
1207
|
|
|
1190
1208
|
// src/config.ts
|
|
1191
1209
|
var DEFAULT_IGNORES = [
|
|
@@ -1700,19 +1718,28 @@ async function computeDiffContext(opts) {
|
|
|
1700
1718
|
return { diff, files, addedByFile };
|
|
1701
1719
|
}
|
|
1702
1720
|
|
|
1703
|
-
// src/core/
|
|
1721
|
+
// src/core/globs.ts
|
|
1704
1722
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1705
|
-
var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
|
|
1706
|
-
var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
|
|
1707
1723
|
var MATCH_OPTS = { dot: true, matchBase: true };
|
|
1708
|
-
function
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
);
|
|
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));
|
|
1713
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";
|
|
1714
1741
|
function filterFilesByFlag(files, patterns) {
|
|
1715
|
-
return files
|
|
1742
|
+
return filterFilesByGlobs(files, patterns);
|
|
1716
1743
|
}
|
|
1717
1744
|
function resolveFlagThresholds(flag, global) {
|
|
1718
1745
|
return {
|
|
@@ -3099,7 +3126,7 @@ import pc3 from "picocolors";
|
|
|
3099
3126
|
// package.json
|
|
3100
3127
|
var package_default = {
|
|
3101
3128
|
name: "@tested/cli",
|
|
3102
|
-
version: "0.1.
|
|
3129
|
+
version: "0.1.10",
|
|
3103
3130
|
description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
|
|
3104
3131
|
license: "MIT",
|
|
3105
3132
|
homepage: "https://tested.dev",
|
|
@@ -3509,6 +3536,116 @@ function registerRunCommand(program2) {
|
|
|
3509
3536
|
// src/commands/diff.ts
|
|
3510
3537
|
import "commander";
|
|
3511
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
|
+
|
|
3512
3649
|
// src/output/human.ts
|
|
3513
3650
|
function formatRange(r) {
|
|
3514
3651
|
return r.start === r.end ? `${r.start}` : `${r.start}-${r.end}`;
|
|
@@ -3656,7 +3793,12 @@ function registerDiffCommand(program2) {
|
|
|
3656
3793
|
});
|
|
3657
3794
|
if (opts.json) {
|
|
3658
3795
|
const flags = resolveFlagsJson({ config, files, addedByFile });
|
|
3659
|
-
const
|
|
3796
|
+
const paths = resolvePathThresholdsJson({ config, files, addedByFile });
|
|
3797
|
+
const payload = {
|
|
3798
|
+
...diff,
|
|
3799
|
+
...flags ? { flags } : {},
|
|
3800
|
+
...paths ? { paths } : {}
|
|
3801
|
+
};
|
|
3660
3802
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
3661
3803
|
} else {
|
|
3662
3804
|
process.stdout.write(
|
|
@@ -3686,6 +3828,7 @@ function formatIncompleteCheck(state, json) {
|
|
|
3686
3828
|
projectPass: true,
|
|
3687
3829
|
overall: "pass",
|
|
3688
3830
|
flagResults: [],
|
|
3831
|
+
pathResults: [],
|
|
3689
3832
|
stdout: JSON.stringify({
|
|
3690
3833
|
overall: "pending",
|
|
3691
3834
|
complete: false,
|
|
@@ -3703,6 +3846,7 @@ function formatIncompleteCheck(state, json) {
|
|
|
3703
3846
|
projectPass: true,
|
|
3704
3847
|
overall: "pass",
|
|
3705
3848
|
flagResults: [],
|
|
3849
|
+
pathResults: [],
|
|
3706
3850
|
stdout: "",
|
|
3707
3851
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3708
3852
|
|
|
@@ -3721,6 +3865,7 @@ function formatCompleteHandshakeCheck(json) {
|
|
|
3721
3865
|
projectPass: true,
|
|
3722
3866
|
overall: "pass",
|
|
3723
3867
|
flagResults: [],
|
|
3868
|
+
pathResults: [],
|
|
3724
3869
|
stdout: JSON.stringify({ overall: "pending", complete: true, note }) + "\n",
|
|
3725
3870
|
stderr: "",
|
|
3726
3871
|
exitCode: 0
|
|
@@ -3732,6 +3877,7 @@ function formatCompleteHandshakeCheck(json) {
|
|
|
3732
3877
|
projectPass: true,
|
|
3733
3878
|
overall: "pass",
|
|
3734
3879
|
flagResults: [],
|
|
3880
|
+
pathResults: [],
|
|
3735
3881
|
stdout: "",
|
|
3736
3882
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3737
3883
|
|
|
@@ -3747,7 +3893,7 @@ function formatMetricLine(label, pct3, threshold, pass, indent = " ") {
|
|
|
3747
3893
|
}
|
|
3748
3894
|
function presentPct(metric) {
|
|
3749
3895
|
if (metric.pct === void 0 || metric.pass === void 0) {
|
|
3750
|
-
throw new Error("present
|
|
3896
|
+
throw new Error("present metric missing pct/pass");
|
|
3751
3897
|
}
|
|
3752
3898
|
return { pct: metric.pct, pass: metric.pass };
|
|
3753
3899
|
}
|
|
@@ -3785,6 +3931,40 @@ function formatFlagLines(results) {
|
|
|
3785
3931
|
}
|
|
3786
3932
|
return lines;
|
|
3787
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,
|
|
3962
|
+
" "
|
|
3963
|
+
)
|
|
3964
|
+
);
|
|
3965
|
+
}
|
|
3966
|
+
return lines;
|
|
3967
|
+
}
|
|
3788
3968
|
function runCheck(input) {
|
|
3789
3969
|
const { config, diff, json } = input;
|
|
3790
3970
|
if (!config.thresholds) {
|
|
@@ -3794,6 +3974,7 @@ function runCheck(input) {
|
|
|
3794
3974
|
projectPass: true,
|
|
3795
3975
|
overall: "pass",
|
|
3796
3976
|
flagResults: [],
|
|
3977
|
+
pathResults: [],
|
|
3797
3978
|
stdout: "",
|
|
3798
3979
|
stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
|
|
3799
3980
|
|
|
@@ -3816,8 +3997,14 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3816
3997
|
addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map(),
|
|
3817
3998
|
...input.onlyFlag !== void 0 ? { onlyFlag: input.onlyFlag } : {}
|
|
3818
3999
|
});
|
|
4000
|
+
const pathResults = evaluatePathThresholds({
|
|
4001
|
+
config,
|
|
4002
|
+
files: input.files ?? [],
|
|
4003
|
+
addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map()
|
|
4004
|
+
});
|
|
3819
4005
|
const flagsOk = flagsPass(flagResults);
|
|
3820
|
-
const
|
|
4006
|
+
const pathsOk = pathThresholdsPass(pathResults);
|
|
4007
|
+
const overall = patchPass && projectPass && flagsOk && pathsOk ? "pass" : "fail";
|
|
3821
4008
|
const exitCode = overall === "pass" ? 0 : 1;
|
|
3822
4009
|
if (json) {
|
|
3823
4010
|
const payload = {
|
|
@@ -3829,6 +4016,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3829
4016
|
},
|
|
3830
4017
|
project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
|
|
3831
4018
|
...flagResults.length > 0 ? { flags: flagsToJson(flagResults) } : {},
|
|
4019
|
+
...pathResults.length > 0 ? { paths: pathThresholdsToJson(pathResults) } : {},
|
|
3832
4020
|
overall,
|
|
3833
4021
|
...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
|
|
3834
4022
|
};
|
|
@@ -3838,6 +4026,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3838
4026
|
projectPass,
|
|
3839
4027
|
overall,
|
|
3840
4028
|
flagResults,
|
|
4029
|
+
pathResults,
|
|
3841
4030
|
stdout: JSON.stringify(payload) + "\n",
|
|
3842
4031
|
stderr: "",
|
|
3843
4032
|
exitCode
|
|
@@ -3857,6 +4046,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3857
4046
|
}
|
|
3858
4047
|
lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
|
|
3859
4048
|
lines.push(...formatFlagLines(flagResults));
|
|
4049
|
+
lines.push(...formatPathLines(pathResults));
|
|
3860
4050
|
if (overall === "fail") {
|
|
3861
4051
|
lines.push("");
|
|
3862
4052
|
if (patchSkipped) {
|
|
@@ -3870,6 +4060,12 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3870
4060
|
)
|
|
3871
4061
|
);
|
|
3872
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
|
+
}
|
|
3873
4069
|
lines.push(tip("add tests for uncovered ranges: tested diff"));
|
|
3874
4070
|
} else {
|
|
3875
4071
|
lines.push("");
|
|
@@ -3886,6 +4082,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3886
4082
|
projectPass,
|
|
3887
4083
|
overall,
|
|
3888
4084
|
flagResults,
|
|
4085
|
+
pathResults,
|
|
3889
4086
|
stdout: lines.join("\n"),
|
|
3890
4087
|
stderr: "",
|
|
3891
4088
|
exitCode
|
|
@@ -3893,7 +4090,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
3893
4090
|
}
|
|
3894
4091
|
function registerCheckCommand(program2) {
|
|
3895
4092
|
program2.command("check").description(
|
|
3896
|
-
"Exit non-zero if patch or
|
|
4093
|
+
"Exit non-zero if patch, project, or path coverage falls below configured thresholds."
|
|
3897
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(
|
|
3898
4095
|
"--file <path>",
|
|
3899
4096
|
"Coverage file to merge (repeatable). Overrides coverage.path.",
|
package/package.json
CHANGED