@feigi/fleet-ctl 3.23.18 → 3.23.20
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
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
# is asked for must name something that resolves from <repo> (a builtin, a
|
|
38
38
|
# PATH entry, an executable path). A red suite is a finding, not a stale
|
|
39
39
|
# Recipe, so nothing here ever runs the command.
|
|
40
|
+
#
|
|
41
|
+
# Exit status: 0 the command is printed; 1 a refusal about the cache, the
|
|
42
|
+
# repository or the arguments; 3 a tool this script needs to read the cache
|
|
43
|
+
# (node, mktemp, cat) could not be started or node died mid-read, so nothing
|
|
44
|
+
# was read and the cache's usability is unknown.
|
|
40
45
|
set -eu
|
|
41
46
|
|
|
42
47
|
# Byte semantics for every construct below that reads a string by bytes:
|
|
@@ -83,6 +88,15 @@ unset GIT_DIR GIT_WORK_TREE
|
|
|
83
88
|
NAME=derive-testcmd
|
|
84
89
|
die() { printf '%s: %s\n' "$NAME" "$1" >&2; exit 1; }
|
|
85
90
|
|
|
91
|
+
# A refusal about the environment, not the cache: a tool this script needs to
|
|
92
|
+
# read the cache (the interpreter, mktemp, cat) could not be started, or the
|
|
93
|
+
# interpreter died mid-read, so nothing was read and the cache's usability is
|
|
94
|
+
# unknown. Exit 3, where every refusal about the cache, the repository or the
|
|
95
|
+
# arguments is exit 1. A git that cannot be started is not told apart: it reads
|
|
96
|
+
# as no repository, exit 1. A consumer that only tests for non-zero sees no
|
|
97
|
+
# difference.
|
|
98
|
+
unrunnable() { printf '%s: %s\n' "$NAME" "$1" >&2; exit 3; }
|
|
99
|
+
|
|
86
100
|
# Named once: every refusal that sends the caller to re-derive names the same
|
|
87
101
|
# step, so a controller or reviewer reading any of them knows what to run.
|
|
88
102
|
derive="run the Recipe derivation step (run-team phase 0, before the first claim — ADR 0015) to derive, prove and write it"
|
|
@@ -113,7 +127,7 @@ cache="${common%/*}/.fleet/recipe.json"
|
|
|
113
127
|
# An INVOCATION, not a name lookup (#1141): a version-manager shim satisfies
|
|
114
128
|
# `command -v node` and then fails, and its stderr would arrive below under the
|
|
115
129
|
# cache's name — a corrupt-cache refusal for a cache nobody read.
|
|
116
|
-
nodeerr=$(node -e 0 </dev/null 2>&1) ||
|
|
130
|
+
nodeerr=$(node -e 0 </dev/null 2>&1) || unrunnable "node is unusable, refusing to read the Recipe cache without the interpreter — $nodeerr"
|
|
117
131
|
|
|
118
132
|
# Validation and extraction in one pass, so a field is never printed out of a
|
|
119
133
|
# cache the checks did not pass. rc 0 prints the value; anything else carries
|
|
@@ -143,11 +157,15 @@ nodeerr=$(node -e 0 </dev/null 2>&1) || die "node is unusable, refusing to read
|
|
|
143
157
|
# newline now survives inside the frame, and the trailing-operator check
|
|
144
158
|
# below treats it exactly like a trailing `;` or `&`: refused, not
|
|
145
159
|
# silently accepted.
|
|
146
|
-
errf=$(mktemp) ||
|
|
147
|
-
lenf=$(mktemp) ||
|
|
148
|
-
|
|
160
|
+
errf=$(mktemp) || unrunnable "cannot create a temporary file to read the Recipe cache"
|
|
161
|
+
lenf=$(mktemp) || unrunnable "cannot create a temporary file to read the Recipe cache"
|
|
162
|
+
# Cleanup decides nothing: an `rm` that cannot be started would otherwise
|
|
163
|
+
# replace the script's own exit status with 127 (and print a line on the
|
|
164
|
+
# success path), so the status the script was leaving with is kept.
|
|
165
|
+
trap 'rc=$?; rm -f "$errf" "$lenf" 2>/dev/null || :; exit $rc' EXIT
|
|
149
166
|
|
|
150
167
|
open='recipe<' close='>recipe'
|
|
168
|
+
rc=0
|
|
151
169
|
framed=$(node -e '
|
|
152
170
|
const fs = require("fs");
|
|
153
171
|
const [file, field, open, close, lenFile] = process.argv.slice(1);
|
|
@@ -166,13 +184,30 @@ if (!counted && !mutated) bad("it carries no proof of real tests — neither a p
|
|
|
166
184
|
const value = r[field];
|
|
167
185
|
fs.writeFileSync(lenFile, String(Buffer.byteLength(open + value + close)));
|
|
168
186
|
process.stdout.write(open + value + close);
|
|
169
|
-
' "$cache" "$field" "$open" "$close" "$lenf" 2>"$errf") ||
|
|
187
|
+
' "$cache" "$field" "$open" "$close" "$lenf" 2>"$errf") || rc=$?
|
|
188
|
+
# node's own refusal of the cache is exit 2 (an uncaught fault is exit 1), the
|
|
189
|
+
# reason on stderr either way; a status of 126 or above is the shell failing to
|
|
190
|
+
# run node or the system killing it mid-read, which says nothing about the cache.
|
|
191
|
+
if [ "$rc" -ge 126 ]; then
|
|
192
|
+
unrunnable "node did not finish reading the Recipe cache (exit $rc), so its usability is unknown"
|
|
193
|
+
fi
|
|
194
|
+
[ "$rc" -eq 0 ] || die "the Recipe cache at $cache is unusable: $(cat "$errf") — $derive"
|
|
170
195
|
carriedmsg="node's stdout carried more than the framed Recipe value — expected exactly the value between '$open' and '$close', got '$framed'. The cache at $cache passed validation; the extra output comes from how node is launched here (a version-manager or proxy shim, a preload), and is refused rather than cut out of the command"
|
|
171
196
|
case $framed in
|
|
172
197
|
"$open"*"$close") ;;
|
|
173
198
|
*) die "$carriedmsg" ;;
|
|
174
199
|
esac
|
|
175
|
-
|
|
200
|
+
# A `cat` that cannot be started (126/127) is the environment's fault, as for
|
|
201
|
+
# `node` above; a `cat` that ran and failed refuses with its own reason, and a
|
|
202
|
+
# count file that was read but holds no digits is the corrupt-count refusal
|
|
203
|
+
# below.
|
|
204
|
+
rc=0
|
|
205
|
+
framedlen=$(cat "$lenf" 2>"$errf") || rc=$?
|
|
206
|
+
case $rc in
|
|
207
|
+
0) ;;
|
|
208
|
+
126|127) unrunnable "cat could not be started (exit $rc), so the Recipe cache cannot be read" ;;
|
|
209
|
+
*) die "the Recipe cache at $cache is unusable: cat failed reading node's byte-count file (exit $rc): $(cat "$errf" 2>/dev/null) — $derive" ;;
|
|
210
|
+
esac
|
|
176
211
|
case $framedlen in
|
|
177
212
|
''|*[!0-9]*) die "the Recipe cache at $cache is unusable: node's byte-count file is missing or corrupt — $derive" ;;
|
|
178
213
|
esac
|
|
@@ -304,9 +304,10 @@ test("the success path writes nothing to stderr, even when node is chatty (#1175
|
|
|
304
304
|
// cache. PATH is REPLACED with links to exactly what the script runs, so the
|
|
305
305
|
// interpreter is really unreachable rather than shadowed.
|
|
306
306
|
const SHIMMED = ["sh", "git", "mktemp", "cat", "rm"];
|
|
307
|
-
function shimPath({ node }) {
|
|
307
|
+
function shimPath({ node, omit }) {
|
|
308
308
|
const bin = tempDir("derive-path-");
|
|
309
309
|
for (const name of SHIMMED) {
|
|
310
|
+
if (name === omit) continue;
|
|
310
311
|
const real = execFileSync("sh", ["-c", `command -v ${name}`], { encoding: "utf8" }).trim();
|
|
311
312
|
symlinkSync(real, join(bin, name));
|
|
312
313
|
}
|
|
@@ -314,11 +315,14 @@ function shimPath({ node }) {
|
|
|
314
315
|
return bin;
|
|
315
316
|
}
|
|
316
317
|
|
|
318
|
+
// Exit 3, where every refusal about the cache, the repository or the arguments
|
|
319
|
+
// is exit 1: the reader read nothing, so a caller can tell its own
|
|
320
|
+
// environment's fault from a verdict on the cache.
|
|
317
321
|
test("an unusable interpreter refuses in this script's own voice, never the cache's", () => {
|
|
318
322
|
const { dir, head } = repo();
|
|
319
323
|
cache(dir, recipe(head, { test: "true" }));
|
|
320
324
|
const absent = derive(dir, "test", { ...process.env, PATH: shimPath({ node: false }) });
|
|
321
|
-
assert.equal(absent.status,
|
|
325
|
+
assert.equal(absent.status, 3);
|
|
322
326
|
assert.equal(absent.out, "");
|
|
323
327
|
assert.match(absent.err, /^derive-testcmd: node is unusable/);
|
|
324
328
|
assert.doesNotMatch(absent.err, /is unusable: it does not parse/);
|
|
@@ -331,6 +335,119 @@ test("an unusable interpreter refuses in this script's own voice, never the cach
|
|
|
331
335
|
assert.equal(present.out, "true");
|
|
332
336
|
});
|
|
333
337
|
|
|
338
|
+
// The reader's other tools, taken away the same way. The control above is the
|
|
339
|
+
// same stripped PATH with every tool present.
|
|
340
|
+
for (const [tool, reason] of [
|
|
341
|
+
["mktemp", /^derive-testcmd: cannot create a temporary file to read the Recipe cache$/m],
|
|
342
|
+
["cat", /^derive-testcmd: cat could not be started \(exit 127\), so the Recipe cache cannot be read$/m],
|
|
343
|
+
]) {
|
|
344
|
+
test(`a ${tool} that cannot be started is the environment's refusal, exit 3, never the cache's`, () => {
|
|
345
|
+
const { dir, head } = repo();
|
|
346
|
+
cache(dir, recipe(head, { test: "true" }));
|
|
347
|
+
const r = derive(dir, "test", { ...process.env, PATH: shimPath({ node: true, omit: tool }) });
|
|
348
|
+
assert.equal(r.status, 3, r.err);
|
|
349
|
+
assert.equal(r.out, "");
|
|
350
|
+
assert.match(r.err, reason);
|
|
351
|
+
assert.doesNotMatch(r.err, /is unusable|byte-count/);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// The reader asks mktemp twice, and a mktemp that starts and then fails — a
|
|
356
|
+
// full or unwritable temp directory — is the same environment fault as one
|
|
357
|
+
// that is missing, on either call.
|
|
358
|
+
test("a mktemp that runs and fails on its second call is exit 3, never the cache's", () => {
|
|
359
|
+
const { dir, head } = repo();
|
|
360
|
+
cache(dir, recipe(head, { test: "true" }));
|
|
361
|
+
const bin = shimPath({ node: true, omit: "mktemp" });
|
|
362
|
+
const real = execFileSync("sh", ["-c", "command -v mktemp"], { encoding: "utf8" }).trim();
|
|
363
|
+
const mark = join(bin, "called");
|
|
364
|
+
writeExecStub(join(bin, "mktemp"), `#!/bin/sh\nif [ -e '${mark}' ]; then echo 'mktemp: no space left' >&2; exit 1; fi\n: > '${mark}'\nexec '${real}' "$@"\n`);
|
|
365
|
+
const r = derive(dir, "test", { ...process.env, PATH: bin });
|
|
366
|
+
assert.equal(r.status, 3, r.err);
|
|
367
|
+
assert.match(r.err, /^derive-testcmd: cannot create a temporary file to read the Recipe cache$/m);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// The reader's `node -e 0` probe passes, and then the interpreter dies during
|
|
371
|
+
// the real read — killed, or no longer runnable. Nothing was read, so that is
|
|
372
|
+
// exit 3 as well. Node's own refusal of the cache (its exit 2) and an uncaught
|
|
373
|
+
// fault of its own (exit 1) are still the cache's exit 1, with node's reason.
|
|
374
|
+
for (const [what, body, status, reason] of [
|
|
375
|
+
["is killed", "kill -9 $$", 3, /^derive-testcmd: node did not finish reading the Recipe cache \(exit 137\)/m],
|
|
376
|
+
["can no longer be run", "exit 127", 3, /^derive-testcmd: node did not finish reading the Recipe cache \(exit 127\)/m],
|
|
377
|
+
["refuses the cache itself", "echo 'it does not parse: boom' >&2; exit 2", 1, /is unusable: it does not parse: boom/],
|
|
378
|
+
["faults on its own", "echo 'TypeError: boom' >&2; exit 1", 1, /is unusable: TypeError: boom/],
|
|
379
|
+
]) {
|
|
380
|
+
test(`a node that passes the probe and then ${what} is exit ${status}`, () => {
|
|
381
|
+
const { dir, head } = repo();
|
|
382
|
+
cache(dir, recipe(head, { test: "true" }));
|
|
383
|
+
const bin = shimPath({ node: false });
|
|
384
|
+
writeExecStub(join(bin, "node"), `#!/bin/sh\nif [ "$1" = -e ] && [ "$2" = 0 ]; then exit 0; fi\n${body}\n`);
|
|
385
|
+
const r = derive(dir, "test", { ...process.env, PATH: bin });
|
|
386
|
+
assert.equal(r.status, status, r.err);
|
|
387
|
+
assert.equal(r.out, "");
|
|
388
|
+
assert.match(r.err, reason);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// A cat that exits 126 — the status a shell gives a command it found but could
|
|
393
|
+
// not execute — is the environment's fault like one that is missing (127). The
|
|
394
|
+
// stub exits 126 itself: whether a shell reports an unexecutable file as 126
|
|
395
|
+
// depends on the shell and its options, and this pins the reader's reading of
|
|
396
|
+
// the status, not the shell's.
|
|
397
|
+
test("a cat that exits 126 is exit 3, never the cache's", () => {
|
|
398
|
+
const { dir, head } = repo();
|
|
399
|
+
cache(dir, recipe(head, { test: "true" }));
|
|
400
|
+
const bin = shimPath({ node: true, omit: "cat" });
|
|
401
|
+
writeExecStub(join(bin, "cat"), "#!/bin/sh\nexit 126\n");
|
|
402
|
+
const r = derive(dir, "test", { ...process.env, PATH: bin });
|
|
403
|
+
assert.equal(r.status, 3, r.err);
|
|
404
|
+
assert.equal(r.out, "");
|
|
405
|
+
assert.match(r.err, /^derive-testcmd: cat could not be started \(exit 126\), so the Recipe cache cannot be read$/m);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// A cat that starts and fails while reading node's byte-count file is the
|
|
409
|
+
// cache refusal, exit 1, and carries cat's own reason.
|
|
410
|
+
test("a cat that runs and fails on the byte-count file is exit 1, carrying its reason", () => {
|
|
411
|
+
const { dir, head } = repo();
|
|
412
|
+
cache(dir, recipe(head, { test: "true" }));
|
|
413
|
+
const bin = shimPath({ node: true, omit: "cat" });
|
|
414
|
+
const real = execFileSync("sh", ["-c", "command -v cat"], { encoding: "utf8" }).trim();
|
|
415
|
+
const mark = join(bin, "called");
|
|
416
|
+
writeExecStub(join(bin, "cat"), `#!/bin/sh\nif [ ! -e '${mark}' ]; then : > '${mark}'; echo 'cat: I/O error' >&2; exit 1; fi\nexec '${real}' "$@"\n`);
|
|
417
|
+
const r = derive(dir, "test", { ...process.env, PATH: bin });
|
|
418
|
+
assert.equal(r.status, 1, r.err);
|
|
419
|
+
assert.equal(r.out, "");
|
|
420
|
+
assert.match(r.err, /cat failed reading node's byte-count file \(exit 1\): cat: I\/O error/);
|
|
421
|
+
assert.doesNotMatch(r.err, /could not be started/);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// `rm` is the reader's cleanup only, so it must not decide the outcome: with
|
|
425
|
+
// it gone a good cache still reads cleanly — status 0 and nothing on stderr,
|
|
426
|
+
// the success-path invariant claim-ticket.sh relies on — and a refused cache
|
|
427
|
+
// keeps its own status.
|
|
428
|
+
test("an rm that cannot be started changes neither the value read nor the refusal's status", () => {
|
|
429
|
+
const { dir, head } = repo();
|
|
430
|
+
cache(dir, recipe(head, { test: "true" }));
|
|
431
|
+
const ok = derive(dir, "test", { ...process.env, PATH: shimPath({ node: true, omit: "rm" }) });
|
|
432
|
+
assert.equal(ok.status, 0, ok.err);
|
|
433
|
+
assert.equal(ok.out, "true");
|
|
434
|
+
assert.equal(ok.err, "");
|
|
435
|
+
|
|
436
|
+
cache(dir, "not json");
|
|
437
|
+
const refused = derive(dir, "test", { ...process.env, PATH: shimPath({ node: true, omit: "rm" }) });
|
|
438
|
+
assert.equal(refused.status, 1, refused.err);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// The false-positive class: a cache the reader really refuses stays exit 1
|
|
442
|
+
// under the same stripped PATH, so exit 3 is not "any refusal".
|
|
443
|
+
test("a cache that is refused stays exit 1 under the stripped PATH", () => {
|
|
444
|
+
const { dir } = repo();
|
|
445
|
+
cache(dir, "not json");
|
|
446
|
+
const r = derive(dir, "test", { ...process.env, PATH: shimPath({ node: true }) });
|
|
447
|
+
assert.equal(r.status, 1, r.err);
|
|
448
|
+
assert.match(r.err, /is unusable/);
|
|
449
|
+
});
|
|
450
|
+
|
|
334
451
|
// --- #2217: node's STDOUT is the capture itself, so the stream split above
|
|
335
452
|
// does nothing for it. A version-manager or proxy shim prints before exec'ing
|
|
336
453
|
// the real node, or — not exec'ing — after it exits, and unframed that chatter
|
package/scripts/recipe-prove.mjs
CHANGED
|
@@ -57,8 +57,10 @@
|
|
|
57
57
|
// repository, origin/main missing, the worktree could not be made), or
|
|
58
58
|
// a git or filesystem fault stopped it before the cache was settled
|
|
59
59
|
// (a git command could not be started, the reader's own git
|
|
60
|
-
// included
|
|
61
|
-
//
|
|
60
|
+
// included; the reader's `sh`, `node`, `mktemp` or `cat` could not be
|
|
61
|
+
// started, or its `node` died mid-read — a `cat` that ran and failed
|
|
62
|
+
// stays the reader's refusal, exit 1; the cache or its temp file
|
|
63
|
+
// could not be written). The log directory under $TMPDIR is removed.
|
|
62
64
|
|
|
63
65
|
import { spawnSync } from "node:child_process";
|
|
64
66
|
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -110,6 +112,11 @@ class Refusal extends Error {
|
|
|
110
112
|
const cannot = (message) => new Refusal(2, message);
|
|
111
113
|
const notProven = (message) => new Refusal(1, message);
|
|
112
114
|
|
|
115
|
+
// The exit status derive-testcmd.sh uses for a refusal about its own
|
|
116
|
+
// environment — a tool it needs would not start, or node died mid-read — where
|
|
117
|
+
// every refusal about the cache is 1.
|
|
118
|
+
const READER_COULD_NOT_RUN = 3;
|
|
119
|
+
|
|
113
120
|
function parseArgs(argv) {
|
|
114
121
|
const [repo, ...rest] = argv;
|
|
115
122
|
if (!repo || repo.startsWith("--")) throw cannot(USAGE);
|
|
@@ -278,9 +285,11 @@ function prove(o, wt, logs) {
|
|
|
278
285
|
// Write, then read back through the one reader. A cache it refuses is undone:
|
|
279
286
|
// the prior bytes restored, or the file removed when none stood before. The
|
|
280
287
|
// cache is undone just the same when the read-back is no verdict: an `sh` that
|
|
281
|
-
// could not be started read nothing
|
|
282
|
-
//
|
|
283
|
-
//
|
|
288
|
+
// could not be started read nothing; a reader that exits READER_COULD_NOT_RUN
|
|
289
|
+
// could not start its `node`, `mktemp` or `cat`, or lost its `node` mid-read;
|
|
290
|
+
// and the reader runs git itself and reads a git that will not start as no
|
|
291
|
+
// repository at all, so its refusal is a verdict on what was proven only while
|
|
292
|
+
// git still starts.
|
|
284
293
|
function writeCache(cache, recipe, repo) {
|
|
285
294
|
mkdirSync(dirname(cache), { recursive: true });
|
|
286
295
|
const prior = existsSync(cache) ? readFileSync(cache) : null;
|
|
@@ -298,6 +307,7 @@ function writeCache(cache, recipe, repo) {
|
|
|
298
307
|
if (prior === null) unlinkSync(cache);
|
|
299
308
|
else writeFileSync(cache, prior);
|
|
300
309
|
if (r.error && !r.pid) throw cannot(`could not start sh to read the Recipe cache back: ${r.error.message}`);
|
|
310
|
+
if (r.status === READER_COULD_NOT_RUN) throw cannot(`${r.stderr.trim()} — the Recipe cache reader could not run a tool it needs, so its refusal is no verdict on what was proven`);
|
|
301
311
|
try {
|
|
302
312
|
git(["--version"]);
|
|
303
313
|
} catch (e) {
|
|
@@ -677,6 +677,48 @@ test("an sh that stops starting before the cache is read back is no verdict, nev
|
|
|
677
677
|
assert.equal(existsSync(cachePath(dir)), false, "a cache the reader never read is rolled back");
|
|
678
678
|
});
|
|
679
679
|
|
|
680
|
+
// The reader's other external tools, taken away the same way after the proof's
|
|
681
|
+
// last step. The control is the same PATH with the tool left in place, which
|
|
682
|
+
// proves — so the no verdict below is about the missing tool, not a PATH too
|
|
683
|
+
// thin for the reader to run at all. `rm` is the reader's cleanup only: it
|
|
684
|
+
// decides nothing about the cache, so the verdict stands without it.
|
|
685
|
+
const READER_TOOLS = [
|
|
686
|
+
{ tool: "node", reason: /derive-testcmd: node is unusable, refusing to read the Recipe cache without the interpreter/ },
|
|
687
|
+
{ tool: "mktemp", reason: /derive-testcmd: cannot create a temporary file to read the Recipe cache/ },
|
|
688
|
+
{ tool: "cat", reason: /derive-testcmd: cat could not be started/ },
|
|
689
|
+
];
|
|
690
|
+
for (const c of READER_TOOLS) {
|
|
691
|
+
test(`a ${c.tool} that stops starting before the cache is read back is no verdict, never NOT PROVEN — and the prior cache is restored`, () => {
|
|
692
|
+
const args = (cmd) => ["--install", "true", "--test", cmd, "--count-line", "tests 1", "--test-count", "1"];
|
|
693
|
+
const kept = repo(MAVEN_FILES);
|
|
694
|
+
const ok = prove(kept.dir, args(`echo 'tests 1'`), { env: { PATH: readerPath() } });
|
|
695
|
+
assert.equal(ok.status, 0, ok.err);
|
|
696
|
+
assert.match(ok.out, /^recipe-prove: PROVEN/);
|
|
697
|
+
|
|
698
|
+
const { dir } = repo(MAVEN_FILES);
|
|
699
|
+
mkdirSync(join(dir, ".fleet"));
|
|
700
|
+
writeFileSync(cachePath(dir), "prior bytes");
|
|
701
|
+
const bin = readerPath();
|
|
702
|
+
const r = prove(dir, args(`rm '${join(bin, c.tool)}'; echo 'tests 1'`), { env: { PATH: bin } });
|
|
703
|
+
assert.equal(r.status, 2, r.err);
|
|
704
|
+
assert.match(r.err, c.reason);
|
|
705
|
+
assert.match(r.err, /the Recipe cache reader could not run a tool it needs, so its refusal is no verdict/);
|
|
706
|
+
assert.doesNotMatch(r.err, /NOT PROVEN/);
|
|
707
|
+
assert.equal(readFileSync(cachePath(dir), "utf8"), "prior bytes", "a cache the reader never settled is rolled back");
|
|
708
|
+
assert.deepEqual(logDirs(r.tmp), [], "no verdict keeps no logs");
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
test("an rm that stops starting before the cache is read back leaves the proof standing — the reader's verdict never depended on it", () => {
|
|
713
|
+
const { dir } = repo(MAVEN_FILES);
|
|
714
|
+
const bin = readerPath();
|
|
715
|
+
// `echo` leads the command: the reader refuses a Test entrypoint whose first word does not resolve, and `rm` would not once the PATH link is gone.
|
|
716
|
+
const r = prove(dir, ["--install", "true", "--test", `echo 'tests 1'; rm '${join(bin, "rm")}'`, "--count-line", "tests 1", "--test-count", "1"], { env: { PATH: bin } });
|
|
717
|
+
assert.equal(r.status, 0, r.err);
|
|
718
|
+
assert.match(r.out, /^recipe-prove: PROVEN/);
|
|
719
|
+
assert.equal(existsSync(cachePath(dir)), true);
|
|
720
|
+
});
|
|
721
|
+
|
|
680
722
|
// Whether the process ignores file modes: root writes into a 0555 directory.
|
|
681
723
|
const IGNORES_MODES = process.getuid?.() === 0 && "root ignores file modes";
|
|
682
724
|
|
|
@@ -76,7 +76,7 @@ gh label create in-progress --color FBCA04 --force # first time only
|
|
|
76
76
|
gh issue edit <N> --add-label in-progress
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
Infer branch/worktree convention from `git worktree list` / `git branch -r` — commonly `feat|fix|refactor/<N>-slug` and `.worktrees/<N>-slug`. Branch off fresh `origin/main`, run the Install step, run the Test entrypoint as the baseline — both the repo's **Recipe**, read from its Recipe cache: `~/.fleet/bin/fleet-run derive-testcmd.sh . install` and `… . test` print them. **No usable cache (either
|
|
79
|
+
Infer branch/worktree convention from `git worktree list` / `git branch -r` — commonly `feat|fix|refactor/<N>-slug` and `.worktrees/<N>-slug`. Branch off fresh `origin/main`, run the Install step, run the Test entrypoint as the baseline — both the repo's **Recipe**, read from its Recipe cache: `~/.fleet/bin/fleet-run derive-testcmd.sh . install` and `… . test` print them. **No usable cache (either exits 1) → run the Recipe derivation step first:** dispatch a `task` member, agent `fleet-recipe-deriver`, its prompt naming the main checkout's absolute path as `<repo>` and a scratch directory of its own; it reads the repo, proves both commands in a throwaway worktree and writes the cache only on proof. `RECIPE NOT PROVEN` → stop and give the maintainer its reason; never pick a command yourself. Exit 3 is no verdict on the cache — the reader could not run a tool it needs — so fix that environment fault and re-run instead of deriving.
|
|
80
80
|
|
|
81
81
|
Abandoned before PR opens → remove `in-progress`.
|
|
82
82
|
|
package/skills/run-team/SKILL.md
CHANGED
|
@@ -369,7 +369,10 @@ phase, or in any later one, asks the maintainer which tickets to take.
|
|
|
369
369
|
command. Check it first: `~/.fleet/bin/fleet-run derive-testcmd.sh .
|
|
370
370
|
install` and `~/.fleet/bin/fleet-run derive-testcmd.sh . test`. **Both exit
|
|
371
371
|
0 → the cache is present and valid: use it as it stands and derive
|
|
372
|
-
nothing.** Either
|
|
372
|
+
nothing.** Either exits 1 (a refusal about the cache) → derive it. Either
|
|
373
|
+
exits 3 → the reader could not run a tool it needs, so the cache's state is
|
|
374
|
+
unknown: fix that environment fault and check again; deriving would not
|
|
375
|
+
help. This step is yours: one derivation
|
|
373
376
|
per run start at most, never one per member, and a member never derives.
|
|
374
377
|
Dispatch a `task` member named `derive-recipe`, agent
|
|
375
378
|
`fleet-recipe-deriver`, and wait for its report — nothing in this run can
|