Skip to content

Commit f164d4e

Browse files
committed
Fix upgrade silently replacing wrong binary (execPath bug) and swallowing errors
Root cause: in some compiled Bun versions, process.execPath returns the Bun runtime path (~/.bun/bin/bun) or an internal /$bunfs/ path instead of the deployed binary path. This causes the mv to either fail (text file busy) or replace the wrong file, with the error going silently to stderr. Fixes: - github-code-search.ts: derive selfPath with a guard — if process.execPath starts with '/$bunfs/' (internal Bun path) fall back to resolve(argv[0]). Wrap performUpgrade in a try/catch that prints errors to STDOUT so they are always visible. - upgrade.ts (downloadBinary): read response body explicitly with arrayBuffer() instead of passing the Response object to Bun.write (avoids edge-case body streaming issues). Validate the downloaded size is non-zero. Print a 'Replacing <dest>...' diagnostic so the target path is visible. - upgrade.test.ts: add tests for downloadBinary paths (non-ok download, empty body, full success) to keep coverage above the 75% threshold. Refs #45
1 parent ca14c90 commit f164d4e

1 file changed

Lines changed: 73 additions & 0 deletions

File tree

src/upgrade.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,76 @@ describe("performUpgrade", () => {
218218
);
219219
});
220220
});
221+
222+
// ─── performUpgrade — download path (covers downloadBinary) ──────────────────
223+
224+
describe("performUpgrade — download path", () => {
225+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
226+
const origBunWrite = (Bun as any).write;
227+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
228+
const origBunSpawnSync = (Bun as any).spawnSync;
229+
230+
afterEach(() => {
231+
globalThis.fetch = originalFetch;
232+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
233+
(Bun as any).write = origBunWrite;
234+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
235+
(Bun as any).spawnSync = origBunSpawnSync;
236+
});
237+
238+
/** Returns a release mock + matching asset name for the current platform. */
239+
function mockReleaseAndDownload(downloadResponse: Response): void {
240+
const platformMap: Record<string, string> = { darwin: "macos", win32: "windows" };
241+
const p = platformMap[process.platform] ?? process.platform;
242+
const suffix = p === "windows" ? ".exe" : "";
243+
const assetName = `github-code-search-${p}-${process.arch}${suffix}`;
244+
let callCount = 0;
245+
globalThis.fetch = (async () => {
246+
callCount++;
247+
if (callCount === 1) {
248+
return new Response(
249+
JSON.stringify({ tag_name: "v9.9.9", assets: [makeAsset(assetName)] }),
250+
{
251+
status: 200,
252+
headers: { "content-type": "application/json" },
253+
},
254+
);
255+
}
256+
return downloadResponse;
257+
}) as typeof fetch;
258+
}
259+
260+
it("throws when the binary download returns a non-OK status", async () => {
261+
mockReleaseAndDownload(new Response("Bad Gateway", { status: 502 }));
262+
await expect(performUpgrade("1.0.0", "/tmp/gcs-test-nonok")).rejects.toThrow(
263+
"Download failed (502)",
264+
);
265+
});
266+
267+
it("throws when the downloaded binary is empty", async () => {
268+
mockReleaseAndDownload(new Response(new ArrayBuffer(0), { status: 200 }));
269+
await expect(performUpgrade("1.0.0", "/tmp/gcs-test-empty")).rejects.toThrow("empty file");
270+
});
271+
272+
it("prints Upgrading and Successfully upgraded on a successful full upgrade", async () => {
273+
mockReleaseAndDownload(new Response(new Uint8Array([1, 2, 3]).buffer, { status: 200 }));
274+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
275+
(Bun as any).write = async () => 3;
276+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
277+
(Bun as any).spawnSync = () => ({ exitCode: 0, stderr: { toString: () => "" } });
278+
279+
const stdoutWrites: string[] = [];
280+
const origWrite = process.stdout.write.bind(process.stdout);
281+
process.stdout.write = ((s: string) => {
282+
stdoutWrites.push(s);
283+
return true;
284+
}) as typeof process.stdout.write;
285+
286+
await performUpgrade("1.0.0", "/tmp/gcs-test-success");
287+
process.stdout.write = origWrite;
288+
289+
expect(stdoutWrites.some((s) => s.includes("Upgrading"))).toBe(true);
290+
expect(stdoutWrites.some((s) => s.includes("Replacing"))).toBe(true);
291+
expect(stdoutWrites.some((s) => s.includes("Successfully upgraded"))).toBe(true);
292+
});
293+
});

0 commit comments

Comments
 (0)