@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.21.3",
3
+ "version": "3.21.5",
4
4
  "description": "Agent fleet: run-team controller, merge bot, PR reviewer, and the ticket pipeline they share",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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"));
@@ -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, so the reader stays the one authority on
42
- // what a usable cache is. A failed proof never touches an existing cache.
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; the cache or its temp file
50
- // could not be written).
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 !== 0 || r.stdout.replace(/\n$/, "") !== recipe[field]) {
230
- if (prior === null) unlinkSync(cache);
231
- else writeFileSync(cache, prior);
232
- throw notProven(`the Recipe cache reader refuses what was proven: ${(r.stderr || r.stdout).trim()}`);
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, no counts (NOT usable), and failing
405
- // tests for the owner and for a non-owner — is checked, because each appends a
406
- // different paragraph to the same opening one.
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 usable = { command: "node --test", logPath: "/r/test-run.log", exitCode: 0, tests: 5, pass: 5, fail: 0 };
409
- const noCounts = { command: "node --test", logPath: "/r/test-run.log", error: "no summary" };
410
- const failing = { command: "node --test", logPath: "/r/test-run.log", exitCode: 1, tests: 5, pass: 3, fail: 2 };
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", noCounts, "tests", /This run is NOT usable: /],
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
  });