Files
pi/packages/coding-agent/test/tools-manager.test.ts
Terminator666666andTerminator666666 57e53b0d79 fix(coding-agent): resolve fd/rg release versions without the GitHub API (#8708)
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>
2026-09-03 10:59:04 +02:00

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",
},
]);
});
});