fix(cli): tighten Windows restart policy-close health checks

Preserve contributor credit and land the narrowed restart-health fix after ProjectClownfish review/follow-up.
This commit is contained in:
Vincent Koc
2026-04-27 04:38:29 -07:00
committed by GitHub
parent 3da6d6ee18
commit c85065eb7f
3 changed files with 80 additions and 12 deletions

View File

@@ -241,14 +241,60 @@ describe("inspectGatewayRestart", () => {
expect(snapshot.staleGatewayPids).toEqual([]);
});
it("treats auth-closed probe as healthy gateway reachability", async () => {
const snapshot = await inspectAmbiguousOwnershipWithProbe({
ok: false,
close: { code: 1008, reason: "auth required" },
});
it.each([
"auth required",
"owner auth required",
"connect failed",
"device required",
"pairing required",
"pairing required: device is asking for more scopes than currently approved",
"unauthorized: gateway token missing (set gateway.remote.token to match gateway.auth.token)",
"unauthorized: gateway password mismatch (set gateway.remote.password to match gateway.auth.password)",
"unauthorized: device token rejected (pair/repair this device, or provide gateway token)",
])(
"treats local policy-close probe reason %s as healthy gateway reachability",
async (reason) => {
const snapshot = await inspectAmbiguousOwnershipWithProbe({
ok: false,
close: { code: 1008, reason },
});
expect(snapshot.healthy).toBe(true);
});
expect(snapshot.healthy).toBe(true);
},
);
it.each([
"",
" ",
"repair required",
"repairing required",
"unpairing required",
"device",
"device required by local spoof",
"device required: identity missing",
"device identity required",
"connect challenge missing nonce",
"connect challenge timeout",
"authoritative policy close",
"device identity mismatch",
"device signature invalid",
"device nonce required",
"token expired",
"password required",
"missing scope: operator.admin",
"role denied",
"unauthorized: session revoked",
])(
"does not treat ambiguous 1008 close reason %s as healthy gateway reachability",
async (reason) => {
const snapshot = await inspectAmbiguousOwnershipWithProbe({
ok: false,
close: { code: 1008, reason },
});
expect(snapshot.healthy).toBe(false);
},
);
it("requires the expected gateway version when provided", async () => {
probeGateway.mockResolvedValue({

View File

@@ -83,12 +83,33 @@ function looksLikeAuthClose(code: number | undefined, reason: string | undefined
return false;
}
const normalized = normalizeLowercaseStringOrEmpty(reason);
if (!normalized) {
return false;
}
// The restart probe runs against loopback only and only decides restart
// liveness, not authorization. Keep this allowlist exact so a local listener
// cannot satisfy the health check with broad device/auth-looking text.
return (
normalized.includes("auth") ||
normalized.includes("token") ||
normalized.includes("password") ||
normalized.includes("scope") ||
normalized.includes("role")
normalized === "auth required" ||
normalized === "owner auth required" ||
normalized === "connect failed" ||
normalized === "device required" ||
normalized === "pairing required" ||
normalized.startsWith("pairing required:") ||
normalized.startsWith("unauthorized: gateway token missing") ||
normalized.startsWith("unauthorized: gateway token mismatch") ||
normalized.startsWith("unauthorized: gateway token not configured") ||
normalized.startsWith("unauthorized: gateway password missing") ||
normalized.startsWith("unauthorized: gateway password mismatch") ||
normalized.startsWith("unauthorized: gateway password not configured") ||
normalized.startsWith("unauthorized: bootstrap token invalid or expired") ||
normalized.startsWith("unauthorized: tailscale identity missing") ||
normalized.startsWith("unauthorized: tailscale proxy headers missing") ||
normalized.startsWith("unauthorized: tailscale identity check failed") ||
normalized.startsWith("unauthorized: tailscale identity mismatch") ||
normalized.startsWith("unauthorized: too many failed authentication attempts") ||
normalized.startsWith("unauthorized: device token mismatch") ||
normalized.startsWith("unauthorized: device token rejected")
);
}