probatio 0.1.0 → 0.1.3
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 +50 -14
- package/dist/cli.js +101 -53
- package/dist/contract.js +7 -5
- package/dist/mcp.js +23 -0
- package/dist/mutate/child-lines.mjs +34 -0
- package/dist/mutate/coverage-map.js +75 -18
- package/dist/mutate/generate.js +25 -4
- package/dist/mutate/mocha-coverage.cjs +71 -0
- package/dist/mutate/node-batch-loader.mjs +85 -0
- package/dist/mutate/node-batch.mjs +130 -0
- package/dist/mutate/node-coverage.mjs +37 -180
- package/dist/mutate/precise-lines.mjs +188 -0
- package/dist/mutate/run.js +319 -47
- package/dist/mutate/rust-coverage.js +4 -19
- package/dist/mutate/sealed.js +40 -1
- package/dist/mutate/suite-decision.js +1 -1
- package/dist/mutate/suites.js +88 -12
- package/dist/mutate/tally.js +38 -1
- package/dist/node-version.js +13 -0
- package/dist/swarm/queue.js +13 -7
- package/dist/verify/change.js +71 -2
- package/package.json +6 -4
package/dist/mutate/suites.js
CHANGED
|
@@ -386,8 +386,8 @@ async function launch(pkg, spec, files, pattern, names, timeoutMs, env, hooks) {
|
|
|
386
386
|
if (spec.kind === "swift")
|
|
387
387
|
return tag(runSwift(pkg, timeoutMs, env), spec.command, wantsNames, selection);
|
|
388
388
|
if (spec.kind === "maven") {
|
|
389
|
-
const tests =
|
|
390
|
-
const args = ["-B", "test", ...(tests.length > 0 ? [`-Dtest=${tests
|
|
389
|
+
const tests = surefireArg(uniqueSurefire(selection));
|
|
390
|
+
const args = ["-B", "test", ...(tests.length > 0 ? [`-Dtest=${tests}`] : [])];
|
|
391
391
|
const whole = wantsNames && tests.length === 0;
|
|
392
392
|
// Surefire leaves the previous run's XML in place. A confirm that did not
|
|
393
393
|
// re-execute a class would otherwise read that class as failed again.
|
|
@@ -400,8 +400,11 @@ async function launch(pkg, spec, files, pattern, names, timeoutMs, env, hooks) {
|
|
|
400
400
|
}
|
|
401
401
|
if (spec.kind === "dotnet-exe")
|
|
402
402
|
return tag(runDotnetExe(pkg, spec, files, timeoutMs, env), spec.command, wantsNames, selection);
|
|
403
|
-
if (spec.kind === "mocha")
|
|
404
|
-
|
|
403
|
+
if (spec.kind === "mocha") {
|
|
404
|
+
const grep = selection.length > 0 ? selection.map(escapeRegExp).join("|") : "";
|
|
405
|
+
const command = grep ? `${spec.command} --grep ${JSON.stringify(grep)}` : spec.command;
|
|
406
|
+
return tag(runMocha(pkg, files, timeoutMs, env, selection), command, false, selection);
|
|
407
|
+
}
|
|
405
408
|
// A tty is only for a suite that runs `docker run -it` (tini). Everything else is bash.
|
|
406
409
|
if (spec.kind === "script") {
|
|
407
410
|
const launched = scriptInvocation(pkg);
|
|
@@ -465,12 +468,47 @@ function goRunFilter(names, pattern) {
|
|
|
465
468
|
return null;
|
|
466
469
|
return `^(${names.map(escapeRegExp).join("|")})$`;
|
|
467
470
|
}
|
|
471
|
+
function uniqueSurefire(names) {
|
|
472
|
+
const out = [];
|
|
473
|
+
const seen = new Set();
|
|
474
|
+
for (const name of names) {
|
|
475
|
+
const selector = surefireTest(name);
|
|
476
|
+
if (!selector || seen.has(selector))
|
|
477
|
+
continue;
|
|
478
|
+
seen.add(selector);
|
|
479
|
+
out.push(selector);
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
/** Surefire 2.12 splits `-Dtest` on commas and then reads `#method+method`. `Class#a,Class#b` runs only `a`. */
|
|
484
|
+
export function surefireArg(tests) {
|
|
485
|
+
const groups = new Map();
|
|
486
|
+
for (const test of tests) {
|
|
487
|
+
const hash = test.indexOf("#");
|
|
488
|
+
const klass = hash === -1 ? test : test.slice(0, hash);
|
|
489
|
+
const method = hash === -1 ? "" : test.slice(hash + 1);
|
|
490
|
+
const methods = groups.get(klass);
|
|
491
|
+
if (methods) {
|
|
492
|
+
if (method)
|
|
493
|
+
methods.push(method);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
groups.set(klass, method ? [method] : []);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return [...groups.entries()]
|
|
500
|
+
.map(([klass, methods]) => (methods.length > 0 ? `${klass}#${methods.join("+")}` : klass))
|
|
501
|
+
.join(",");
|
|
502
|
+
}
|
|
468
503
|
function surefireTest(name) {
|
|
469
504
|
const leaf = name.split("::").pop() ?? name;
|
|
470
505
|
const dot = leaf.lastIndexOf(".");
|
|
471
506
|
const body = dot <= 0 ? leaf : `${leaf.slice(0, dot)}#${leaf.slice(dot + 1)}`;
|
|
472
|
-
// testShut[0]
|
|
473
|
-
|
|
507
|
+
// testShut[0] matches nothing. A bare testShut matches on Surefire 3.6 and runs
|
|
508
|
+
// zero tests on 3.5. testShut* matches every invocation on both. method(Parser)[1] keeps the types.
|
|
509
|
+
const invocation = /\[[^\]]*\]$/.test(body);
|
|
510
|
+
const method = body.replace(/\[[^\]]*\]$/, "");
|
|
511
|
+
return invocation ? `${method}*` : method;
|
|
474
512
|
}
|
|
475
513
|
function escapeRegExp(value) {
|
|
476
514
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -481,18 +519,46 @@ function runShell(pkg, command, timeoutMs, env) {
|
|
|
481
519
|
const child = /\b(java|javac|mvn)\b/.test(command) ? javaEnv(env) : env;
|
|
482
520
|
return spawnCollected("sh", ["-c", command], pkg, { ...child, PYTHONDONTWRITEBYTECODE: "1" }, timeoutMs);
|
|
483
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* Node's `--test-timeout` caps the file, not only a test that left the option unset.
|
|
524
|
+
* A test that sets a longer `timeout` is cancelled, and the failure name is the file path.
|
|
525
|
+
* The flag stays at least that long, and it stays inside the suite cap.
|
|
526
|
+
*/
|
|
527
|
+
export function nodeTestTimeoutMs(pkg, files, testTimeoutMs, suiteTimeoutMs) {
|
|
528
|
+
let declared = 0;
|
|
529
|
+
for (const rel of files) {
|
|
530
|
+
let text = "";
|
|
531
|
+
try {
|
|
532
|
+
text = readFileSync(path.join(pkg, rel), "utf8");
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
for (const match of text.matchAll(/timeout\s*:\s*(\d[\d_]*)/g)) {
|
|
538
|
+
const value = Number(match[1].replaceAll("_", ""));
|
|
539
|
+
if (Number.isFinite(value) && value > declared)
|
|
540
|
+
declared = value;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const floor = Number.isFinite(testTimeoutMs) && testTimeoutMs > 0 ? testTimeoutMs : 0;
|
|
544
|
+
let chosen = Math.max(floor, declared);
|
|
545
|
+
if (Number.isFinite(suiteTimeoutMs) && suiteTimeoutMs > 0 && chosen > suiteTimeoutMs)
|
|
546
|
+
chosen = suiteTimeoutMs;
|
|
547
|
+
return chosen > 0 ? chosen : floor;
|
|
548
|
+
}
|
|
484
549
|
async function runNode(pkg, files, pattern, timeoutMs, env, hooks) {
|
|
485
550
|
const tsx = path.join(pkg, "node_modules", ".bin", "tsx");
|
|
486
551
|
const bin = existsSync(tsx) ? tsx : process.execPath;
|
|
487
552
|
const prefix = existsSync(tsx) ? [] : ["--experimental-strip-types"];
|
|
488
553
|
const hook = env.PROBATIO_COVERAGE_MAP ? fileURLToPath(new URL("./node-coverage.mjs", import.meta.url)) : null;
|
|
554
|
+
const fileTimeout = nodeTestTimeoutMs(pkg, files, hooks.testTimeoutMs, timeoutMs);
|
|
489
555
|
const result = await spawnCollected(bin, [
|
|
490
556
|
...prefix,
|
|
491
557
|
...(hook ? ["--import", hook] : []),
|
|
492
558
|
"--test",
|
|
493
559
|
`--test-reporter=${hooks.reporterPath}`,
|
|
494
560
|
`--test-concurrency=${hooks.concurrency}`,
|
|
495
|
-
`--test-timeout=${
|
|
561
|
+
`--test-timeout=${fileTimeout}`,
|
|
496
562
|
...(pattern ? [`--test-name-pattern=${pattern}`] : []),
|
|
497
563
|
...files,
|
|
498
564
|
], pkg, env, timeoutMs);
|
|
@@ -545,7 +611,7 @@ function runPython(pkg, files, pattern, timeoutMs, env, script, nodeIds) {
|
|
|
545
611
|
const targets = nodeIds && nodeIds.length > 0 ? nodeIds : files;
|
|
546
612
|
return spawnCollected("python3", [reporter, ...targets, ...(pattern ? ["--pattern", pattern] : [])], pkg, next, timeoutMs);
|
|
547
613
|
}
|
|
548
|
-
async function runMocha(pkg, files, timeoutMs, env) {
|
|
614
|
+
async function runMocha(pkg, files, timeoutMs, env, names = null) {
|
|
549
615
|
const mocha = ["mocha.js", "mocha"]
|
|
550
616
|
.map((name) => path.join(pkg, "node_modules", "mocha", "bin", name))
|
|
551
617
|
.find((file) => existsSync(file));
|
|
@@ -556,6 +622,10 @@ async function runMocha(pkg, files, timeoutMs, env) {
|
|
|
556
622
|
return prepared;
|
|
557
623
|
const config = readMochaConfig(pkg);
|
|
558
624
|
const args = [mocha, ...buildMochaArgs(pkg, files)];
|
|
625
|
+
if (names && names.length > 0)
|
|
626
|
+
args.push("--grep", names.map(escapeRegExp).join("|"));
|
|
627
|
+
if (env.PROBATIO_COVERAGE_MAP)
|
|
628
|
+
args.push("--require", fileURLToPath(new URL("./mocha-coverage.cjs", import.meta.url)));
|
|
559
629
|
if (config && config.require.includes("esm")) {
|
|
560
630
|
// The `esm` loader throws on current Node before any test runs. Drop that one require and keep the rest of the project's mocha config.
|
|
561
631
|
const requires = config.require.filter((name) => name !== "esm");
|
|
@@ -729,7 +799,8 @@ function scriptInvocation(pkg) {
|
|
|
729
799
|
if (!suiteNeedsTty(pkg))
|
|
730
800
|
return { bin: "bash", args: ["run_tests.sh"], command: "bash run_tests.sh" };
|
|
731
801
|
if (gnuScript()) {
|
|
732
|
-
|
|
802
|
+
// -e returns the child status. Without it, util-linux script exits 0 and a failing suite looks green.
|
|
803
|
+
const args = ["-q", "-e", "-c", "bash run_tests.sh", "/dev/null"];
|
|
733
804
|
return { bin: "script", args, command: shown("script", args) };
|
|
734
805
|
}
|
|
735
806
|
const args = ["-q", "/dev/null", "bash", "run_tests.sh"];
|
|
@@ -1368,6 +1439,11 @@ function mavenReport(pkg, text, code) {
|
|
|
1368
1439
|
}
|
|
1369
1440
|
return parseSurefire(pkg) ?? parseMavenText(text);
|
|
1370
1441
|
}
|
|
1442
|
+
/** An attribute on the opening tag. `classname` must not be read as `name`. */
|
|
1443
|
+
export function surefireAttr(fragment, key) {
|
|
1444
|
+
const open = fragment.slice(0, fragment.indexOf(">") === -1 ? fragment.length : fragment.indexOf(">"));
|
|
1445
|
+
return new RegExp(`(?:^|\\s)${key}="([^"]*)"`).exec(open)?.[1] ?? "";
|
|
1446
|
+
}
|
|
1371
1447
|
function parseSurefire(pkg) {
|
|
1372
1448
|
const dir = path.join(pkg, "target", "surefire-reports");
|
|
1373
1449
|
if (!existsSync(dir))
|
|
@@ -1379,11 +1455,11 @@ function parseSurefire(pkg) {
|
|
|
1379
1455
|
continue;
|
|
1380
1456
|
const xml = readFileSync(path.join(dir, name), "utf8");
|
|
1381
1457
|
for (const part of xml.split("<testcase ").slice(1)) {
|
|
1382
|
-
const test =
|
|
1383
|
-
const klass =
|
|
1458
|
+
const test = surefireAttr(part, "name");
|
|
1459
|
+
const klass = surefireAttr(part, "classname");
|
|
1384
1460
|
if (!test)
|
|
1385
1461
|
continue;
|
|
1386
|
-
const id = `${klass}.${test}
|
|
1462
|
+
const id = klass ? `${klass}.${test}` : test;
|
|
1387
1463
|
names.push(id);
|
|
1388
1464
|
if (part.includes("<failure") || part.includes("<error"))
|
|
1389
1465
|
failed.push({ name: id, file: klass, line: 0 });
|
package/dist/mutate/tally.js
CHANGED
|
@@ -105,7 +105,7 @@ export function tallyRun(outDir) {
|
|
|
105
105
|
command: "mutate.tally",
|
|
106
106
|
summary,
|
|
107
107
|
next,
|
|
108
|
-
nextCall:
|
|
108
|
+
nextCall: resumeArgv(outDir, keep),
|
|
109
109
|
keep,
|
|
110
110
|
drop,
|
|
111
111
|
gaps,
|
|
@@ -117,6 +117,43 @@ export function tallyRun(outDir) {
|
|
|
117
117
|
writeFileSync(fullPath, `${JSON.stringify(complete, null, 2)}\n`);
|
|
118
118
|
return { ...complete, gaps: shown, rest: gaps.length - shown.length };
|
|
119
119
|
}
|
|
120
|
+
function resumeArgv(outDir, keep) {
|
|
121
|
+
if (keep.length === 0)
|
|
122
|
+
return null;
|
|
123
|
+
let stored = {};
|
|
124
|
+
try {
|
|
125
|
+
stored = JSON.parse(readFileSync(path.join(outDir, "run.json"), "utf8"));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
stored = {};
|
|
129
|
+
}
|
|
130
|
+
const argv = ["mutate", "run"];
|
|
131
|
+
if (stored.package)
|
|
132
|
+
argv.push("--package", stored.package);
|
|
133
|
+
for (const dir of stored.patches ?? [])
|
|
134
|
+
argv.push("--patches", dir);
|
|
135
|
+
argv.push("--out", stored.out || outDir);
|
|
136
|
+
if (stored.repo)
|
|
137
|
+
argv.push("--repo", stored.repo);
|
|
138
|
+
if (stored.commit)
|
|
139
|
+
argv.push("--commit", stored.commit);
|
|
140
|
+
if (typeof stored.workers === "number")
|
|
141
|
+
argv.push("--workers", String(stored.workers));
|
|
142
|
+
if (stored.confirm === false)
|
|
143
|
+
argv.push("--no-confirm");
|
|
144
|
+
if (stored.testsDir && stored.testsDir !== "tests")
|
|
145
|
+
argv.push("--tests-dir", stored.testsDir);
|
|
146
|
+
if (typeof stored.suiteTimeoutMs === "number")
|
|
147
|
+
argv.push("--suite-timeout-ms", String(stored.suiteTimeoutMs));
|
|
148
|
+
if (typeof stored.testTimeoutMs === "number")
|
|
149
|
+
argv.push("--test-timeout-ms", String(stored.testTimeoutMs));
|
|
150
|
+
if (stored.direction && stored.direction !== "auto")
|
|
151
|
+
argv.push("--direction", stored.direction);
|
|
152
|
+
if (stored.suiteCommand)
|
|
153
|
+
argv.push("--suite-command", stored.suiteCommand);
|
|
154
|
+
argv.push(...keep.flatMap((id) => ["--only-test", id]));
|
|
155
|
+
return { argv };
|
|
156
|
+
}
|
|
120
157
|
function readResults(dir) {
|
|
121
158
|
const out = [];
|
|
122
159
|
for (const name of readdirSync(dir)) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** `--experimental-strip-types` needs this. Older Node is refused before a command runs. */
|
|
2
|
+
export const MIN_NODE = "22.6";
|
|
3
|
+
export function nodeTooOld(version) {
|
|
4
|
+
const match = /^(\d+)\.(\d+)/.exec(version);
|
|
5
|
+
const required = `Node ${version} is too old. Probatio needs Node ${MIN_NODE} or newer.`;
|
|
6
|
+
if (!match)
|
|
7
|
+
return required;
|
|
8
|
+
const major = Number(match[1]);
|
|
9
|
+
const minor = Number(match[2]);
|
|
10
|
+
if (major > 22 || (major === 22 && minor >= 6))
|
|
11
|
+
return null;
|
|
12
|
+
return required;
|
|
13
|
+
}
|
package/dist/swarm/queue.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { loadSealed } from "../memory/state.js";
|
|
4
|
+
/** One path segment. Rejects slashes and `..` so a claim cannot leave the queue directory. */
|
|
5
|
+
export function safeFilePart(value) {
|
|
6
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value) || value === "." || value === "..")
|
|
7
|
+
return null;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
4
10
|
export function seedQueue(root, ids) {
|
|
5
11
|
const sealed = new Set(loadSealed(root));
|
|
6
12
|
const queue = path.join(root, "queue");
|
|
7
13
|
mkdirSync(queue, { recursive: true });
|
|
8
14
|
const written = [];
|
|
9
15
|
for (const id of [...ids].sort()) {
|
|
10
|
-
if (sealed.has(id))
|
|
16
|
+
if (sealed.has(id) || !safeFilePart(id))
|
|
11
17
|
continue;
|
|
12
18
|
const dest = path.join(queue, `${id}.json`);
|
|
13
19
|
if (existsSync(dest))
|
|
@@ -19,23 +25,23 @@ export function seedQueue(root, ids) {
|
|
|
19
25
|
}
|
|
20
26
|
/** Claim by atomic rename. A second claim fails because the queue file is already gone. */
|
|
21
27
|
export function claimItem(root, id, agent, now, leaseMs) {
|
|
28
|
+
if (!safeFilePart(id) || !safeFilePart(agent))
|
|
29
|
+
return { ok: false, summary: "agent and id must be a single file name.", file: null };
|
|
22
30
|
const source = path.join(root, "queue", `${id}.json`);
|
|
23
31
|
const claimed = path.join(root, "claimed");
|
|
24
32
|
const dest = path.join(claimed, `${agent}-${id}.json`);
|
|
25
33
|
if (!existsSync(source))
|
|
26
34
|
return { ok: false, summary: `${id} is not in the queue.`, file: null };
|
|
27
35
|
mkdirSync(claimed, { recursive: true });
|
|
28
|
-
|
|
29
|
-
body.id = id;
|
|
30
|
-
body.agent = agent;
|
|
31
|
-
body.leasedUntil = now + leaseMs;
|
|
32
|
-
writeFileSync(source, `${JSON.stringify(body)}\n`);
|
|
36
|
+
// Rename first. Writing the body before the rename lets the other claim overwrite the winner.
|
|
33
37
|
try {
|
|
34
38
|
renameSync(source, dest);
|
|
35
39
|
}
|
|
36
40
|
catch {
|
|
37
41
|
return { ok: false, summary: `${id} was claimed by someone else.`, file: null };
|
|
38
42
|
}
|
|
43
|
+
const body = { id, agent, leasedUntil: now + leaseMs };
|
|
44
|
+
writeFileSync(dest, `${JSON.stringify(body)}\n`);
|
|
39
45
|
return { ok: true, summary: `${agent} claimed ${id} until ${body.leasedUntil}.`, file: dest };
|
|
40
46
|
}
|
|
41
47
|
/** Move claims whose lease has expired back to the queue. */
|
|
@@ -53,7 +59,7 @@ export function reapClaims(root, now) {
|
|
|
53
59
|
const body = JSON.parse(readFileSync(file, "utf8"));
|
|
54
60
|
if (typeof body.leasedUntil !== "number" || body.leasedUntil > now)
|
|
55
61
|
continue;
|
|
56
|
-
if (!body.id)
|
|
62
|
+
if (!body.id || !safeFilePart(body.id))
|
|
57
63
|
continue;
|
|
58
64
|
renameSync(file, path.join(queue, `${body.id}.json`));
|
|
59
65
|
returned.push(body.id);
|
package/dist/verify/change.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { SCHEMA_VERSION } from "../contract.js";
|
|
4
4
|
import { asTable } from "../golden/check.js";
|
|
@@ -23,12 +23,18 @@ export async function verifyChange(options) {
|
|
|
23
23
|
const ranges = parseUnified(diff.stdout);
|
|
24
24
|
const names = git(repo, ["diff", "--name-only", "--no-renames", options.base, options.commit, ...scope]);
|
|
25
25
|
const changed = names.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
26
|
+
const status = git(repo, ["status", "--porcelain", ...scope]);
|
|
27
|
+
const dirty = status.stdout.trim().length > 0;
|
|
28
|
+
if (dirty && (options.base === options.commit || changed.length === 0)) {
|
|
29
|
+
return fail(options.outDir, "The working tree has uncommitted changes. verify-change did not score them. Commit the edit, or pass --base and --commit that name different commits with a diff.");
|
|
30
|
+
}
|
|
26
31
|
const goldenContract = goldenDiffs(repo, options.base, options.commit, changed);
|
|
27
32
|
const packageFiles = [];
|
|
28
33
|
const chosen = [];
|
|
34
|
+
const libImported = testsImportLib(packageDir);
|
|
29
35
|
for (const [file, lines] of ranges) {
|
|
30
36
|
const rel = toPackage(prefix, file);
|
|
31
|
-
if (!rel || !rel
|
|
37
|
+
if (!rel || !isVerifySource(rel, packageDir, libImported))
|
|
32
38
|
continue;
|
|
33
39
|
packageFiles.push(rel);
|
|
34
40
|
const text = showFile(repo, options.commit, file);
|
|
@@ -269,3 +275,66 @@ function toPackage(prefix, file) {
|
|
|
269
275
|
return null;
|
|
270
276
|
return file.slice(prefix.length + 1);
|
|
271
277
|
}
|
|
278
|
+
const SKIPPED_SOURCE = new Set(["node_modules", "dist", "docs", "tests", "test"]);
|
|
279
|
+
/** src/, a lib/ tree the tests import, or a Go file beside go.mod. Tests, docs, dist, and node_modules stay out. */
|
|
280
|
+
function isVerifySource(rel, packageDir, libImported) {
|
|
281
|
+
if (rel.endsWith(".d.ts"))
|
|
282
|
+
return false;
|
|
283
|
+
const parts = rel.split("/");
|
|
284
|
+
if (parts.some((part) => SKIPPED_SOURCE.has(part)))
|
|
285
|
+
return false;
|
|
286
|
+
if (rel.startsWith("src/"))
|
|
287
|
+
return true;
|
|
288
|
+
if (libImported && rel.startsWith("lib/"))
|
|
289
|
+
return true;
|
|
290
|
+
if (!rel.includes("/") && rel.endsWith(".go") && !rel.endsWith("_test.go") && existsSync(path.join(packageDir, "go.mod")))
|
|
291
|
+
return true;
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
function testsImportLib(packageDir) {
|
|
295
|
+
const pattern = /(?:require\s*\(\s*|from\s+)['"](?:@[^'"]+\/)?(?:\.\.\/|\.\/)*lib\//;
|
|
296
|
+
for (const dirName of ["tests", "test"]) {
|
|
297
|
+
const dir = path.join(packageDir, dirName);
|
|
298
|
+
if (!existsSync(dir))
|
|
299
|
+
continue;
|
|
300
|
+
if (walkText(dir).some((text) => pattern.test(text)))
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
function walkText(dir) {
|
|
306
|
+
const out = [];
|
|
307
|
+
const visit = (current) => {
|
|
308
|
+
let entries;
|
|
309
|
+
try {
|
|
310
|
+
entries = readdirSync(current);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
for (const name of entries) {
|
|
316
|
+
if (name === "node_modules" || name === "dist")
|
|
317
|
+
continue;
|
|
318
|
+
const full = path.join(current, name);
|
|
319
|
+
let info;
|
|
320
|
+
try {
|
|
321
|
+
info = statSync(full);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (info.isDirectory())
|
|
327
|
+
visit(full);
|
|
328
|
+
else if (info.isFile() && info.size < 1_000_000) {
|
|
329
|
+
try {
|
|
330
|
+
out.push(readFileSync(full, "utf8"));
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// An unreadable test file does not count as an import of lib/.
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
visit(dir);
|
|
339
|
+
return out;
|
|
340
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "probatio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Mutation runner for AI agents. Scores a test suite by the bugs it catches.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"probatio": "dist/cli.js"
|
|
7
|
+
"probatio": "dist/cli.js",
|
|
8
|
+
"probatio-mcp": "dist/mcp.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist"
|
|
@@ -19,12 +20,13 @@
|
|
|
19
20
|
"url": "https://github.com/IronAdamant/Probatio/issues"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
|
-
"build": "tsc -p tsconfig.json && mkdir -p dist/mutate && cp src/mutate/fail-reporter.mjs src/mutate/node-coverage.mjs src/mutate/py-reporter.py src/mutate/py-pytest-reporter.py src/mutate/probatio-jacoco-run.java dist/mutate/",
|
|
23
|
+
"build": "tsc -p tsconfig.json && mkdir -p dist/mutate && cp src/mutate/fail-reporter.mjs src/mutate/node-coverage.mjs src/mutate/precise-lines.mjs src/mutate/child-lines.mjs src/mutate/mocha-coverage.cjs src/mutate/node-batch.mjs src/mutate/node-batch-loader.mjs src/mutate/py-reporter.py src/mutate/py-pytest-reporter.py src/mutate/probatio-jacoco-run.java dist/mutate/",
|
|
24
|
+
"prepack": "npm run build",
|
|
23
25
|
"test": "tsx --test tests/*.test.ts",
|
|
24
26
|
"probatio": "tsx src/cli.ts"
|
|
25
27
|
},
|
|
26
28
|
"engines": {
|
|
27
|
-
"node": ">=22"
|
|
29
|
+
"node": ">=22.6"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"typescript": "^5.7.2"
|