fix(cli): report web readiness probe failures (#169)

Co-authored-by: Yaowei Zheng <hiyouga@buaa.edu.cn>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Laodouuu
2026-08-04 12:34:27 +08:00
committed by GitHub
parent 4d5d55d15a
commit e05dea536b
3 changed files with 244 additions and 15 deletions
+93 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import path from "node:path";
import { Command } from "commander";
import { DEFAULT_SERVER_PORT } from "@prismshadow/penguin-core";
@@ -8,8 +8,10 @@ import {
browserCommand,
browserUrl,
cliEntryFor,
describeReadinessFailure,
registerServeCommands,
resolvePort,
waitForReady,
} from "../src/commands/serve.js";
import { getMessages } from "../src/i18n.js";
@@ -93,3 +95,93 @@ describe("cliEntryFor (the entry advertised for the web self-update)", () => {
expect(cliEntryFor("")).toBeNull();
});
});
describe("readiness probe diagnostics", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("reports a successful HTTP response as ready regardless of its status", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(null, { status: 503 }));
await expect(waitForReady("http://127.0.0.1:7364/", 0, 0)).resolves.toEqual({
ready: true,
});
});
it("keeps polling after failed probes and reports ready once a response arrives", async () => {
vi.spyOn(globalThis, "fetch")
.mockRejectedValueOnce(new TypeError("fetch failed"))
.mockResolvedValue(new Response(null, { status: 200 }));
await expect(waitForReady("http://127.0.0.1:7364/", 5_000, 0)).resolves.toEqual({
ready: true,
});
});
it("retains the nested undici error from the last failed probe", async () => {
const cause = Object.assign(new Error("Connect Timeout Error"), {
code: "UND_ERR_CONNECT_TIMEOUT",
});
vi.spyOn(globalThis, "fetch").mockRejectedValue(
Object.assign(new TypeError("fetch failed"), { cause }),
);
await expect(waitForReady("http://127.0.0.1:7364/", 0, 0)).resolves.toEqual({
ready: false,
failure: {
kind: "timeout",
detail: "UND_ERR_CONNECT_TIMEOUT: Connect Timeout Error",
},
});
});
it.each([
["ECONNREFUSED", "refused"],
["ECONNRESET", "reset"],
["EACCES", "permission"],
["ENOTFOUND", "dns"],
] as const)("classifies %s failures as %s", (code, kind) => {
expect(describeReadinessFailure(Object.assign(new Error("probe failed"), { code }))).toEqual({
kind,
detail: `${code}: probe failed`,
});
});
it("classifies the probe's own 1s abort (DOMException TimeoutError) as a timeout", () => {
expect(
describeReadinessFailure(
new DOMException("The operation was aborted due to timeout", "TimeoutError"),
),
).toEqual({
kind: "timeout",
detail: "TimeoutError: The operation was aborted due to timeout",
});
});
it("digs the connect error out of an empty-message AggregateError (multi-address host)", () => {
const aggregate = Object.assign(
new AggregateError([
Object.assign(new Error("connect ECONNREFUSED ::1:7364"), { code: "ECONNREFUSED" }),
Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:7364"), { code: "ECONNREFUSED" }),
]),
{ code: "ECONNREFUSED" },
);
const error = Object.assign(new TypeError("fetch failed"), { cause: aggregate });
expect(describeReadinessFailure(error)).toEqual({
kind: "refused",
detail: "ECONNREFUSED: connect ECONNREFUSED ::1:7364",
});
});
it("includes actionable localized firewall guidance for connection timeouts", () => {
const detail = "UND_ERR_CONNECT_TIMEOUT: Connect Timeout Error";
expect(
getMessages("en").webProbeFailed("http://127.0.0.1:7364/", detail, "timeout", 7364),
).toContain("Allow PenguinHarness to communicate on local port 7364");
expect(
getMessages("zh").webProbeFailed("http://127.0.0.1:7364/", detail, "timeout", 7364),
).toContain("请允许 PenguinHarness 在本机端口 7364 上通信");
});
});