fix(browse): handoff() + launchHeaded() spread the shared STEALTH_LAUNCH_ARGS

handoff() built its launch args from only ['--hide-crash-restore-bubble',
...buildGStackLaunchArgs()], omitting STEALTH_LAUNCH_ARGS — so a handed-off
browser kept the --disable-blink-features=AutomationControlled tell that
launch() and launchHeaded() strip. launchHeaded() also hardcoded the flag as a
literal. Both now spread the shared constant, so the AutomationControlled flag
lives in one place across all three launch paths.

Tripwires: STEALTH_LAUNCH_ARGS spread into >= 3 sites (no inline literal) and
STEALTH_IGNORE_DEFAULT_ARGS wired into both persistent-context paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-06-18 01:26:48 -07:00
parent e4c372ed98
commit 21b37cca9f
2 changed files with 46 additions and 6 deletions
+36
View File
@@ -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);
});
});