diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index 05178dc1e..94fcc3f5c 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -437,12 +437,13 @@ export class BrowserManager { // Find the gstack extension directory for auto-loading const extensionPath = this.findExtensionPath(); - const { buildGStackLaunchArgs } = await import('./stealth'); + const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth'); const launchArgs = [ '--hide-crash-restore-bubble', - // Anti-bot-detection: remove the navigator.webdriver flag that Playwright sets. - // Sites like Google and NYTimes check this to block automation browsers. - '--disable-blink-features=AutomationControlled', + // Anti-bot-detection: --disable-blink-features=AutomationControlled (and any + // future blink-level tells) via the shared STEALTH_LAUNCH_ARGS constant — the + // same flag launch() and handoff() use, kept in one place instead of a literal. + ...STEALTH_LAUNCH_ARGS, // GStack Pack 1: per-install hardware/GPU/UA-CH overrides for the // C++ patches in gbrowser's Chromium build. Each switch is a no-op // on Chromium builds without the corresponding patch (the patch's @@ -1541,8 +1542,11 @@ export class BrowserManager { const fs = require('fs'); const path = require('path'); const extensionPath = this.findExtensionPath(); - const { buildGStackLaunchArgs } = await import('./stealth'); - const launchArgs: string[] = ['--hide-crash-restore-bubble', ...buildGStackLaunchArgs()]; + const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth'); + // Same blink-level stealth flags as launch()/launchHeaded(). Without + // STEALTH_LAUNCH_ARGS the handed-off browser kept the AutomationControlled + // tell that the other two paths strip. + const launchArgs: string[] = ['--hide-crash-restore-bubble', ...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()]; if (extensionPath) { launchArgs.push(`--disable-extensions-except=${extensionPath}`); launchArgs.push(`--load-extension=${extensionPath}`); diff --git a/browse/test/browser-manager-unit.test.ts b/browse/test/browser-manager-unit.test.ts index 7156212c6..711ecb832 100644 --- a/browse/test/browser-manager-unit.test.ts +++ b/browse/test/browser-manager-unit.test.ts @@ -253,4 +253,40 @@ describe('stealth injected on every launch path', () => { const handoffBody = src.slice(handoffStart, resumeAnchor > 0 ? resumeAnchor : handoffStart + 4000); expect(handoffBody).toContain('applyStealth('); }); + + it('buildGStackLaunchArgs() is spread into all 3 launch sites', async () => { + // Same silent-drop regression class as applyStealth: a launch path that + // omits buildGStackLaunchArgs() loses the per-install GPU/UA/hardware + // cmdline spoof. launch(), launchHeaded(), and handoff() must all call it. + const { readFileSync } = await import('node:fs'); + const { join } = await import('node:path'); + const src = readFileSync(join(import.meta.dir, '..', 'src', 'browser-manager.ts'), 'utf-8'); + const callSites = src.match(/buildGStackLaunchArgs\(\)/g) || []; + expect(callSites.length).toBeGreaterThanOrEqual(3); + }); + + it('STEALTH_LAUNCH_ARGS is spread into all 3 launch sites (no hardcoded literal)', async () => { + // The --disable-blink-features=AutomationControlled flag must come from the + // shared constant on launch(), launchHeaded(), AND handoff(). handoff() + // previously omitted it, leaving the AutomationControlled tell on the + // handed-off browser. No path may inline the literal instead. + const { readFileSync } = await import('node:fs'); + const { join } = await import('node:path'); + const src = readFileSync(join(import.meta.dir, '..', 'src', 'browser-manager.ts'), 'utf-8'); + const spreads = src.match(/\.\.\.STEALTH_LAUNCH_ARGS/g) || []; + expect(spreads.length).toBeGreaterThanOrEqual(3); + // The literal must not be reintroduced in a launchArgs array (it belongs in + // the STEALTH_LAUNCH_ARGS constant in stealth.ts, not inline here). + expect(src).not.toContain("'--disable-blink-features=AutomationControlled'"); + }); + + it('STEALTH_IGNORE_DEFAULT_ARGS is wired into both persistent-context paths', async () => { + // launchHeaded() and handoff() both launchPersistentContext and must strip + // Playwright's automation-tell defaults via the shared constant. + const { readFileSync } = await import('node:fs'); + const { join } = await import('node:path'); + const src = readFileSync(join(import.meta.dir, '..', 'src', 'browser-manager.ts'), 'utf-8'); + const sites = src.match(/ignoreDefaultArgs:\s*STEALTH_IGNORE_DEFAULT_ARGS/g) || []; + expect(sites.length).toBeGreaterThanOrEqual(2); + }); });