cli: release host.lock on a late fork or send throw; retry a vanishing lock (row 106, #1600)

Darkwing round 1, approved by Dewey in #1600 comment 28319 on manifest
ae38cbe280aa5885aec7648d38143920753db86ea1ca3934d0a8885fb61e5105.
Sage gate rerun on f63c965f9 plus build.patch: every package suite and
every scripts/test-*.sh green (cli 168, harness 73, test-release 14).
readHostState now treats a host.json that vanishes mid-read as no host
for stopHost and hostStatus too (Dewey note b); Sage accepts it as
correct. Nothing goes live until a re-pin under Mos's go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 18:01:02 -05:00
co-authored by Claude Opus 5.5
parent f63c965f99
commit 8b58cb7e47
5 changed files with 109 additions and 24 deletions
+15 -8
View File
@@ -5,7 +5,7 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
import { join } from "node:path";
import { fetchReview, git as runnerGit, HAVE_MAX } from "../src/git-tools.mjs";
@@ -82,23 +82,30 @@ test("fetch_review's have lists the working refs before origin's, and no more th
// Past HAVE_MAX the order is per clone: each clone's working refs newest
// first, one clone after another, so an earlier clone's older refs come
// ahead of a later clone's newer ones. The clones' order is the workspace's
// directory order, so the test takes it from the request.
// directory order, so the test reads it first and gives the first-listed
// clone the older refs: one order across clones by date would put the
// other clone first, whatever order the filesystem lists them in (row 106,
// #1600, Dewey b).
test("fetch_review's have orders the working refs newest first within each clone, one clone after another", async (t) => {
const dir = scratch(t);
const workspace = join(dir, "ws");
// 20 working refs in each clone; beta's are all newer than alpha's.
// Created in a shuffled order, so the name order isn't the date order.
mkdirSync(join(workspace, "alpha"), { recursive: true });
mkdirSync(join(workspace, "beta"));
const [early, late] = readdirSync(workspace);
// 20 working refs in each clone; the late clone's are all newer than the
// early one's. Created in a shuffled order, so the name order isn't the
// date order.
const order = [7, 2, 15, 0, 11, 19, 4, 9, 13, 1, 17, 6, 3, 18, 10, 5, 14, 8, 16, 12];
const refs = (task, base) => Object.fromEntries(order.map((i) => [`refs/remotes/working/work/vikunja-${task}-${i + 1}`, base + i * 60]));
const alpha = clone(join(workspace, "alpha"), refs(1, 1_700_000_000));
const beta = clone(join(workspace, "beta"), refs(2, 1_750_000_000));
const older = clone(join(workspace, early), refs(1, 1_700_000_000));
const newer = clone(join(workspace, late), refs(2, 1_750_000_000));
assert.deepEqual(readdirSync(workspace), [early, late], "the clones kept their directory order");
const host = await fakeHost(t, dir);
await assert.rejects(fetchReview({ path: host.path, cap: "c".repeat(64), workspace, task_ref: "vikunja:1/2", request: "r1", tmp: dir, env: ENV }), (e) => e.code === "request-not-found");
const [req] = host.lines;
// Newest first: the ref for i = 19 first, i = 0 last.
const newest = (oids, task) => order.slice().sort((x, y) => y - x).map((i) => oids[`refs/remotes/working/work/vikunja-${task}-${i + 1}`]);
const [first, second] = new Set(Object.values(alpha)).has(req.have[0]) ? [newest(alpha, 1), newest(beta, 2)] : [newest(beta, 2), newest(alpha, 1)];
assert.deepEqual(req.have, [...first, ...second].slice(0, HAVE_MAX));
assert.deepEqual(req.have, [...newest(older, 1), ...newest(newer, 2)].slice(0, HAVE_MAX));
});
// A git on PATH that runs `body` in place of the real one.