fix(upgrade): doctor fetches tags before deciding latest + post-flight asserts served version (#173) (#174)

Two fixes for the two halves of issue #173, both from live incidents during the
2026-07-17 fleet update:

1. scripts/doctor.mjs — `git show origin/main:package.json` reads the LOCALLY
   CACHED remote ref; without a fetch first, any machine that hadn't pulled since
   the last release saw latest == current and reported "Already at latest" (live
   repro: Oracle VM at 3.21.1 with v3.22.1 released). The doctor now runs
   `git fetch --tags --quiet` (15s timeout) before comparing, gated on
   !opts.skipNetwork; on failure (offline/auth) it falls through to the cached
   ref — the pre-existing behavior. All existing doctor tests pass mockLatest +
   skipNetwork, so no test touches the network.

2. scripts/upgrade.mjs — post-flight accepted any healthy /health (auth.ok only),
   so a stale process holding the port passed post-flight while still serving the
   OLD version (live repro: a Jul-7 nohup-fallback orphan held :3456; upgrade
   "succeeded", /health kept serving 3.21.1). New exported predicate
   postFlightOk(body, target): auth.ok AND /health.version === target (leading-v
   tolerant; empty target degrades to the auth-only check, never blocks). The
   failure message now reports the last-seen version and points at the
   stale-process diagnosis (`ss -ltnp` / `lsof -i`).

Tests: +4, mutation-proven — reverting the predicate to auth-only fails the
"orphan case" test (337/1). Full suite 338 passed / 0 failed.

No server.mjs change — scripts layer only; no cli.js operation involved, so no
citation applies.

Closes #173

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude <claude-opus-4-8> <noreply@anthropic.com>
This commit is contained in:
dtzp555-max
2026-07-17 18:52:45 +10:00
committed by GitHub
co-authored by taodeng Claude <claude-opus-4-8> <noreply@anthropic.com>
parent bafad077ff
commit f14f4ec754
3 changed files with 57 additions and 3 deletions
+25 -1
View File
@@ -822,10 +822,34 @@ test("doctor falls back to currentVersion when origin/main unreachable (no stale
});
// ── Upgrade Tests ──
import { runUpgrade } from "./scripts/upgrade.mjs";
import { runUpgrade, postFlightOk } from "./scripts/upgrade.mjs";
console.log("\nUpgrade:");
// ── postFlightOk (issue #173) — the acceptance predicate for phase 6 ─────────
// Mutation-proof: revert the version comparison to auth-only and the "stale process
// still holds the port" test below goes green-to-red (that case is the 2026-07-17
// Oracle incident: orphan answered auth.ok=true while serving the OLD version).
test("postFlightOk: rejects a healthy-looking probe that serves the WRONG version (orphan case)", () => {
assert.equal(postFlightOk({ auth: { ok: true }, version: "3.21.1" }, "v3.22.1"), false);
});
test("postFlightOk: accepts auth.ok + exact target version, tolerating the leading v", () => {
assert.equal(postFlightOk({ auth: { ok: true }, version: "3.22.1" }, "v3.22.1"), true);
assert.equal(postFlightOk({ auth: { ok: true }, version: "3.22.1" }, "3.22.1"), true);
});
test("postFlightOk: auth failure rejects regardless of version", () => {
assert.equal(postFlightOk({ auth: { ok: false }, version: "3.22.1" }, "v3.22.1"), false);
assert.equal(postFlightOk({ version: "3.22.1" }, "v3.22.1"), false);
assert.equal(postFlightOk(null, "v3.22.1"), false);
});
test("postFlightOk: unknown/empty target degrades to the auth-only check (never blocks)", () => {
assert.equal(postFlightOk({ auth: { ok: true }, version: "3.22.1" }, ""), true);
assert.equal(postFlightOk({ auth: { ok: true }, version: "3.22.1" }, undefined), true);
});
test("upgrade --dry-run prints plan, no side effects", async () => {
const result = await runUpgrade({
dryRun: true,