aboutsummaryrefslogtreecommitdiff
path: root/test/profile/browser-process-test.ts
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-26 20:51:43 +0530
committeramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-26 20:51:43 +0530
commit7d2256e584709e4b7c27ab83b61950f00def7046 (patch)
tree642775bd745cb59423e762f4df8ff6ea65520ac4 /test/profile/browser-process-test.ts
parent083012ef3ede192505a953b7779d0c916dcaa98f (diff)
fix(profile): resolve replaced binaries and surface real launch failures
Two failures made a profile launch report the wrong cause. Linux appends " (deleted)" to /proc/<pid>/exe once the running binary is unlinked, which is what a google-chrome package upgrade does while the browser stays open. detectRunningBrowser passed that literal string on as the executable path, so every launch tried to spawn "/opt/google/chrome/chrome (deleted)" and hit ENOENT. Executable paths are now validated against disk, with the marker stripped and argv[0] used as a fallback; a binary genuinely named "… (deleted)" still wins. spawn() reports a missing or non-executable binary asynchronously via an 'error' event rather than throwing, and that event was discarded — so openProfileWindow returned success, no window opened, and the caller blamed the browser 15s later with a sentinel timeout. The launch now awaits 'spawn'/'error', returns the real cause, and removes its handshake page on the failure path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'test/profile/browser-process-test.ts')
-rw-r--r--test/profile/browser-process-test.ts71
1 files changed, 67 insertions, 4 deletions
diff --git a/test/profile/browser-process-test.ts b/test/profile/browser-process-test.ts
index 29982e5..29b3ddf 100644
--- a/test/profile/browser-process-test.ts
+++ b/test/profile/browser-process-test.ts
@@ -9,9 +9,16 @@
*
* Run: npx tsx test/profile/browser-process-test.ts
*/
-import { homedir } from "node:os";
+import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
-import { parseUserDataDirFlag, rankBrowserCandidate } from "../../src/profile/browser-process";
+import {
+ parseUserDataDirFlag,
+ rankBrowserCandidate,
+ resolveBrowserExecutable,
+ spawnableExePath,
+ stripDeletedSuffix,
+} from "../../src/profile/browser-process";
let passed = 0;
let failed = 0;
@@ -25,7 +32,7 @@ const check = (cond: boolean, label: string): void => {
}
};
-function main(): void {
+async function main(): Promise<void> {
// B1: --user-data-dir parsing
{
check(
@@ -101,8 +108,64 @@ function main(): void {
);
}
+ // B6: Linux's deleted-binary marker. An in-place `google-chrome` upgrade
+ // unlinks the running binary, after which /proc/<pid>/exe reads
+ // "…/chrome (deleted)" — a path spawn() rejects with ENOENT.
+ {
+ check(
+ stripDeletedSuffix("/opt/google/chrome/chrome (deleted)") === "/opt/google/chrome/chrome",
+ "B6: marker stripped from a replaced binary's path",
+ );
+ check(stripDeletedSuffix("/opt/google/chrome/chrome") === undefined, "B6: unmarked path left alone");
+ check(stripDeletedSuffix(" (deleted)") === undefined, "B6: marker alone is not a path");
+ }
+
+ // B7: only a path that is actually on disk may be handed on as spawnable.
+ {
+ const root = mkdtempSync(join(tmpdir(), "pi-exe-"));
+ try {
+ const real = join(root, "chrome");
+ writeFileSync(real, "#!/bin/sh\n");
+ chmodSync(real, 0o755);
+
+ check(await spawnableExePath(real) === real, "B7: an existing path is returned unchanged");
+ check(
+ await spawnableExePath(`${real} (deleted)`) === real,
+ "B7: a replaced binary resolves to the live install path",
+ );
+ check(await spawnableExePath(join(root, "absent")) === undefined, "B7: a missing path yields undefined");
+ check(await spawnableExePath("") === undefined, "B7: an empty path yields undefined");
+
+ // The launch gate must never pass on a path it cannot spawn — that is
+ // what produced a 15s "couldn't open a window" timeout instead of a
+ // named error.
+ check(
+ await resolveBrowserExecutable({ running: true, exePath: `${real} (deleted)` }) === real,
+ "B7: resolveBrowserExecutable repairs a replaced-binary path",
+ );
+ if (process.platform !== "win32") {
+ check(
+ await resolveBrowserExecutable({ running: true, exePath: join(root, "absent") }) === undefined,
+ "B7: an unspawnable path is not passed on as usable",
+ );
+ }
+
+ // A binary genuinely named "… (deleted)" must win over the stripped form.
+ // Created last, so the checks above see only the ordinary case.
+ const literal = join(root, "chrome (deleted)");
+ writeFileSync(literal, "#!/bin/sh\n");
+ chmodSync(literal, 0o755);
+ check(
+ await spawnableExePath(literal) === literal,
+ "B7: a real file named '… (deleted)' is preferred over stripping",
+ );
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ }
+
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed === 0 ? 0 : 1);
}
-main();
+void main();