@feigi/fleet-ctl 3.21.3 → 3.21.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -72,6 +72,22 @@ test("the crash populations are told apart off refutersDispatched in the recipes
|
|
|
72
72
|
assert.match(recipes, /select\(\.refutersDispatched == 0\)/);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
// The recipes' comments are prose no ruling paragraph's pin reads. The one on
|
|
76
|
+
// the testEnvironment recipe says what that field is evidence about: the
|
|
77
|
+
// review's one shared test run, which no specialist returns a copy of, so no
|
|
78
|
+
// recipe comment names a specialist `test_run` field that no longer exists.
|
|
79
|
+
test("the testEnvironment recipe calls it evidence about the shared test run, and no recipe comment names a test_run field", () => {
|
|
80
|
+
const recipes = section().match(/^jq .*$/gm) ?? [];
|
|
81
|
+
const line = recipes.find((l) => l.startsWith("jq -r '.testEnvironment' "));
|
|
82
|
+
assert.ok(line, "the result-file section no longer has a `jq -r '.testEnvironment'` recipe");
|
|
83
|
+
assert.match(line, phrase("# what the shared test run is evidence about"), "the testEnvironment recipe's comment no longer names the shared test run");
|
|
84
|
+
// Every line of every fenced recipe block, not only the `jq ` ones: the `R=`
|
|
85
|
+
// line and any other line the block grows carry comments too.
|
|
86
|
+
const fenced = [...section().matchAll(/^```bash\n([\s\S]*?)^```$/gm)].flatMap((m) => m[1].split("\n"));
|
|
87
|
+
assert.ok(fenced.length > 0, "the result-file section lost its fenced recipe block");
|
|
88
|
+
for (const l of fenced) assert.doesNotMatch(l, /test_run/i, `a recipe line names the retired test_run field: ${l}`);
|
|
89
|
+
});
|
|
90
|
+
|
|
75
91
|
test("the fix-applier owns the mutual-exclusion scan, run twice, with a single refuter for a conflicting pair", () => {
|
|
76
92
|
const p = ruling("**Scan the findings against each other for MUTUAL EXCLUSION");
|
|
77
93
|
assert.match(p, phrase("run it twice"));
|
package/scripts/recipe-prove.mjs
CHANGED
|
@@ -38,16 +38,18 @@
|
|
|
38
38
|
// — <workspace> being the directory holding the repository's common git dir,
|
|
39
39
|
// the place derive-testcmd.sh reads it from — and then read back through
|
|
40
40
|
// derive-testcmd.sh for both fields. A cache the reader refuses is rolled
|
|
41
|
-
// back to whatever stood before,
|
|
42
|
-
// what a usable cache is. A failed
|
|
41
|
+
// back to whatever stood before, as is one whose read-back is no verdict, so
|
|
42
|
+
// the reader stays the one authority on what a usable cache is. A failed
|
|
43
|
+
// proof never touches an existing cache.
|
|
43
44
|
//
|
|
44
45
|
// Exit 0: proven, cache written (its path and contents on stdout).
|
|
45
46
|
// Exit 1: NOT PROVEN — no cache written; the reason on stderr.
|
|
46
47
|
// Exit 2: no verdict on the proof — it could not be attempted (usage, not a
|
|
47
48
|
// repository, origin/main missing, the worktree could not be made), or
|
|
48
49
|
// a git or filesystem fault stopped it before the cache was settled
|
|
49
|
-
// (a git command could not be started
|
|
50
|
-
//
|
|
50
|
+
// (a git command could not be started, the reader's own git
|
|
51
|
+
// included, or the reader's `sh`; the cache or its temp file could
|
|
52
|
+
// not be written).
|
|
51
53
|
|
|
52
54
|
import { spawnSync } from "node:child_process";
|
|
53
55
|
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -212,7 +214,11 @@ function prove(o, wt, logs) {
|
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
// Write, then read back through the one reader. A cache it refuses is undone:
|
|
215
|
-
// the prior bytes restored, or the file removed when none stood before.
|
|
217
|
+
// the prior bytes restored, or the file removed when none stood before. The
|
|
218
|
+
// cache is undone just the same when the read-back is no verdict: an `sh` that
|
|
219
|
+
// could not be started read nothing, and the reader runs git itself and reads
|
|
220
|
+
// a git that will not start as no repository at all, so its refusal is a
|
|
221
|
+
// verdict on what was proven only while git still starts.
|
|
216
222
|
function writeCache(cache, recipe, repo) {
|
|
217
223
|
mkdirSync(dirname(cache), { recursive: true });
|
|
218
224
|
const prior = existsSync(cache) ? readFileSync(cache) : null;
|
|
@@ -226,11 +232,17 @@ function writeCache(cache, recipe, repo) {
|
|
|
226
232
|
}
|
|
227
233
|
for (const field of ["install", "test"]) {
|
|
228
234
|
const r = spawnSync("sh", [READER, repo, field], { env: ENV, encoding: "utf8" });
|
|
229
|
-
if (r.status
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
if (r.status === 0 && r.stdout.replace(/\n$/, "") === recipe[field]) continue;
|
|
236
|
+
if (prior === null) unlinkSync(cache);
|
|
237
|
+
else writeFileSync(cache, prior);
|
|
238
|
+
if (r.error && !r.pid) throw cannot(`could not start sh to read the Recipe cache back: ${r.error.message}`);
|
|
239
|
+
try {
|
|
240
|
+
git(["--version"]);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
if (!(e instanceof Refusal)) throw e;
|
|
243
|
+
throw cannot(`${e.message} — the Recipe cache reader runs git, so its refusal is no verdict on what was proven`);
|
|
233
244
|
}
|
|
245
|
+
throw notProven(`the Recipe cache reader refuses what was proven: ${(r.stderr || r.stdout).trim()}`);
|
|
234
246
|
}
|
|
235
247
|
}
|
|
236
248
|
|
|
@@ -442,6 +442,54 @@ test("a git that stops starting during cleanup does not replace the proof's own
|
|
|
442
442
|
assert.match(r.err, /skipped git worktree prune: could not start git/);
|
|
443
443
|
});
|
|
444
444
|
|
|
445
|
+
// A PATH holding exactly what the proof and the Recipe cache reader run — `sh`,
|
|
446
|
+
// a `git` that execs the real one, `node`, and the reader's `mktemp`, `cat`
|
|
447
|
+
// and `rm` — so the Test entrypoint can take git away by deleting it. With the
|
|
448
|
+
// count proof no proof step runs git after the Test entrypoint, so the first
|
|
449
|
+
// thing to meet the missing git is the reader, during the read-back.
|
|
450
|
+
function readerPath() {
|
|
451
|
+
const realGit = execFileSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).trim();
|
|
452
|
+
const bin = tempDir("recipe-prove-path-");
|
|
453
|
+
writeExecStub(join(bin, "git"), `#!/bin/sh\nexec '${realGit}' "$@"\n`);
|
|
454
|
+
symlinkSync("/bin/sh", join(bin, "sh"));
|
|
455
|
+
symlinkSync(process.execPath, join(bin, "node"));
|
|
456
|
+
for (const tool of ["mktemp", "cat", "rm"]) {
|
|
457
|
+
symlinkSync(execFileSync("sh", ["-c", `command -v ${tool}`], { encoding: "utf8" }).trim(), join(bin, tool));
|
|
458
|
+
}
|
|
459
|
+
return bin;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
test("a git that stops starting before the cache is read back is no verdict, never NOT PROVEN — and the prior cache is restored", () => {
|
|
463
|
+
// The control: the same PATH with git left in place is a proof that holds.
|
|
464
|
+
const kept = repo(MAVEN_FILES);
|
|
465
|
+
const ok = prove(kept.dir, ["--install", "true", "--test", "echo 'tests 1'", "--count-line", "tests 1", "--test-count", "1"], { env: { PATH: readerPath() } });
|
|
466
|
+
assert.equal(ok.status, 0, ok.err);
|
|
467
|
+
assert.match(ok.out, /^recipe-prove: PROVEN/);
|
|
468
|
+
|
|
469
|
+
const { dir } = repo(MAVEN_FILES);
|
|
470
|
+
mkdirSync(join(dir, ".fleet"));
|
|
471
|
+
writeFileSync(cachePath(dir), "prior bytes");
|
|
472
|
+
const bin = readerPath();
|
|
473
|
+
const r = prove(dir, ["--install", "true", "--test", `rm '${join(bin, "git")}'; echo 'tests 1'`, "--count-line", "tests 1", "--test-count", "1"], { env: { PATH: bin } });
|
|
474
|
+
assert.equal(r.status, 2, r.err);
|
|
475
|
+
assert.match(r.err, /^recipe-prove: could not start git: .*ENOENT — the Recipe cache reader runs git, so its refusal is no verdict/m);
|
|
476
|
+
assert.doesNotMatch(r.err, /NOT PROVEN/);
|
|
477
|
+
assert.doesNotMatch(r.err, /not a git repository/);
|
|
478
|
+
assert.equal(readFileSync(cachePath(dir), "utf8"), "prior bytes", "a cache the reader never settled is rolled back");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// The reader's own `sh`, taken away the same way: a read-back that never ran
|
|
482
|
+
// read nothing, so it is no verdict either.
|
|
483
|
+
test("an sh that stops starting before the cache is read back is no verdict, never NOT PROVEN — and no cache is left", () => {
|
|
484
|
+
const { dir } = repo(MAVEN_FILES);
|
|
485
|
+
const bin = readerPath();
|
|
486
|
+
const r = prove(dir, ["--install", "true", "--test", `rm '${join(bin, "sh")}'; echo 'tests 1'`, "--count-line", "tests 1", "--test-count", "1"], { env: { PATH: bin } });
|
|
487
|
+
assert.equal(r.status, 2, r.err);
|
|
488
|
+
assert.match(r.err, /^recipe-prove: could not start sh to read the Recipe cache back: .*ENOENT/m);
|
|
489
|
+
assert.doesNotMatch(r.err, /NOT PROVEN/);
|
|
490
|
+
assert.equal(existsSync(cachePath(dir)), false, "a cache the reader never read is rolled back");
|
|
491
|
+
});
|
|
492
|
+
|
|
445
493
|
// Whether the process ignores file modes: root writes into a 0555 directory.
|
|
446
494
|
const IGNORES_MODES = process.getuid?.() === 0 && "root ignores file modes";
|
|
447
495
|
|
|
@@ -401,18 +401,57 @@ test("the returned object carries dimensionsUnrun alongside dimensionsRun", () =
|
|
|
401
401
|
// The note hands every specialist the shared run and tells it not to run the
|
|
402
402
|
// full command itself — and nothing more about the run: the shared run is the
|
|
403
403
|
// single record of it, so no specialist is told to report or copy it anywhere.
|
|
404
|
-
// Every rendering the note has — usable,
|
|
405
|
-
//
|
|
406
|
-
//
|
|
404
|
+
// Every rendering the note has is checked — usable, each reason a run is NOT
|
|
405
|
+
// usable (each reason's own text is interpolated into that paragraph), and
|
|
406
|
+
// failing or cancelled tests for the owner and for a non-owner — because each
|
|
407
|
+
// appends a different paragraph to the same opening one.
|
|
408
|
+
//
|
|
409
|
+
// The no-copy check reads the whole note with its line wrapping flattened, so
|
|
410
|
+
// a request appended after the opening paragraph, or one the hard wrap splits
|
|
411
|
+
// across two lines, is still one sentence to it. It is a bounded word list, not
|
|
412
|
+
// a proof: a request worded with a verb or noun outside the lists passes it.
|
|
413
|
+
// COPY_REQUEST matches three shapes: a copy verb followed within a few words
|
|
414
|
+
// by the run, its counts, its log or its result; a copy verb followed in the
|
|
415
|
+
// same sentence by a destination in the specialist's own output ("in your
|
|
416
|
+
// result", "to the payload"); and the run, counts or log said to "must be"
|
|
417
|
+
// reported, returned, included and the like. The verbs are whole words, so
|
|
418
|
+
// inflected ones never match as written — and must not be widened to
|
|
419
|
+
// inflections, because the "no exit status" rendering's own "reported counts
|
|
420
|
+
// but no exit status" would then match. The word window is what keeps the
|
|
421
|
+
// opening paragraph's "your own copy of the snapshot are still yours to run"
|
|
422
|
+
// green: it uses `copy` as a noun and ends on `run` in the same sentence.
|
|
423
|
+
// Every rendering below must pass it. The run-is-null reason ("returned
|
|
424
|
+
// nothing") is not rendered here: sharedRunNote dereferences run.command, and
|
|
425
|
+
// that text reaches a note only as the "(the test-run agent returned nothing)"
|
|
426
|
+
// error suffix runReview sets.
|
|
427
|
+
const COPY_VERB = "report|copy|echo|record|return|repeat|restate|include|paste|attach|quote|put|add|list";
|
|
428
|
+
const COPY_OBJECT = "run|counts?|logs?|results?";
|
|
429
|
+
const COPY_REQUEST = new RegExp(
|
|
430
|
+
[
|
|
431
|
+
String.raw`\b(?:${COPY_VERB})\b(?:\s+[^\s.]+){0,4}?\s+(?:${COPY_OBJECT})\b`,
|
|
432
|
+
String.raw`\b(?:${COPY_VERB})\b[^.]*\b(?:in|to|into)\s+(?:your|the)\s+(?:\w+\s+)?(?:result|payload|scope_searched|summary|findings?)\b`,
|
|
433
|
+
String.raw`\b(?:${COPY_OBJECT})\b(?:\s+[^\s.]+){0,3}?\s+(?:must|should|shall)\s+be\s+(?:report|return|includ|record|cop|attach|quot|past|echo|list|add|put)\w*`,
|
|
434
|
+
].join("|"),
|
|
435
|
+
"i",
|
|
436
|
+
);
|
|
407
437
|
test("the shared-run note hands over the run and forbids a rerun, and asks for no copy of it in any rendering", () => {
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
const failing = {
|
|
438
|
+
const base = { command: "node --test", logPath: "/r/test-run.log" };
|
|
439
|
+
const usable = { ...base, exitCode: 0, tests: 5, pass: 5, fail: 0 };
|
|
440
|
+
const failing = { ...base, exitCode: 1, tests: 5, pass: 3, fail: 2 };
|
|
441
|
+
const cancelled = { ...base, exitCode: 1, tests: 5, pass: 4, fail: 0, cancelled: 1 };
|
|
442
|
+
const unusable = (why) => new RegExp(`This run is NOT usable: \`node --test\` ${why}`);
|
|
411
443
|
for (const [name, run, key, paragraph] of [
|
|
412
444
|
["usable", usable, "tests", null],
|
|
413
|
-
["no counts",
|
|
445
|
+
["no counts", { ...base, error: "no summary" }, "tests", unusable("produced no counts")],
|
|
446
|
+
["0 tests", { ...base, exitCode: 0, tests: 0, pass: 0, fail: 0 }, "tests", unusable("produced 0 tests")],
|
|
447
|
+
["every test skipped", { ...base, exitCode: 0, tests: 5, pass: 0, fail: 0 }, "tests", unusable("passed nothing and failed nothing")],
|
|
448
|
+
["most never ran", { ...base, exitCode: 0, tests: 10, pass: 2, fail: 0 }, "tests", unusable("passed 2 and failed 0 of the 10")],
|
|
449
|
+
["no exit status", { ...base, tests: 5, pass: 5, fail: 0 }, "tests", unusable("reported counts but no exit status")],
|
|
450
|
+
["non-zero exit, nothing failing", { ...base, exitCode: 1, tests: 5, pass: 5, fail: 0 }, "tests", unusable("exited 1 but reported no failing")],
|
|
414
451
|
["failing, owner", failing, "correctness", /filing them is YOUR job in this review/],
|
|
415
452
|
["failing, non-owner", failing, "tests", /the correctness dimension files the finding/],
|
|
453
|
+
["cancelled, owner", cancelled, "correctness", /cancelled tests, and filing them is YOUR job/],
|
|
454
|
+
["cancelled, non-owner", cancelled, "tests", /cancelled tests, and the correctness dimension files/],
|
|
416
455
|
]) {
|
|
417
456
|
const note = sharedRunNote(run, "correctness", key);
|
|
418
457
|
assert.doesNotMatch(note, /test_run/, `${name}: the note still names test_run`);
|
|
@@ -425,6 +464,8 @@ test("the shared-run note hands over the run and forbids a rerun, and asks for n
|
|
|
425
464
|
// asks the specialist to report the run.
|
|
426
465
|
const first = note.split(/\n(?=This run is NOT usable|It has )/)[0];
|
|
427
466
|
assert.match(first, /Read the log for anything the counts do not say\.$/, `${name}: the opening paragraph says more after the log instruction`);
|
|
467
|
+
// ...and no paragraph this rendering appends asks for one either.
|
|
468
|
+
assert.doesNotMatch(note.replace(/\s+/g, " "), COPY_REQUEST, `${name}: the note asks the specialist to report or copy the run`);
|
|
428
469
|
if (paragraph) assert.match(note, paragraph, `${name}: the paragraph this rendering appends is missing`);
|
|
429
470
|
}
|
|
430
471
|
});
|