@feigi/fleet-ctl 3.18.12 → 3.18.14

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.18.12",
3
+ "version": "3.18.14",
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": {
@@ -1240,55 +1240,86 @@ function runCli(args = [], {
1240
1240
  // every path below the resolved one. (A symlinked path no longer stops a
1241
1241
  // copy running its own main(): is-cli.mjs compares by realpath.)
1242
1242
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-")));
1243
- const bin = join(dir, "bin");
1244
- const repo = join(dir, "repo");
1245
- mkdirSync(bin);
1246
- mkdirSync(join(repo, ".fleet"), { recursive: true });
1247
- assert.equal(spawnBounded("git", ["init", "-q", repo]).status, 0);
1248
- writeFileSync(join(repo, ".git", "info", "exclude"), ".fleet/\n.worktrees/\n");
1249
- writeExecStub(join(bin, "gh"), GH_STUB);
1250
- const script = join(bin, "fleet-tick.mjs");
1251
- writeFileSync(script, readFileSync(SCRIPT));
1252
- for (const [name, path] of SIBLING_MODULES) writeFileSync(join(bin, name), readFileSync(path));
1253
- writeFileSync(join(bin, "shortlist.mjs"), SHORTLIST_STUB);
1254
- const fx = (name, content) => { const p = join(dir, name); writeFileSync(p, content); return p; };
1255
- if (ledger !== undefined) writeFileSync(join(repo, ".fleet", "ledger.md"), ledgerText(ledger));
1256
- if (shortlist !== undefined) writeFileSync(join(repo, ".fleet", "shortlist.json"), shortlist);
1257
- beforeRun(repo);
1258
- if (baseline) {
1259
- const rec = spawnBounded(process.execPath, [join(bin, "main-checkout.mjs"), "--record"], { cwd: repo });
1260
- assert.equal(rec.status, 0, rec.stderr);
1243
+ // Everything from here to the return can throw: a failed `git init` or
1244
+ // `--record`, or spawnBounded's timeout (#2320). The dir goes whenever that
1245
+ // happens, `keep` or not — a throw hands the caller no `r.dir` to remove.
1246
+ // `keep` only spares it on a normal return.
1247
+ let returned = false;
1248
+ try {
1249
+ const bin = join(dir, "bin");
1250
+ const repo = join(dir, "repo");
1251
+ mkdirSync(bin);
1252
+ mkdirSync(join(repo, ".fleet"), { recursive: true });
1253
+ assert.equal(spawnBounded("git", ["init", "-q", repo]).status, 0);
1254
+ writeFileSync(join(repo, ".git", "info", "exclude"), ".fleet/\n.worktrees/\n");
1255
+ writeExecStub(join(bin, "gh"), GH_STUB);
1256
+ const script = join(bin, "fleet-tick.mjs");
1257
+ writeFileSync(script, readFileSync(SCRIPT));
1258
+ for (const [name, path] of SIBLING_MODULES) writeFileSync(join(bin, name), readFileSync(path));
1259
+ writeFileSync(join(bin, "shortlist.mjs"), SHORTLIST_STUB);
1260
+ const fx = (name, content) => { const p = join(dir, name); writeFileSync(p, content); return p; };
1261
+ if (ledger !== undefined) writeFileSync(join(repo, ".fleet", "ledger.md"), ledgerText(ledger));
1262
+ if (shortlist !== undefined) writeFileSync(join(repo, ".fleet", "shortlist.json"), shortlist);
1263
+ beforeRun(repo);
1264
+ if (baseline) {
1265
+ const rec = spawnBounded(process.execPath, [join(bin, "main-checkout.mjs"), "--record"], { cwd: repo });
1266
+ assert.equal(rec.status, 0, rec.stderr);
1267
+ }
1268
+ afterBaseline(repo);
1269
+ const refreshLog = fx("refresh.log", "");
1270
+ const issueViewLog = fx("issue-view.log", "");
1271
+ // Every case gets its own state file unless it names one: the default path
1272
+ // resolves against the git common dir, and a shared streak and digest would
1273
+ // make the fold cases order-dependent. `defaultState` opts out for the case
1274
+ // whose subject IS that resolution.
1275
+ const stateArg = args.includes("--state") || defaultState ? [] : ["--state", join(dir, "heartbeat.json")];
1276
+ const r = spawnBounded(process.execPath, [script, ...args, ...stateArg], {
1277
+ cwd: repo,
1278
+ env: {
1279
+ ...process.env, PATH: `${bin}:${process.env.PATH}`,
1280
+ FIXTURE_PRS: fx("prs.json", JSON.stringify(prs)),
1281
+ FIXTURE_CLAIMED: fx("claimed.json", JSON.stringify(claimed)),
1282
+ FIXTURE_ISSUE_STATES: fx("issue-states.json", JSON.stringify(issueStates)),
1283
+ FIXTURE_REFRESH: fx("refresh.json", refresh),
1284
+ REFRESH_LOG: refreshLog, ISSUE_VIEW_LOG: issueViewLog,
1285
+ ...(refreshFail ? { REFRESH_FAIL: "1" } : {}),
1286
+ ...extraEnv,
1287
+ },
1288
+ });
1289
+ r.refreshed = readFileSync(refreshLog, "utf8").split("\n").filter(Boolean).length;
1290
+ r.issueViews = readFileSync(issueViewLog, "utf8").split("\n").filter(Boolean);
1291
+ r.repo = repo;
1292
+ r.dir = dir;
1293
+ returned = true;
1294
+ return r;
1295
+ } finally {
1296
+ if (!returned || !keep) rmSync(dir, { recursive: true, force: true });
1261
1297
  }
1262
- afterBaseline(repo);
1263
- const refreshLog = fx("refresh.log", "");
1264
- const issueViewLog = fx("issue-view.log", "");
1265
- // Every case gets its own state file unless it names one: the default path
1266
- // resolves against the git common dir, and a shared streak and digest would
1267
- // make the fold cases order-dependent. `defaultState` opts out for the case
1268
- // whose subject IS that resolution.
1269
- const stateArg = args.includes("--state") || defaultState ? [] : ["--state", join(dir, "heartbeat.json")];
1270
- const r = spawnBounded(process.execPath, [script, ...args, ...stateArg], {
1271
- cwd: repo,
1272
- env: {
1273
- ...process.env, PATH: `${bin}:${process.env.PATH}`,
1274
- FIXTURE_PRS: fx("prs.json", JSON.stringify(prs)),
1275
- FIXTURE_CLAIMED: fx("claimed.json", JSON.stringify(claimed)),
1276
- FIXTURE_ISSUE_STATES: fx("issue-states.json", JSON.stringify(issueStates)),
1277
- FIXTURE_REFRESH: fx("refresh.json", refresh),
1278
- REFRESH_LOG: refreshLog, ISSUE_VIEW_LOG: issueViewLog,
1279
- ...(refreshFail ? { REFRESH_FAIL: "1" } : {}),
1280
- ...extraEnv,
1281
- },
1282
- });
1283
- r.refreshed = readFileSync(refreshLog, "utf8").split("\n").filter(Boolean).length;
1284
- r.issueViews = readFileSync(issueViewLog, "utf8").split("\n").filter(Boolean);
1285
- r.repo = repo;
1286
- r.dir = dir;
1287
- if (!keep) rmSync(dir, { recursive: true, force: true });
1288
- return r;
1289
1298
  }
1290
1299
  const lineOf = (r, role) => r.stdout.split("\n").filter((l) => l.startsWith(role));
1291
1300
 
1301
+ // A throw inside runCli (a failed `git init`/`--record` assert, spawnBounded's
1302
+ // timeout) used to leave its `fleet-tick-*` fixture in the tmpdir.
1303
+ // `beforeRun` runs after the dir exists, so it stands in for any such throw.
1304
+ for (const keep of [false, true]) {
1305
+ test(`CLI: runCli removes its fixture dir when it throws mid-helper, keep: ${keep}`, () => {
1306
+ let dir;
1307
+ assert.throws(() => runCli([], { keep, beforeRun: (repo) => { dir = join(repo, ".."); throw new Error("boom"); } }), /boom/);
1308
+ assert.ok(dir, "beforeRun never ran, so the throw happened before the dir existed");
1309
+ assert.equal(existsSync(dir), false);
1310
+ });
1311
+ }
1312
+
1313
+ test("CLI: runCli with keep: true hands back a fixture dir that still exists", () => {
1314
+ const r = runCli([], { keep: true });
1315
+ try {
1316
+ assert.equal(existsSync(r.dir), true);
1317
+ } finally {
1318
+ rmSync(r.dir, { recursive: true, force: true });
1319
+ }
1320
+ assert.equal(existsSync(runCli([]).dir), false, "without keep the dir is gone on return");
1321
+ });
1322
+
1292
1323
  test("CLI: the #1692 shape, read off the ledger — two live implementers at cap 2 pull nothing", () => {
1293
1324
  const r = runCli([], {
1294
1325
  ledger: { rows: ["#412 impl-412 · tier-ok=impl-412:fleet-implementer", "#415 impl-415 · tier-ok=impl-415:fleet-implementer"], dispatched: ["impl-412", "impl-415"] },
@@ -1463,15 +1494,15 @@ test("CLI: a ledger token outside the grammar refuses the tick, printing no row"
1463
1494
  assert.match(r.stderr, /^fleet-tick: .*impl-9.*'merged' is not an outcome/m);
1464
1495
  });
1465
1496
 
1466
- test("CLI: a ledger ledger.mjs itself refuses is a refusal here too", () => {
1497
+ test("CLI: a ledger ledger.mjs itself refuses is a refusal here too", (t) => {
1467
1498
  // Two drain markers: ledger.mjs read exits 2 on the broken invariant.
1468
1499
  const dir = runCli([], { shortlist: shortlistText([]), keep: true });
1500
+ t.after(() => rmSync(dir.dir, { recursive: true, force: true }));
1469
1501
  writeFileSync(join(dir.repo, ".fleet", "ledger.md"), `${ledgerText({ drain: "a" })}\n- b\n`);
1470
1502
  const r = spawnBounded(process.execPath, [join(dir.dir, "bin", "fleet-tick.mjs"), "--state", join(dir.dir, "hb.json")], {
1471
1503
  cwd: dir.repo,
1472
1504
  env: { ...process.env, PATH: `${join(dir.dir, "bin")}:${process.env.PATH}`, FIXTURE_PRS: join(dir.dir, "prs.json") },
1473
1505
  });
1474
- rmSync(dir.dir, { recursive: true, force: true });
1475
1506
  assert.equal(r.status, 2, r.stderr);
1476
1507
  assert.equal(r.stdout.trim(), "");
1477
1508
  assert.match(r.stderr, /fleet-tick: ledger\.mjs read exited 2/);
@@ -1676,38 +1707,38 @@ test("CLI: a past-pin halt prints DISPATCH review for its PR (#2083)", () => {
1676
1707
  assert.match(r.stdout, /^reviewers {4}0\/6 → DISPATCH review PR#40 /m);
1677
1708
  });
1678
1709
 
1679
- test("CLI: an ambient GIT_DIR naming another repository cannot move the shortlist read", () => {
1710
+ test("CLI: an ambient GIT_DIR naming another repository cannot move the shortlist read", (t) => {
1680
1711
  const decoy = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-decoy-")));
1712
+ t.after(() => rmSync(decoy, { recursive: true, force: true }));
1681
1713
  spawnBounded("git", ["init", "-q", decoy]);
1682
1714
  mkdirSync(join(decoy, ".fleet"));
1683
1715
  writeFileSync(join(decoy, ".fleet", "shortlist.json"), shortlistText([666]));
1684
1716
  const r = runCli([], { shortlist: shortlistText([7, 8]), env: { GIT_DIR: join(decoy, ".git") } });
1685
- rmSync(decoy, { recursive: true, force: true });
1686
1717
  assert.equal(r.status, 0, r.stderr);
1687
1718
  assert.match(r.stdout, /^implementers 0\/2 → PULL #7 #8 /m);
1688
1719
  });
1689
1720
 
1690
- test("CLI: an inherited GIT_DIR cannot retarget the tier-mismatch closed-ticket probe", () => {
1721
+ test("CLI: an inherited GIT_DIR cannot retarget the tier-mismatch closed-ticket probe", (t) => {
1691
1722
  const decoy = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-decoy-tier-")));
1723
+ t.after(() => rmSync(decoy, { recursive: true, force: true }));
1692
1724
  spawnBounded("git", ["init", "-q", decoy]);
1693
1725
  const r = runCli([], {
1694
1726
  shortlist: shortlistText([1, 2, 3]), ledger: { rows: ["#7 impl-7=tier-mismatch"], dispatched: ["impl-7=tier-mismatch"] },
1695
1727
  issueStates: { 7: "OPEN" }, env: { GIT_DIR: join(decoy, ".git"), GH_REPO: "someone/else" },
1696
1728
  });
1697
- rmSync(decoy, { recursive: true, force: true });
1698
1729
  assert.equal(r.status, 0, r.stderr);
1699
1730
  assert.deepEqual(r.issueViews, ["7"], "the probe must still run");
1700
1731
  assert.match(r.stdout, /^implementers 0\/2 → HOLD \(tier mismatch impl-7\)/m, "#7 is OPEN in the case's own repository - the hold stands");
1701
1732
  });
1702
1733
 
1703
- test("CLI: an inherited GIT_DIR cannot retarget the behind-issue premise probe", () => {
1734
+ test("CLI: an inherited GIT_DIR cannot retarget the behind-issue premise probe", (t) => {
1704
1735
  const decoy = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-decoy-gh-")));
1736
+ t.after(() => rmSync(decoy, { recursive: true, force: true }));
1705
1737
  spawnBounded("git", ["init", "-q", decoy]);
1706
1738
  const r = runCli([], {
1707
1739
  shortlist: shortlistText([1, 2, 3]), ledger: { rows: ["#50 excluded · behind-issue:#9"] }, issueStates: { 9: "OPEN" },
1708
1740
  env: { GIT_DIR: join(decoy, ".git"), GH_REPO: "someone/else" },
1709
1741
  });
1710
- rmSync(decoy, { recursive: true, force: true });
1711
1742
  assert.equal(r.status, 0, r.stderr);
1712
1743
  assert.deepEqual(r.issueViews, ["9"], "the probe must still run");
1713
1744
  assert.equal(r.refreshed, 0, "#9 is OPEN in the case's own repository — the exclusion stands");
@@ -1724,11 +1755,12 @@ const tickAgain = (r) => spawnBounded(process.execPath, [join(r.dir, "bin", "fle
1724
1755
  env: { ...process.env, PATH: `${join(r.dir, "bin")}:${process.env.PATH}`, FIXTURE_PRS: join(r.dir, "prs.json") },
1725
1756
  });
1726
1757
 
1727
- test("CLI: a stray write after the baseline prints MAIN-CHECKOUT-DIRTY first, naming the path and the live members, and holds every dispatching row", () => {
1758
+ test("CLI: a stray write after the baseline prints MAIN-CHECKOUT-DIRTY first, naming the path and the live members, and holds every dispatching row", (t) => {
1728
1759
  const r = runCli([], {
1729
1760
  ...LIVE, prs: [pr(350)], keep: true,
1730
1761
  afterBaseline: (repo) => writeFileSync(join(repo, "stray.mjs"), "x\n"),
1731
1762
  });
1763
+ t.after(() => rmSync(r.dir, { recursive: true, force: true }));
1732
1764
  assert.equal(r.status, 0, r.stderr);
1733
1765
  const lines = r.stdout.trim().split("\n");
1734
1766
  assert.match(lines[0], /^MAIN-CHECKOUT-DIRTY stray\.mjs — changed since the run's baseline; live members: impl-412 — dispatch held: resolve the stray paths FIRST, then re-baseline with ~\/\.fleet\/bin\/fleet-run main-checkout\.mjs --record/);
@@ -1744,7 +1776,6 @@ test("CLI: a stray write after the baseline prints MAIN-CHECKOUT-DIRTY first, na
1744
1776
  const rec = spawnBounded(process.execPath, [join(r.dir, "bin", "main-checkout.mjs"), "--record"], { cwd: r.repo });
1745
1777
  assert.equal(rec.status, 0, rec.stderr);
1746
1778
  const after = tickAgain(r);
1747
- rmSync(r.dir, { recursive: true, force: true });
1748
1779
  assert.equal(after.status, 0, after.stderr);
1749
1780
  assert.doesNotMatch(after.stdout, /MAIN-CHECKOUT|main checkout/);
1750
1781
  assert.match(after.stdout, /^implementers 1\/2 → PULL #420 /m);
@@ -1782,25 +1813,41 @@ test("CLI: a main checkout git cannot read is MAIN-CHECKOUT-UNKNOWN and held, ne
1782
1813
  // Three `--fold-unchanged` ticks over one kept fixture and one state file.
1783
1814
  const foldTicks = (fixture) => {
1784
1815
  const first = runCli(["--fold-unchanged"], { ...fixture, keep: true });
1785
- const state = join(first.dir, "heartbeat.json");
1786
- const quiet = () => JSON.parse(readFileSync(state, "utf8")).quiet;
1787
- const ticks = [{ stdout: first.stdout, status: first.status, stderr: first.stderr, quiet: quiet() }];
1788
- for (let i = 0; i < 2; i++) {
1789
- const t = spawnBounded(process.execPath, [join(first.dir, "bin", "fleet-tick.mjs"), "--fold-unchanged", "--state", state], {
1790
- cwd: first.repo,
1791
- env: {
1792
- ...process.env, PATH: `${join(first.dir, "bin")}:${process.env.PATH}`,
1793
- FIXTURE_PRS: join(first.dir, "prs.json"), FIXTURE_CLAIMED: join(first.dir, "claimed.json"),
1794
- FIXTURE_ISSUE_STATES: join(first.dir, "issue-states.json"), FIXTURE_REFRESH: join(first.dir, "refresh.json"),
1795
- REFRESH_LOG: join(first.dir, "refresh.log"), ISSUE_VIEW_LOG: join(first.dir, "issue-view.log"),
1796
- },
1797
- });
1798
- ticks.push({ stdout: t.stdout, status: t.status, stderr: t.stderr, quiet: quiet() });
1816
+ try {
1817
+ const state = join(first.dir, "heartbeat.json");
1818
+ const quiet = () => JSON.parse(readFileSync(state, "utf8")).quiet;
1819
+ const ticks = [{ stdout: first.stdout, status: first.status, stderr: first.stderr, quiet: quiet() }];
1820
+ for (let i = 0; i < 2; i++) {
1821
+ const t = spawnBounded(process.execPath, [join(first.dir, "bin", "fleet-tick.mjs"), "--fold-unchanged", "--state", state], {
1822
+ cwd: first.repo,
1823
+ env: {
1824
+ ...process.env, PATH: `${join(first.dir, "bin")}:${process.env.PATH}`,
1825
+ FIXTURE_PRS: join(first.dir, "prs.json"), FIXTURE_CLAIMED: join(first.dir, "claimed.json"),
1826
+ FIXTURE_ISSUE_STATES: join(first.dir, "issue-states.json"), FIXTURE_REFRESH: join(first.dir, "refresh.json"),
1827
+ REFRESH_LOG: join(first.dir, "refresh.log"), ISSUE_VIEW_LOG: join(first.dir, "issue-view.log"),
1828
+ },
1829
+ });
1830
+ ticks.push({ stdout: t.stdout, status: t.status, stderr: t.stderr, quiet: quiet() });
1831
+ }
1832
+ return ticks;
1833
+ } finally {
1834
+ rmSync(first.dir, { recursive: true, force: true });
1799
1835
  }
1800
- rmSync(first.dir, { recursive: true, force: true });
1801
- return ticks;
1802
1836
  };
1803
1837
 
1838
+ // foldTicks keeps its fixture across three ticks, so a throw after the first
1839
+ // run (here: `heartbeat.json` is a directory, so reading the state fails)
1840
+ // must still remove the dir — the caller never gets it back.
1841
+ test("CLI: foldTicks removes its kept fixture dir when a tick throws", () => {
1842
+ let dir;
1843
+ assert.throws(() => foldTicks({
1844
+ ...LIVE,
1845
+ afterBaseline: (repo) => { dir = join(repo, ".."); mkdirSync(join(dir, "heartbeat.json")); },
1846
+ }), /EISDIR/);
1847
+ assert.ok(dir, "afterBaseline never ran, so the throw happened before the dir existed");
1848
+ assert.equal(existsSync(dir), false);
1849
+ });
1850
+
1804
1851
  for (const [state, afterBaseline] of [
1805
1852
  ["dirty", (repo) => writeFileSync(join(repo, "stray.mjs"), "x\n")],
1806
1853
  ["unknown", (repo) => writeFileSync(join(repo, ".git", "index"), "not an index")],
@@ -1839,12 +1886,12 @@ test("CLI: an in-flight review with no member token is named as review:PR#<n> on
1839
1886
  assert.match(r.stdout, /^MAIN-CHECKOUT-DIRTY stray\.mjs — changed since the run's baseline; live members: review:PR#51 — /m);
1840
1887
  });
1841
1888
 
1842
- test("CLI: an ambient GIT_DIR naming a dirty repository does not move the main-checkout check", () => {
1889
+ test("CLI: an ambient GIT_DIR naming a dirty repository does not move the main-checkout check", (t) => {
1843
1890
  const decoy = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-decoy-mc-")));
1891
+ t.after(() => rmSync(decoy, { recursive: true, force: true }));
1844
1892
  spawnBounded("git", ["init", "-q", decoy]);
1845
1893
  writeFileSync(join(decoy, "decoy-stray.txt"), "x\n");
1846
1894
  const r = runCli([], { ...LIVE, env: { GIT_DIR: join(decoy, ".git"), GIT_WORK_TREE: decoy } });
1847
- rmSync(decoy, { recursive: true, force: true });
1848
1895
  assert.equal(r.status, 0, r.stderr);
1849
1896
  assert.doesNotMatch(r.stdout, /MAIN-CHECKOUT/);
1850
1897
  assert.match(r.stdout, /^implementers 1\/2 → PULL #420 /m);
@@ -1857,8 +1904,9 @@ test("CLI: an ambient GIT_DIR naming a dirty repository does not move the main-c
1857
1904
 
1858
1905
  const IDLE = { shortlist: shortlistText([]), refresh: shortlistText([]) };
1859
1906
 
1860
- test("CLI: the quiet streak lengthens on an idle tick and resets on an actionable one", () => {
1907
+ test("CLI: the quiet streak lengthens on an idle tick and resets on an actionable one", (t) => {
1861
1908
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-state-")));
1909
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
1862
1910
  const path = join(dir, "heartbeat.json");
1863
1911
  runCli(["--state", path], IDLE);
1864
1912
  assert.equal(JSON.parse(readFileSync(path, "utf8")).quiet, 1);
@@ -1866,22 +1914,22 @@ test("CLI: the quiet streak lengthens on an idle tick and resets on an actionabl
1866
1914
  assert.equal(JSON.parse(readFileSync(path, "utf8")).quiet, 2);
1867
1915
  runCli(["--state", path], { ...IDLE, shortlist: shortlistText([9]) });
1868
1916
  assert.equal(JSON.parse(readFileSync(path, "utf8")).quiet, 0);
1869
- rmSync(dir, { recursive: true, force: true });
1870
1917
  });
1871
1918
 
1872
- test("CLI: --fold-unchanged folds a repeated idle tick to one line", () => {
1919
+ test("CLI: --fold-unchanged folds a repeated idle tick to one line", (t) => {
1873
1920
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-fold-")));
1921
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
1874
1922
  const path = join(dir, "heartbeat.json");
1875
1923
  const first = runCli(["--fold-unchanged", "--state", path], IDLE);
1876
1924
  assert.equal(first.status, 0, first.stderr);
1877
1925
  assert.ok(first.stdout.trim().split("\n").length >= 3);
1878
1926
  const second = runCli(["--fold-unchanged", "--state", path], IDLE);
1879
1927
  assert.deepEqual(second.stdout.trim().split("\n"), ["fleet-tick: unchanged, nothing to act on (quiet=2) — full rows on the next change"]);
1880
- rmSync(dir, { recursive: true, force: true });
1881
1928
  });
1882
1929
 
1883
- test("CLI: --fold-unchanged never folds an actionable tick, even an identical one", () => {
1930
+ test("CLI: --fold-unchanged never folds an actionable tick, even an identical one", (t) => {
1884
1931
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-fold-act-")));
1932
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
1885
1933
  const path = join(dir, "heartbeat.json");
1886
1934
  const busy = { shortlist: shortlistText([9, 10]) };
1887
1935
  const first = runCli(["--fold-unchanged", "--state", path], busy);
@@ -1889,11 +1937,11 @@ test("CLI: --fold-unchanged never folds an actionable tick, even an identical on
1889
1937
  assert.equal(second.stdout, first.stdout);
1890
1938
  assert.match(second.stdout, /PULL #9 #10/);
1891
1939
  assert.equal(JSON.parse(readFileSync(path, "utf8")).quiet, 0);
1892
- rmSync(dir, { recursive: true, force: true });
1893
1940
  });
1894
1941
 
1895
- test("CLI: --fold-unchanged does not fold when the rows change, even with nothing to act on", () => {
1942
+ test("CLI: --fold-unchanged does not fold when the rows change, even with nothing to act on", (t) => {
1896
1943
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-fold-differs-")));
1944
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
1897
1945
  const path = join(dir, "heartbeat.json");
1898
1946
  runCli(["--fold-unchanged", "--state", path], IDLE);
1899
1947
  // A queued candidate, held behind a lower PR: still nothing to act on.
@@ -1906,25 +1954,25 @@ test("CLI: --fold-unchanged does not fold when the rows change, even with nothin
1906
1954
  assert.doesNotMatch(second.stdout, /nothing to act on/);
1907
1955
  const third = runCli(["--fold-unchanged", "--state", path], held);
1908
1956
  assert.match(third.stdout, /^fleet-tick: unchanged, nothing to act on \(quiet=3\)/);
1909
- rmSync(dir, { recursive: true, force: true });
1910
1957
  });
1911
1958
 
1912
- test("CLI: with no --state, the tick resolves the run's shared default", () => {
1959
+ test("CLI: with no --state, the tick resolves the run's shared default", (t) => {
1913
1960
  const r = runCli([], { ...IDLE, defaultState: true, keep: true });
1961
+ t.after(() => rmSync(r.dir, { recursive: true, force: true }));
1914
1962
  const state = join(r.repo, ".fleet", "heartbeat.json");
1915
1963
  assert.equal(r.status, 0, r.stderr);
1916
1964
  assert.doesNotMatch(r.stderr, /WARNING/);
1917
1965
  assert.ok(existsSync(state));
1918
1966
  assert.equal(JSON.parse(readFileSync(state, "utf8")).quiet, 1);
1919
- rmSync(r.dir, { recursive: true, force: true });
1920
1967
  });
1921
1968
 
1922
1969
  // The prior run's liveness - #1597.
1923
1970
  const beat = (ms, interval, stopped = "") =>
1924
1971
  JSON.stringify({ quiet: 0, elapsed: 0, digest: "", beat: { at: Date.now() - ms, interval, stopped } });
1925
1972
 
1926
- test("CLI: a stale prior beat is reported, and reported FIRST, with the stranded claims and the supply", () => {
1973
+ test("CLI: a stale prior beat is reported, and reported FIRST, with the stranded claims and the supply", (t) => {
1927
1974
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-stall-")));
1975
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
1928
1976
  const path = join(dir, "heartbeat.json");
1929
1977
  writeFileSync(path, beat(90 * 60_000, 1200));
1930
1978
  const r = runCli(["--state", path], { shortlist: shortlistText([9], 1), claimed: [{ number: 41 }, { number: 42 }, { number: 43 }] });
@@ -1934,31 +1982,31 @@ test("CLI: a stale prior beat is reported, and reported FIRST, with the stranded
1934
1982
  assert.match(lines[1], /^implementers/);
1935
1983
  assert.match(lines[0], /3 ticket\(s\) claimed and in flight/);
1936
1984
  assert.match(lines[0], /pool supply 1/);
1937
- rmSync(dir, { recursive: true, force: true });
1938
1985
  });
1939
1986
 
1940
- test("CLI: an unreadable shortlist makes the stall report's supply unknown, never zero", () => {
1987
+ test("CLI: an unreadable shortlist makes the stall report's supply unknown, never zero", (t) => {
1941
1988
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-stall-supply-")));
1989
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
1942
1990
  const path = join(dir, "heartbeat.json");
1943
1991
  writeFileSync(path, beat(90 * 60_000, 1200));
1944
1992
  const r = runCli(["--state", path], { shortlist: "{" });
1945
1993
  assert.match(r.stdout.split("\n")[0], /pool supply unknown/);
1946
- rmSync(dir, { recursive: true, force: true });
1947
1994
  });
1948
1995
 
1949
- test("CLI: the stall is announced even when the reconcile then refuses", () => {
1996
+ test("CLI: the stall is announced even when the reconcile then refuses", (t) => {
1950
1997
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-stall-refuse-")));
1998
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
1951
1999
  const path = join(dir, "heartbeat.json");
1952
2000
  writeFileSync(path, beat(90 * 60_000, 1200));
1953
2001
  const r = runCli(["--state", path], { ...IDLE, claimed: [{ number: 41 }], env: { PR_FAIL: "1" } });
1954
2002
  assert.equal(r.status, 2);
1955
2003
  assert.match(r.stderr, /gh pr list failed/);
1956
2004
  assert.match(r.stdout, /heartbeat STALLED/);
1957
- rmSync(dir, { recursive: true, force: true });
1958
2005
  });
1959
2006
 
1960
- test("CLI: an unreadable claim count is `unknown`, never zero", () => {
2007
+ test("CLI: an unreadable claim count is `unknown`, never zero", (t) => {
1961
2008
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-stall-unknown-")));
2009
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
1962
2010
  const path = join(dir, "heartbeat.json");
1963
2011
  writeFileSync(path, beat(90 * 60_000, 1200));
1964
2012
  const r = runCli(["--state", path], { ...IDLE, env: { CLAIMED_FAIL: "1" } });
@@ -1966,7 +2014,6 @@ test("CLI: an unreadable claim count is `unknown`, never zero", () => {
1966
2014
  assert.match(r.stdout, /unknown ticket\(s\) claimed and in flight/);
1967
2015
  assert.match(r.stderr, /claimed ticket count unknown/);
1968
2016
  assert.match(r.stderr, /boom/);
1969
- rmSync(dir, { recursive: true, force: true });
1970
2017
  });
1971
2018
 
1972
2019
  // The claimed-count query is capped at CLAIMED_LIMIT (200). At exactly the cap
@@ -1981,8 +2028,9 @@ test("CLI: an unreadable claim count is `unknown`, never zero", () => {
1981
2028
  // /; 199 ticket/). The literal `199 ticket(s)` adjacency — no `+` allowed
1982
2029
  // between the digits and the space — is what refuses `199+`; the `; ` prefix
1983
2030
  // only anchors the match to the claimed-count field.
1984
- test("CLI: a claim count AT the query cap is disclosed as a floor (`200+`); one below it is exact", () => {
2031
+ test("CLI: a claim count AT the query cap is disclosed as a floor (`200+`); one below it is exact", (t) => {
1985
2032
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-stall-cap-")));
2033
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
1986
2034
  const path = join(dir, "heartbeat.json");
1987
2035
  const stalled = (count) => {
1988
2036
  // Re-staled every call: each run records its own tick in the state file.
@@ -1996,21 +2044,21 @@ test("CLI: a claim count AT the query cap is disclosed as a floor (`200+`); one
1996
2044
  };
1997
2045
  assert.match(stalled(200), /; 200\+ ticket\(s\) claimed and in flight/);
1998
2046
  assert.match(stalled(199), /; 199 ticket\(s\) claimed and in flight/);
1999
- rmSync(dir, { recursive: true, force: true });
2000
2047
  });
2001
2048
 
2002
- test("CLI: a beat within the interval it promised is not reported at all", () => {
2049
+ test("CLI: a beat within the interval it promised is not reported at all", (t) => {
2003
2050
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-quiet-")));
2051
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
2004
2052
  const path = join(dir, "heartbeat.json");
2005
2053
  writeFileSync(path, beat(20 * 60_000, 1200));
2006
2054
  assert.doesNotMatch(runCli(["--state", path], IDLE).stdout, /STALLED/);
2007
2055
  writeFileSync(path, beat(20 * 60_000, 300));
2008
2056
  assert.match(runCli(["--state", path], IDLE).stdout, /STALLED/);
2009
- rmSync(dir, { recursive: true, force: true });
2010
2057
  });
2011
2058
 
2012
- test("CLI: the streak write carries the heartbeat's mark instead of erasing it, and writes its own", () => {
2059
+ test("CLI: the streak write carries the heartbeat's mark instead of erasing it, and writes its own", (t) => {
2013
2060
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-mark-carry-")));
2061
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
2014
2062
  const path = join(dir, "heartbeat.json");
2015
2063
  const mark = { at: Date.now() - 60_000, interval: 300, stopped: "" };
2016
2064
  writeFileSync(path, JSON.stringify({ quiet: 3, elapsed: 7, digest: "old", beat: mark }));
@@ -2020,11 +2068,11 @@ test("CLI: the streak write carries the heartbeat's mark instead of erasing it,
2020
2068
  assert.deepEqual(after.beat, mark);
2021
2069
  assert.equal(after.quiet, 4);
2022
2070
  assert.ok(after.ticked && after.ticked.at >= Date.now() - 5000);
2023
- rmSync(dir, { recursive: true, force: true });
2024
2071
  });
2025
2072
 
2026
- test("CLI: a deliberate stop is reported with its reason, and a run that never beat says nothing", () => {
2073
+ test("CLI: a deliberate stop is reported with its reason, and a run that never beat says nothing", (t) => {
2027
2074
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-stopped-")));
2075
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
2028
2076
  const path = join(dir, "heartbeat.json");
2029
2077
  writeFileSync(path, beat(6 * 60_000, 300, "context ceiling reached"));
2030
2078
  const r = runCli(["--state", path], IDLE);
@@ -2033,13 +2081,13 @@ test("CLI: a deliberate stop is reported with its reason, and a run that never b
2033
2081
  assert.doesNotMatch(r.stdout, /without a recorded reason/);
2034
2082
  writeFileSync(path, JSON.stringify({ quiet: 0, elapsed: 0, digest: "" }));
2035
2083
  assert.doesNotMatch(runCli(["--state", path], IDLE).stdout, /STALLED/);
2036
- rmSync(dir, { recursive: true, force: true });
2037
2084
  });
2038
2085
 
2039
- test("CLI: a busy, fully-staffed fleet is not reported STALLED just because the queue never drained", () => {
2086
+ test("CLI: a busy, fully-staffed fleet is not reported STALLED just because the queue never drained", (t) => {
2040
2087
  // `beat` refreshes only when the queue drains; a tick two minutes ago on a
2041
2088
  // completion edge is what tells a busy run from a dead one.
2042
2089
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "fleet-tick-busy-not-stalled-")));
2090
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
2043
2091
  const path = join(dir, "heartbeat.json");
2044
2092
  writeFileSync(path, JSON.stringify({
2045
2093
  quiet: 0, elapsed: 0, digest: "",
@@ -2053,5 +2101,4 @@ test("CLI: a busy, fully-staffed fleet is not reported STALLED just because the
2053
2101
  assert.equal(r.status, 0, r.stderr);
2054
2102
  assert.doesNotMatch(r.stdout, /STALLED/);
2055
2103
  assert.match(r.stdout, /^implementers 2\/2 → AT CAP/m);
2056
- rmSync(dir, { recursive: true, force: true });
2057
2104
  });