@tested/cli 0.1.0 → 0.1.1
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 +13 -1
- package/dist/td.js +54 -22
- package/dist/tested.js +54 -22
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Node >= 20.19.
|
|
|
12
12
|
```bash
|
|
13
13
|
pnpm add -D @tested/cli
|
|
14
14
|
# or
|
|
15
|
-
npx @tested/cli
|
|
15
|
+
npx @tested/cli # runs `tested` (`td` is the same CLI after install)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
CI uses the composite Action (`tested-hq/cli/action@main`). The Action clones this repo and builds the CLI.
|
|
@@ -110,6 +110,18 @@ $ echo $?
|
|
|
110
110
|
1
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
If the patch has no executable lines in scope (tests-only, comments-only, docs-only, or ignored files), the patch gate is **skipped** — not reported as 0% coverage. Project still applies.
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
$ tested check
|
|
117
|
+
tested.dev — coverage gate [PASS]
|
|
118
|
+
|
|
119
|
+
Patch - no executable lines in the patch [SKIP]
|
|
120
|
+
Project 64.1% (threshold 60) [PASS]
|
|
121
|
+
|
|
122
|
+
No executable lines in the patch — patch gate skipped. Project threshold met.
|
|
123
|
+
```
|
|
124
|
+
|
|
113
125
|
If `thresholds` is missing from `.tested.yaml`, `tested check` prints a notice on stderr and exits 0 — configs that haven't opted in stay green.
|
|
114
126
|
|
|
115
127
|
### GitHub Actions
|
package/dist/td.js
CHANGED
|
@@ -44,6 +44,8 @@ function badge(kind) {
|
|
|
44
44
|
return pc.yellow("[WARN]");
|
|
45
45
|
case "info":
|
|
46
46
|
return pc.cyan("[INFO]");
|
|
47
|
+
case "skip":
|
|
48
|
+
return pc.cyan("[SKIP]");
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
function metricBar(pct3, width = 10) {
|
|
@@ -179,6 +181,8 @@ function buildInitYaml(args) {
|
|
|
179
181
|
if (args.testRunner) {
|
|
180
182
|
lines.push(`testRunner: ${args.testRunner}`);
|
|
181
183
|
}
|
|
184
|
+
lines.push("# Patch gate is skipped when the diff has no executable lines");
|
|
185
|
+
lines.push("# (tests-only, comments-only, docs-only, or ignored files).");
|
|
182
186
|
lines.push("thresholds:");
|
|
183
187
|
lines.push(" patch: 80");
|
|
184
188
|
lines.push(" project: 60");
|
|
@@ -545,7 +549,9 @@ var FileCoverageSchema = z2.object({
|
|
|
545
549
|
var CoverageTotalsSchema = z2.object({
|
|
546
550
|
executable: z2.number().int().nonnegative(),
|
|
547
551
|
covered: z2.number().int().nonnegative(),
|
|
548
|
-
pct: z2.number().min(0).max(100)
|
|
552
|
+
pct: z2.number().min(0).max(100),
|
|
553
|
+
/** Present when executable === 0 — not a 0% coverage result. */
|
|
554
|
+
empty: z2.literal(true).optional()
|
|
549
555
|
});
|
|
550
556
|
var ProjectTotalsSchema = CoverageTotalsSchema.extend({
|
|
551
557
|
delta: z2.number().nullable()
|
|
@@ -614,7 +620,14 @@ async function headSha(ctx) {
|
|
|
614
620
|
return (await ctx.git.revparse(["HEAD"])).trim();
|
|
615
621
|
}
|
|
616
622
|
async function unifiedDiff(ctx, base) {
|
|
617
|
-
|
|
623
|
+
try {
|
|
624
|
+
const mergeBase = (await ctx.git.raw(["merge-base", base, "HEAD"])).trim();
|
|
625
|
+
if (mergeBase) {
|
|
626
|
+
return ctx.git.diff([`${base}...HEAD`]);
|
|
627
|
+
}
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
return ctx.git.diff([base, "HEAD"]);
|
|
618
631
|
}
|
|
619
632
|
async function remoteUrl(ctx, remote = "origin") {
|
|
620
633
|
return (await ctx.git.raw(["remote", "get-url", remote])).trim();
|
|
@@ -763,6 +776,10 @@ function assertWithinRoot(root, resolvedPath) {
|
|
|
763
776
|
}
|
|
764
777
|
|
|
765
778
|
// src/core/patch.ts
|
|
779
|
+
var EMPTY_PATCH_REASON = "no executable lines in the patch";
|
|
780
|
+
function isEmptyPatch(totals) {
|
|
781
|
+
return totals.executable === 0;
|
|
782
|
+
}
|
|
766
783
|
function pct(covered, executable) {
|
|
767
784
|
if (executable === 0) return 0;
|
|
768
785
|
return Math.round(covered / executable * 1e3) / 10;
|
|
@@ -862,10 +879,10 @@ function uncoveredRanges(file) {
|
|
|
862
879
|
function buildDiffOutput(args) {
|
|
863
880
|
const patch = computePatchCoverage(args.files, args.addedByFile);
|
|
864
881
|
const project = computeProjectCoverage(args.files);
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
...project.byFile.keys()
|
|
868
|
-
|
|
882
|
+
const emptyPatch = isEmptyPatch(patch.totals);
|
|
883
|
+
const fileNames = new Set(
|
|
884
|
+
emptyPatch ? [] : [...patch.byFile.keys(), ...project.byFile.keys()]
|
|
885
|
+
);
|
|
869
886
|
const files = [];
|
|
870
887
|
for (const name of fileNames) {
|
|
871
888
|
const file = args.files.find((f) => f.path === name);
|
|
@@ -882,7 +899,7 @@ function buildDiffOutput(args) {
|
|
|
882
899
|
schemaVersion: 1,
|
|
883
900
|
base: args.base,
|
|
884
901
|
head: args.head,
|
|
885
|
-
patch: patch.totals,
|
|
902
|
+
patch: emptyPatch ? { ...patch.totals, empty: true } : patch.totals,
|
|
886
903
|
project: { ...project.totals, delta: args.projectDelta ?? null },
|
|
887
904
|
files,
|
|
888
905
|
ignored: [...args.ignored]
|
|
@@ -1865,8 +1882,6 @@ function buildCiSnippet() {
|
|
|
1865
1882
|
" runs-on: ubuntu-latest",
|
|
1866
1883
|
" steps:",
|
|
1867
1884
|
" - uses: actions/checkout@v4",
|
|
1868
|
-
" with:",
|
|
1869
|
-
" fetch-depth: 0",
|
|
1870
1885
|
" - uses: tested-hq/cli/action@main",
|
|
1871
1886
|
" with:",
|
|
1872
1887
|
" # pin ref for reproducible installs (do not use floating tags in prod)",
|
|
@@ -1931,6 +1946,12 @@ function formatSetupHuman(opts) {
|
|
|
1931
1946
|
])
|
|
1932
1947
|
);
|
|
1933
1948
|
lines.push("");
|
|
1949
|
+
lines.push(
|
|
1950
|
+
dim(
|
|
1951
|
+
"Patch gate is skipped when the diff has no executable lines (tests-only, docs, comments)."
|
|
1952
|
+
)
|
|
1953
|
+
);
|
|
1954
|
+
lines.push("");
|
|
1934
1955
|
lines.push(tip("re-check anytime: tested doctor"));
|
|
1935
1956
|
lines.push("");
|
|
1936
1957
|
return lines.join("\n");
|
|
@@ -2196,8 +2217,8 @@ function coloredPctCell(pct3, width = 6) {
|
|
|
2196
2217
|
function formatMetricRow(label, pct3, covered, executable, emptyNote) {
|
|
2197
2218
|
const labelPad = label.padEnd(8);
|
|
2198
2219
|
if (executable === 0) {
|
|
2199
|
-
const note = emptyNote ??
|
|
2200
|
-
return ` ${labelPad} ${dim("-".padEnd(6))} ${
|
|
2220
|
+
const note = emptyNote ?? EMPTY_PATCH_REASON;
|
|
2221
|
+
return ` ${labelPad} ${dim("-".padEnd(6))} ${note}`;
|
|
2201
2222
|
}
|
|
2202
2223
|
const pctStr = coloredPctCell(pct3);
|
|
2203
2224
|
const bar = metricBar(pct3);
|
|
@@ -2216,7 +2237,7 @@ function formatHuman(out, opts = {}) {
|
|
|
2216
2237
|
out.patch.pct,
|
|
2217
2238
|
out.patch.covered,
|
|
2218
2239
|
out.patch.executable,
|
|
2219
|
-
|
|
2240
|
+
EMPTY_PATCH_REASON
|
|
2220
2241
|
)
|
|
2221
2242
|
);
|
|
2222
2243
|
const projectRow = formatMetricRow(
|
|
@@ -2229,7 +2250,7 @@ function formatHuman(out, opts = {}) {
|
|
|
2229
2250
|
if (opts.thresholds) {
|
|
2230
2251
|
const patchPass = out.patch.pct >= opts.thresholds.patch;
|
|
2231
2252
|
const projectPass = out.project.pct >= opts.thresholds.project;
|
|
2232
|
-
const patchOk = out.patch
|
|
2253
|
+
const patchOk = isEmptyPatch(out.patch) ? true : patchPass;
|
|
2233
2254
|
const overall = patchOk && projectPass;
|
|
2234
2255
|
const details = [];
|
|
2235
2256
|
if (!patchOk) {
|
|
@@ -2240,7 +2261,7 @@ function formatHuman(out, opts = {}) {
|
|
|
2240
2261
|
`project ${out.project.pct.toFixed(1)}% < ${opts.thresholds.project}%`
|
|
2241
2262
|
);
|
|
2242
2263
|
}
|
|
2243
|
-
const detail = details.length > 0 ? dim(` ${details.join("; ")}`) : "";
|
|
2264
|
+
const detail = details.length > 0 ? dim(` ${details.join("; ")}`) : isEmptyPatch(out.patch) ? dim(` ${EMPTY_PATCH_REASON}`) : "";
|
|
2244
2265
|
lines.push(
|
|
2245
2266
|
` ${"Gate".padEnd(8)} ${overall ? badge("pass") : badge("fail")}${detail}`
|
|
2246
2267
|
);
|
|
@@ -2253,12 +2274,15 @@ function formatHuman(out, opts = {}) {
|
|
|
2253
2274
|
);
|
|
2254
2275
|
}
|
|
2255
2276
|
}
|
|
2256
|
-
if (out.
|
|
2277
|
+
if (isEmptyPatch(out.patch)) {
|
|
2278
|
+
lines.push("");
|
|
2279
|
+
lines.push(dim("No executable lines in the patch \u2014 patch gate does not apply."));
|
|
2280
|
+
} else if (out.files.length > 0) {
|
|
2257
2281
|
lines.push("");
|
|
2258
2282
|
lines.push(heading("Files in diff:"));
|
|
2259
2283
|
const anyPatch = out.files.some((f) => f.patchCoverage !== null);
|
|
2260
2284
|
if (!anyPatch) {
|
|
2261
|
-
lines.push(dim(
|
|
2285
|
+
lines.push(dim(` (project coverage \u2014 ${EMPTY_PATCH_REASON})`));
|
|
2262
2286
|
}
|
|
2263
2287
|
for (const f of out.files) {
|
|
2264
2288
|
const hasPatch = f.patchCoverage !== null;
|
|
@@ -2344,7 +2368,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
2344
2368
|
const projectPct = diff.project.pct;
|
|
2345
2369
|
const patchThreshold = config.thresholds.patch;
|
|
2346
2370
|
const projectThreshold = config.thresholds.project;
|
|
2347
|
-
const patchSkipped = diff.patch
|
|
2371
|
+
const patchSkipped = isEmptyPatch(diff.patch);
|
|
2348
2372
|
const patchPass = patchSkipped ? true : patchPct >= patchThreshold;
|
|
2349
2373
|
const projectPass = projectPct >= projectThreshold;
|
|
2350
2374
|
const overall = patchPass && projectPass ? "pass" : "fail";
|
|
@@ -2355,10 +2379,11 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
2355
2379
|
pct: patchPct,
|
|
2356
2380
|
threshold: patchThreshold,
|
|
2357
2381
|
pass: patchPass,
|
|
2358
|
-
...patchSkipped ? { skipped: true } : {}
|
|
2382
|
+
...patchSkipped ? { skipped: true, reason: EMPTY_PATCH_REASON } : {}
|
|
2359
2383
|
},
|
|
2360
2384
|
project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
|
|
2361
|
-
overall
|
|
2385
|
+
overall,
|
|
2386
|
+
...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
|
|
2362
2387
|
};
|
|
2363
2388
|
return {
|
|
2364
2389
|
skipped: false,
|
|
@@ -2377,7 +2402,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
2377
2402
|
lines.push("");
|
|
2378
2403
|
if (patchSkipped) {
|
|
2379
2404
|
lines.push(
|
|
2380
|
-
` ${"Patch".padEnd(8)} ${dim("-")} ${
|
|
2405
|
+
` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
|
|
2381
2406
|
);
|
|
2382
2407
|
} else {
|
|
2383
2408
|
lines.push(formatMetricLine("Patch", patchPct, patchThreshold, patchPass));
|
|
@@ -2385,10 +2410,17 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
2385
2410
|
lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
|
|
2386
2411
|
if (overall === "fail") {
|
|
2387
2412
|
lines.push("");
|
|
2413
|
+
if (patchSkipped) {
|
|
2414
|
+
lines.push(dim("No executable lines in the patch \u2014 patch gate skipped."));
|
|
2415
|
+
}
|
|
2388
2416
|
lines.push(tip("add tests for uncovered ranges: tested diff"));
|
|
2389
2417
|
} else {
|
|
2390
2418
|
lines.push("");
|
|
2391
|
-
lines.push(
|
|
2419
|
+
lines.push(
|
|
2420
|
+
dim(
|
|
2421
|
+
patchSkipped ? "No executable lines in the patch \u2014 patch gate skipped. Project threshold met." : "thresholds met"
|
|
2422
|
+
)
|
|
2423
|
+
);
|
|
2392
2424
|
}
|
|
2393
2425
|
lines.push("");
|
|
2394
2426
|
return {
|
|
@@ -2561,7 +2593,7 @@ function createProgram() {
|
|
|
2561
2593
|
"Agent loop:",
|
|
2562
2594
|
" tested setup \u2192 tested run \u2192 tested diff \u2192 tested check \u2192 tested push --pr <n>"
|
|
2563
2595
|
].join("\n")
|
|
2564
|
-
).version("0.1.
|
|
2596
|
+
).version("0.1.1");
|
|
2565
2597
|
registerSetupCommand(program2);
|
|
2566
2598
|
registerDoctorCommand(program2);
|
|
2567
2599
|
registerInitCommand(program2);
|
package/dist/tested.js
CHANGED
|
@@ -44,6 +44,8 @@ function badge(kind) {
|
|
|
44
44
|
return pc.yellow("[WARN]");
|
|
45
45
|
case "info":
|
|
46
46
|
return pc.cyan("[INFO]");
|
|
47
|
+
case "skip":
|
|
48
|
+
return pc.cyan("[SKIP]");
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
function metricBar(pct3, width = 10) {
|
|
@@ -179,6 +181,8 @@ function buildInitYaml(args) {
|
|
|
179
181
|
if (args.testRunner) {
|
|
180
182
|
lines.push(`testRunner: ${args.testRunner}`);
|
|
181
183
|
}
|
|
184
|
+
lines.push("# Patch gate is skipped when the diff has no executable lines");
|
|
185
|
+
lines.push("# (tests-only, comments-only, docs-only, or ignored files).");
|
|
182
186
|
lines.push("thresholds:");
|
|
183
187
|
lines.push(" patch: 80");
|
|
184
188
|
lines.push(" project: 60");
|
|
@@ -545,7 +549,9 @@ var FileCoverageSchema = z2.object({
|
|
|
545
549
|
var CoverageTotalsSchema = z2.object({
|
|
546
550
|
executable: z2.number().int().nonnegative(),
|
|
547
551
|
covered: z2.number().int().nonnegative(),
|
|
548
|
-
pct: z2.number().min(0).max(100)
|
|
552
|
+
pct: z2.number().min(0).max(100),
|
|
553
|
+
/** Present when executable === 0 — not a 0% coverage result. */
|
|
554
|
+
empty: z2.literal(true).optional()
|
|
549
555
|
});
|
|
550
556
|
var ProjectTotalsSchema = CoverageTotalsSchema.extend({
|
|
551
557
|
delta: z2.number().nullable()
|
|
@@ -614,7 +620,14 @@ async function headSha(ctx) {
|
|
|
614
620
|
return (await ctx.git.revparse(["HEAD"])).trim();
|
|
615
621
|
}
|
|
616
622
|
async function unifiedDiff(ctx, base) {
|
|
617
|
-
|
|
623
|
+
try {
|
|
624
|
+
const mergeBase = (await ctx.git.raw(["merge-base", base, "HEAD"])).trim();
|
|
625
|
+
if (mergeBase) {
|
|
626
|
+
return ctx.git.diff([`${base}...HEAD`]);
|
|
627
|
+
}
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
return ctx.git.diff([base, "HEAD"]);
|
|
618
631
|
}
|
|
619
632
|
async function remoteUrl(ctx, remote = "origin") {
|
|
620
633
|
return (await ctx.git.raw(["remote", "get-url", remote])).trim();
|
|
@@ -763,6 +776,10 @@ function assertWithinRoot(root, resolvedPath) {
|
|
|
763
776
|
}
|
|
764
777
|
|
|
765
778
|
// src/core/patch.ts
|
|
779
|
+
var EMPTY_PATCH_REASON = "no executable lines in the patch";
|
|
780
|
+
function isEmptyPatch(totals) {
|
|
781
|
+
return totals.executable === 0;
|
|
782
|
+
}
|
|
766
783
|
function pct(covered, executable) {
|
|
767
784
|
if (executable === 0) return 0;
|
|
768
785
|
return Math.round(covered / executable * 1e3) / 10;
|
|
@@ -862,10 +879,10 @@ function uncoveredRanges(file) {
|
|
|
862
879
|
function buildDiffOutput(args) {
|
|
863
880
|
const patch = computePatchCoverage(args.files, args.addedByFile);
|
|
864
881
|
const project = computeProjectCoverage(args.files);
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
...project.byFile.keys()
|
|
868
|
-
|
|
882
|
+
const emptyPatch = isEmptyPatch(patch.totals);
|
|
883
|
+
const fileNames = new Set(
|
|
884
|
+
emptyPatch ? [] : [...patch.byFile.keys(), ...project.byFile.keys()]
|
|
885
|
+
);
|
|
869
886
|
const files = [];
|
|
870
887
|
for (const name of fileNames) {
|
|
871
888
|
const file = args.files.find((f) => f.path === name);
|
|
@@ -882,7 +899,7 @@ function buildDiffOutput(args) {
|
|
|
882
899
|
schemaVersion: 1,
|
|
883
900
|
base: args.base,
|
|
884
901
|
head: args.head,
|
|
885
|
-
patch: patch.totals,
|
|
902
|
+
patch: emptyPatch ? { ...patch.totals, empty: true } : patch.totals,
|
|
886
903
|
project: { ...project.totals, delta: args.projectDelta ?? null },
|
|
887
904
|
files,
|
|
888
905
|
ignored: [...args.ignored]
|
|
@@ -1865,8 +1882,6 @@ function buildCiSnippet() {
|
|
|
1865
1882
|
" runs-on: ubuntu-latest",
|
|
1866
1883
|
" steps:",
|
|
1867
1884
|
" - uses: actions/checkout@v4",
|
|
1868
|
-
" with:",
|
|
1869
|
-
" fetch-depth: 0",
|
|
1870
1885
|
" - uses: tested-hq/cli/action@main",
|
|
1871
1886
|
" with:",
|
|
1872
1887
|
" # pin ref for reproducible installs (do not use floating tags in prod)",
|
|
@@ -1931,6 +1946,12 @@ function formatSetupHuman(opts) {
|
|
|
1931
1946
|
])
|
|
1932
1947
|
);
|
|
1933
1948
|
lines.push("");
|
|
1949
|
+
lines.push(
|
|
1950
|
+
dim(
|
|
1951
|
+
"Patch gate is skipped when the diff has no executable lines (tests-only, docs, comments)."
|
|
1952
|
+
)
|
|
1953
|
+
);
|
|
1954
|
+
lines.push("");
|
|
1934
1955
|
lines.push(tip("re-check anytime: tested doctor"));
|
|
1935
1956
|
lines.push("");
|
|
1936
1957
|
return lines.join("\n");
|
|
@@ -2196,8 +2217,8 @@ function coloredPctCell(pct3, width = 6) {
|
|
|
2196
2217
|
function formatMetricRow(label, pct3, covered, executable, emptyNote) {
|
|
2197
2218
|
const labelPad = label.padEnd(8);
|
|
2198
2219
|
if (executable === 0) {
|
|
2199
|
-
const note = emptyNote ??
|
|
2200
|
-
return ` ${labelPad} ${dim("-".padEnd(6))} ${
|
|
2220
|
+
const note = emptyNote ?? EMPTY_PATCH_REASON;
|
|
2221
|
+
return ` ${labelPad} ${dim("-".padEnd(6))} ${note}`;
|
|
2201
2222
|
}
|
|
2202
2223
|
const pctStr = coloredPctCell(pct3);
|
|
2203
2224
|
const bar = metricBar(pct3);
|
|
@@ -2216,7 +2237,7 @@ function formatHuman(out, opts = {}) {
|
|
|
2216
2237
|
out.patch.pct,
|
|
2217
2238
|
out.patch.covered,
|
|
2218
2239
|
out.patch.executable,
|
|
2219
|
-
|
|
2240
|
+
EMPTY_PATCH_REASON
|
|
2220
2241
|
)
|
|
2221
2242
|
);
|
|
2222
2243
|
const projectRow = formatMetricRow(
|
|
@@ -2229,7 +2250,7 @@ function formatHuman(out, opts = {}) {
|
|
|
2229
2250
|
if (opts.thresholds) {
|
|
2230
2251
|
const patchPass = out.patch.pct >= opts.thresholds.patch;
|
|
2231
2252
|
const projectPass = out.project.pct >= opts.thresholds.project;
|
|
2232
|
-
const patchOk = out.patch
|
|
2253
|
+
const patchOk = isEmptyPatch(out.patch) ? true : patchPass;
|
|
2233
2254
|
const overall = patchOk && projectPass;
|
|
2234
2255
|
const details = [];
|
|
2235
2256
|
if (!patchOk) {
|
|
@@ -2240,7 +2261,7 @@ function formatHuman(out, opts = {}) {
|
|
|
2240
2261
|
`project ${out.project.pct.toFixed(1)}% < ${opts.thresholds.project}%`
|
|
2241
2262
|
);
|
|
2242
2263
|
}
|
|
2243
|
-
const detail = details.length > 0 ? dim(` ${details.join("; ")}`) : "";
|
|
2264
|
+
const detail = details.length > 0 ? dim(` ${details.join("; ")}`) : isEmptyPatch(out.patch) ? dim(` ${EMPTY_PATCH_REASON}`) : "";
|
|
2244
2265
|
lines.push(
|
|
2245
2266
|
` ${"Gate".padEnd(8)} ${overall ? badge("pass") : badge("fail")}${detail}`
|
|
2246
2267
|
);
|
|
@@ -2253,12 +2274,15 @@ function formatHuman(out, opts = {}) {
|
|
|
2253
2274
|
);
|
|
2254
2275
|
}
|
|
2255
2276
|
}
|
|
2256
|
-
if (out.
|
|
2277
|
+
if (isEmptyPatch(out.patch)) {
|
|
2278
|
+
lines.push("");
|
|
2279
|
+
lines.push(dim("No executable lines in the patch \u2014 patch gate does not apply."));
|
|
2280
|
+
} else if (out.files.length > 0) {
|
|
2257
2281
|
lines.push("");
|
|
2258
2282
|
lines.push(heading("Files in diff:"));
|
|
2259
2283
|
const anyPatch = out.files.some((f) => f.patchCoverage !== null);
|
|
2260
2284
|
if (!anyPatch) {
|
|
2261
|
-
lines.push(dim(
|
|
2285
|
+
lines.push(dim(` (project coverage \u2014 ${EMPTY_PATCH_REASON})`));
|
|
2262
2286
|
}
|
|
2263
2287
|
for (const f of out.files) {
|
|
2264
2288
|
const hasPatch = f.patchCoverage !== null;
|
|
@@ -2344,7 +2368,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
2344
2368
|
const projectPct = diff.project.pct;
|
|
2345
2369
|
const patchThreshold = config.thresholds.patch;
|
|
2346
2370
|
const projectThreshold = config.thresholds.project;
|
|
2347
|
-
const patchSkipped = diff.patch
|
|
2371
|
+
const patchSkipped = isEmptyPatch(diff.patch);
|
|
2348
2372
|
const patchPass = patchSkipped ? true : patchPct >= patchThreshold;
|
|
2349
2373
|
const projectPass = projectPct >= projectThreshold;
|
|
2350
2374
|
const overall = patchPass && projectPass ? "pass" : "fail";
|
|
@@ -2355,10 +2379,11 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
2355
2379
|
pct: patchPct,
|
|
2356
2380
|
threshold: patchThreshold,
|
|
2357
2381
|
pass: patchPass,
|
|
2358
|
-
...patchSkipped ? { skipped: true } : {}
|
|
2382
|
+
...patchSkipped ? { skipped: true, reason: EMPTY_PATCH_REASON } : {}
|
|
2359
2383
|
},
|
|
2360
2384
|
project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
|
|
2361
|
-
overall
|
|
2385
|
+
overall,
|
|
2386
|
+
...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
|
|
2362
2387
|
};
|
|
2363
2388
|
return {
|
|
2364
2389
|
skipped: false,
|
|
@@ -2377,7 +2402,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
2377
2402
|
lines.push("");
|
|
2378
2403
|
if (patchSkipped) {
|
|
2379
2404
|
lines.push(
|
|
2380
|
-
` ${"Patch".padEnd(8)} ${dim("-")} ${
|
|
2405
|
+
` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
|
|
2381
2406
|
);
|
|
2382
2407
|
} else {
|
|
2383
2408
|
lines.push(formatMetricLine("Patch", patchPct, patchThreshold, patchPass));
|
|
@@ -2385,10 +2410,17 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
|
|
|
2385
2410
|
lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
|
|
2386
2411
|
if (overall === "fail") {
|
|
2387
2412
|
lines.push("");
|
|
2413
|
+
if (patchSkipped) {
|
|
2414
|
+
lines.push(dim("No executable lines in the patch \u2014 patch gate skipped."));
|
|
2415
|
+
}
|
|
2388
2416
|
lines.push(tip("add tests for uncovered ranges: tested diff"));
|
|
2389
2417
|
} else {
|
|
2390
2418
|
lines.push("");
|
|
2391
|
-
lines.push(
|
|
2419
|
+
lines.push(
|
|
2420
|
+
dim(
|
|
2421
|
+
patchSkipped ? "No executable lines in the patch \u2014 patch gate skipped. Project threshold met." : "thresholds met"
|
|
2422
|
+
)
|
|
2423
|
+
);
|
|
2392
2424
|
}
|
|
2393
2425
|
lines.push("");
|
|
2394
2426
|
return {
|
|
@@ -2561,7 +2593,7 @@ function createProgram() {
|
|
|
2561
2593
|
"Agent loop:",
|
|
2562
2594
|
" tested setup \u2192 tested run \u2192 tested diff \u2192 tested check \u2192 tested push --pr <n>"
|
|
2563
2595
|
].join("\n")
|
|
2564
|
-
).version("0.1.
|
|
2596
|
+
).version("0.1.1");
|
|
2565
2597
|
registerSetupCommand(program2);
|
|
2566
2598
|
registerDoctorCommand(program2);
|
|
2567
2599
|
registerInitCommand(program2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tested/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://tested.dev",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"packageManager": "pnpm@11.3.0",
|
|
19
19
|
"bin": {
|
|
20
20
|
"tested": "./dist/tested.js",
|
|
21
|
-
"td": "./dist/
|
|
21
|
+
"td": "./dist/tested.js"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|