@feigi/fleet-ctl 3.18.9 → 3.18.11
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 +1 -1
- package/scripts/board.test.mjs +53 -14
- package/scripts/citation-sweep-prose.test.mjs +1 -1
- package/scripts/ledger.mjs +143 -147
- package/scripts/member-record.mjs +51 -53
package/package.json
CHANGED
package/scripts/board.test.mjs
CHANGED
|
@@ -2045,11 +2045,17 @@ function muteHolder() {
|
|
|
2045
2045
|
// degrade arm has to stay fatal, exactly as it was before #1585 introduced
|
|
2046
2046
|
// scanning at all.
|
|
2047
2047
|
//
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2050
|
-
|
|
2048
|
+
// The blocker is a bare net server, and it has to be OURS: a port some
|
|
2049
|
+
// outside process already holds can be let go between this bind and
|
|
2050
|
+
// board's own, and board then binds 8123 and serves (#2522). The degrade
|
|
2051
|
+
// arm derives no port but 8123, so there is none to move to: when an
|
|
2052
|
+
// outside process holds it, the row skips rather than ride that process's
|
|
2053
|
+
// timing.
|
|
2054
|
+
test("CLI: a derived port held by anything is fatal on the degrade arm — there is no identity to scan for", async (t) => {
|
|
2051
2055
|
const blocker = createServer();
|
|
2052
|
-
await new Promise((res) => { blocker.once("error", res); blocker.listen(8123, res); });
|
|
2056
|
+
const failure = await new Promise((res) => { blocker.once("error", res); blocker.listen(8123, () => res(null)); });
|
|
2057
|
+
if (failure?.code === "EADDRINUSE") return t.skip(heldOutside(8123));
|
|
2058
|
+
if (failure) throw failure;
|
|
2053
2059
|
const nobin = tempDir("board-nobin-");
|
|
2054
2060
|
const cwd = tempDir("board-serve-");
|
|
2055
2061
|
try {
|
|
@@ -2621,19 +2627,47 @@ test("cockpitPorts: the window wraps at the top of the range rather than leaving
|
|
|
2621
2627
|
|
|
2622
2628
|
// A holder serving `dir` on a real ephemeral port, through the same
|
|
2623
2629
|
// server-creation seam the cockpit itself uses — no second I/O surface, in
|
|
2624
|
-
// the tests either.
|
|
2630
|
+
// the tests either. A port someone else already holds comes back as
|
|
2631
|
+
// `port: null`; any other bind failure rejects.
|
|
2625
2632
|
async function holderOn(dir, port = 0) {
|
|
2626
2633
|
const server = createBoardServer(dir);
|
|
2627
|
-
const bound = await new Promise((res
|
|
2634
|
+
const bound = await new Promise((res, rej) => {
|
|
2635
|
+
server.once("error", (e) => (e.code === "EADDRINUSE" ? res(null) : rej(e)));
|
|
2636
|
+
server.listen(port, () => res(server.address().port));
|
|
2637
|
+
});
|
|
2628
2638
|
return { server, port: bound };
|
|
2629
2639
|
}
|
|
2630
2640
|
|
|
2641
|
+
// The skip reason for a row that could not bind its fixed port itself
|
|
2642
|
+
// (#2522): the outside process holding it can let go before the launch
|
|
2643
|
+
// binds, and the launch then takes the port the row needed held.
|
|
2644
|
+
function heldOutside(port) {
|
|
2645
|
+
return `port ${port} is held by a process outside this test, which can release it before the launch binds`;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2631
2648
|
const boardDir = (payload) => {
|
|
2632
2649
|
const dir = tempDir("board-holder-");
|
|
2633
2650
|
if (payload !== undefined) writeFileSync(join(dir, "board.json"), payload);
|
|
2634
2651
|
return dir;
|
|
2635
2652
|
};
|
|
2636
2653
|
|
|
2654
|
+
// holderOn's other half: only EADDRINUSE reads as "someone else holds it". A
|
|
2655
|
+
// listen() that fails for any other reason must reject, not come back as
|
|
2656
|
+
// `port: null`, which the rows that skip on a null port would then report as
|
|
2657
|
+
// a port held from outside. A unix-socket path under a directory that does
|
|
2658
|
+
// not exist fails through the same 'error' event without privileges; the
|
|
2659
|
+
// code it fails with differs by platform, and that it is not EADDRINUSE is
|
|
2660
|
+
// the point.
|
|
2661
|
+
test("holderOn: a bind failure that is not EADDRINUSE rejects instead of reading as a held port", async () => {
|
|
2662
|
+
const dir = boardDir();
|
|
2663
|
+
try {
|
|
2664
|
+
await assert.rejects(
|
|
2665
|
+
holderOn(dir, join(dir, "no-such-dir", "s.sock")),
|
|
2666
|
+
(e) => e instanceof Error && e.code !== undefined && e.code !== "EADDRINUSE",
|
|
2667
|
+
);
|
|
2668
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
2669
|
+
});
|
|
2670
|
+
|
|
2637
2671
|
// The half that must ACCEPT: a real board payload naming a workspace is the
|
|
2638
2672
|
// one answer the probe has to believe, and everything below is a way of not
|
|
2639
2673
|
// believing it. A probe that only ever returns null passes every negative
|
|
@@ -2908,13 +2942,14 @@ for (const [platform, launchers, ran, warns] of [
|
|
|
2908
2942
|
// workspace field distinguishes it from the cockpit reused above. A
|
|
2909
2943
|
// handshake that asks "did anyone answer" rather than "whose board is this"
|
|
2910
2944
|
// adopts it, and this run gets no board of its own at all.
|
|
2911
|
-
test("CLI: a holder reporting a different workspace is not adopted — the launch serves elsewhere", async () => {
|
|
2945
|
+
test("CLI: a holder reporting a different workspace is not adopted — the launch serves elsewhere", async (t) => {
|
|
2912
2946
|
const bin = gitOnlyPath(), repo = gitRepo("board-ws-foreign-");
|
|
2913
2947
|
const { port: derived } = resolveCockpitInstance({ cwd: repo, gitCommonDir: join(repo, ".git") });
|
|
2914
2948
|
const dir = boardDir(JSON.stringify({ tickets: [], workspace: "/some/other/workspace" }));
|
|
2915
|
-
const { server } = await holderOn(dir, derived);
|
|
2916
|
-
const launch = serveProcess(repo, bin, ["--interval", "3600"]);
|
|
2949
|
+
const { server, port: held } = await holderOn(dir, derived);
|
|
2950
|
+
const launch = held === null ? null : serveProcess(repo, bin, ["--interval", "3600"]);
|
|
2917
2951
|
try {
|
|
2952
|
+
if (!launch) return t.skip(heldOutside(derived));
|
|
2918
2953
|
const url = await withTimeout(launch.url, 20000, "the launch to step over the foreign holder");
|
|
2919
2954
|
assert.notEqual(url, `http://localhost:${derived}`, "the launch adopted a board belonging to another workspace");
|
|
2920
2955
|
const window = cockpitPorts({ port: derived, derived: true });
|
|
@@ -2924,7 +2959,7 @@ test("CLI: a holder reporting a different workspace is not adopted — the launc
|
|
|
2924
2959
|
await untilBoardJson(url);
|
|
2925
2960
|
assert.equal((await (await fetch(`${url}/board.json`)).json()).workspace, realpathSync(repo));
|
|
2926
2961
|
} finally {
|
|
2927
|
-
launch
|
|
2962
|
+
launch?.p.kill("SIGKILL");
|
|
2928
2963
|
server.close(() => {});
|
|
2929
2964
|
for (const d of [bin, repo, dir]) rmSync(d, { recursive: true, force: true });
|
|
2930
2965
|
}
|
|
@@ -2944,16 +2979,20 @@ test("CLI: a holder reporting a different workspace is not adopted — the launc
|
|
|
2944
2979
|
// PROBE_TIMEOUT_MS). That is the bound doing its job rather than a hang,
|
|
2945
2980
|
// and it is why this row is the only slow one here — the rows that need a
|
|
2946
2981
|
// holder to really answer use serveProcess(), which leaves the loop free.
|
|
2947
|
-
test("CLI: an exhausted derived range exits non-zero and names the ports it tried", async () => {
|
|
2982
|
+
test("CLI: an exhausted derived range exits non-zero and names the ports it tried", async (t) => {
|
|
2948
2983
|
const bin = gitOnlyPath(), repo = gitRepo("board-ws-full-");
|
|
2949
2984
|
const instance = resolveCockpitInstance({ cwd: repo, gitCommonDir: join(repo, ".git") });
|
|
2950
2985
|
const ports = cockpitPorts(instance);
|
|
2951
2986
|
const dir = boardDir(undefined);
|
|
2952
2987
|
const blockers = [];
|
|
2953
2988
|
try {
|
|
2954
|
-
//
|
|
2955
|
-
//
|
|
2956
|
-
for (const p of ports)
|
|
2989
|
+
// The range full of OUR sockets: a port something else already holds
|
|
2990
|
+
// can be let go before the launch binds, and the launch then serves.
|
|
2991
|
+
for (const p of ports) {
|
|
2992
|
+
const holder = await holderOn(dir, p);
|
|
2993
|
+
if (holder.port === null) return t.skip(heldOutside(p));
|
|
2994
|
+
blockers.push(holder.server);
|
|
2995
|
+
}
|
|
2957
2996
|
const r = serveSync(repo, bin, ["--interval", "3600"], 30000);
|
|
2958
2997
|
assert.equal(r.status, 2, `an exhausted range must refuse, not hang and not succeed: ${r.stderr}`);
|
|
2959
2998
|
for (const p of ports) assert.match(r.stderr, new RegExp(`\\b${p}\\b`), `the refusal does not name ${p}: ${r.stderr}`);
|
|
@@ -514,7 +514,7 @@ const FILES = [
|
|
|
514
514
|
{
|
|
515
515
|
path: ["scripts", "ledger.mjs"],
|
|
516
516
|
stale: [/fleet-plugin-design\.md:200/, /candidates\.mjs:279/],
|
|
517
|
-
live: ["
|
|
517
|
+
live: ["refuseIfCapped"],
|
|
518
518
|
},
|
|
519
519
|
{
|
|
520
520
|
path: ["scripts", "ledger.test.mjs"],
|