mirror of
https://github.com/earendil-works/pi.git
synced 2026-10-19 05:49:16 +08:00
The fd/ripgrep auto-download resolved the latest version through api.github.com, which shares the anonymous API quota (60 requests/hour per IP). Behind shared egress IPs (corporate proxies, CI runners, NAT) that quota is permanently exhausted by unrelated traffic, so the download failed with 'GitHub API error: 403' on every launch while the actual release download endpoint would have worked fine. Resolve the version from the /releases/latest web redirect instead: it carries the tag in the Location header, is not subject to the API quota, and lives on the same origin as the binary download itself. The redirect body is discarded so the connection can be reused, and the pinned fd version on darwin/x64 now skips the lookup entirely. Also improve download failure diagnostics: status messages include the error cause chain so transport errors surface their actionable detail instead of a bare 'fetch failed', and HTTP download errors include the failing URL. Fixes #8594 Co-authored-by: Terminator666666 <Terminator666666@users.noreply.github.com>
143 lines
4.2 KiB
TypeScript
143 lines
4.2 KiB
TypeScript
import type * as ChildProcess from "node:child_process";
|
|
import type * as Fs from "node:fs";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { ensureTool, getLatestVersion, type ToolStatus } from "../src/utils/tools-manager.ts";
|
|
|
|
const originalOffline = process.env.PI_OFFLINE;
|
|
|
|
vi.mock("fs", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof Fs>();
|
|
return {
|
|
...actual,
|
|
existsSync: vi.fn(() => false),
|
|
};
|
|
});
|
|
|
|
vi.mock("child_process", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof ChildProcess>();
|
|
return {
|
|
...actual,
|
|
spawnSync: vi.fn(() => ({ error: new Error("not found") })),
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalOffline === undefined) delete process.env.PI_OFFLINE;
|
|
else process.env.PI_OFFLINE = originalOffline;
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
function redirectResponse(location: string): Response {
|
|
return new Response(null, { status: 302, headers: { location } });
|
|
}
|
|
|
|
describe("getLatestVersion", () => {
|
|
it("resolves the version from the release page redirect", async () => {
|
|
const fetchMock = vi.fn(async () => redirectResponse("https://github.com/sharkdp/fd/releases/tag/v10.4.2"));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await expect(getLatestVersion("sharkdp/fd")).resolves.toBe("10.4.2");
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
"https://github.com/sharkdp/fd/releases/latest",
|
|
expect.objectContaining({ redirect: "manual" }),
|
|
);
|
|
});
|
|
|
|
it("keeps tags without a v prefix intact", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => redirectResponse("https://github.com/BurntSushi/ripgrep/releases/tag/15.2.0")),
|
|
);
|
|
|
|
await expect(getLatestVersion("BurntSushi/ripgrep")).resolves.toBe("15.2.0");
|
|
});
|
|
|
|
it("resolves relative redirect targets", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => redirectResponse("/sharkdp/fd/releases/tag/v10.4.2")),
|
|
);
|
|
|
|
await expect(getLatestVersion("sharkdp/fd")).resolves.toBe("10.4.2");
|
|
});
|
|
|
|
it("discards the redirect response body", async () => {
|
|
const response = new Response("<html></html>", {
|
|
status: 302,
|
|
headers: { location: "https://github.com/sharkdp/fd/releases/tag/v10.4.2" },
|
|
});
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => response),
|
|
);
|
|
|
|
await expect(getLatestVersion("sharkdp/fd")).resolves.toBe("10.4.2");
|
|
expect(response.bodyUsed).toBe(true);
|
|
});
|
|
|
|
it("fails clearly when the endpoint does not redirect", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => new Response("not found", { status: 404 })),
|
|
);
|
|
|
|
await expect(getLatestVersion("sharkdp/fd")).rejects.toThrow(
|
|
"Failed to resolve latest sharkdp/fd release: HTTP 404 without redirect",
|
|
);
|
|
});
|
|
|
|
it("fails clearly when the redirect does not point at a release tag", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => redirectResponse("https://github.com/login")),
|
|
);
|
|
|
|
await expect(getLatestVersion("sharkdp/fd")).rejects.toThrow(
|
|
"Failed to resolve latest sharkdp/fd release: unexpected redirect to https://github.com/login",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("ensureTool", () => {
|
|
it("reports status through a callback without writing to the console", async () => {
|
|
process.env.PI_OFFLINE = "1";
|
|
const statuses: ToolStatus[] = [];
|
|
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
|
|
const result = await ensureTool("fd", (status) => statuses.push(status));
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(statuses).toEqual([
|
|
{
|
|
type: "warning",
|
|
message: "fd not found. Offline mode enabled, skipping download.",
|
|
},
|
|
]);
|
|
expect(consoleLog).not.toHaveBeenCalled();
|
|
consoleLog.mockRestore();
|
|
});
|
|
|
|
it("surfaces the error cause chain when a download fails", async () => {
|
|
delete process.env.PI_OFFLINE;
|
|
const cause = new Error("connect ETIMEDOUT 140.82.113.3:443");
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => {
|
|
throw new TypeError("fetch failed", { cause });
|
|
}),
|
|
);
|
|
const statuses: ToolStatus[] = [];
|
|
|
|
const result = await ensureTool("fd", (status) => statuses.push(status));
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(statuses).toEqual([
|
|
{ type: "info", message: "fd not found. Downloading..." },
|
|
{
|
|
type: "warning",
|
|
message: "Failed to download fd: fetch failed: connect ETIMEDOUT 140.82.113.3:443",
|
|
},
|
|
]);
|
|
});
|
|
});
|