mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-06 08:07:53 +02:00
fix(browse): apply stealth on every launch path + share automation-artifact cleanup
handoff() built cmdline args but never called applyStealth, so a handed-off browser had no JS stealth (no webdriver mask, no chrome.* shape, no toString proxy). And the cdc_/Permissions cleanup shim lived inline in launchHeaded() only, so headless launch() reported Notification.permission='default' without the matching permissions.query='prompt' answer — the exact cross-source inconsistency the shim exists to prevent. Move the cleanup into AUTOMATION_ARTIFACT_CLEANUP_SCRIPT inside applyStealth so all three launch paths (launch, launchHeaded, handoff) get identical stealth, and call applyStealth(newContext) in handoff() before restoreState() navigates. A static tripwire in browser-manager-unit.test.ts fails CI if any launch path drops the applyStealth call again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -227,3 +227,30 @@ describe('BrowserManager.onDisconnect exit-code propagation', () => {
|
||||
expect(shutdownCalls).toEqual([0, 2, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Stealth injected on EVERY launch path (regression tripwire) ───
|
||||
//
|
||||
// applyStealth must run on launch() (headless), launchHeaded(), AND
|
||||
// handoff(). The blend of Layer C with extended mode left handoff() building
|
||||
// cmdline args but never calling applyStealth, so a handed-off browser had
|
||||
// no JS stealth (no webdriver mask, no chrome.* shape, no toString proxy).
|
||||
// This static check fails CI if any launch path drops the call again.
|
||||
describe('stealth injected on every launch path', () => {
|
||||
it('handoff() calls applyStealth and there are >= 3 call sites', async () => {
|
||||
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');
|
||||
|
||||
// >= 3 total applyStealth call sites (launch, launchHeaded, handoff).
|
||||
const callSites = src.match(/applyStealth\(/g) || [];
|
||||
expect(callSites.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
// The handoff() method body specifically must call applyStealth, before
|
||||
// the resume() JSDoc that follows it.
|
||||
const handoffStart = src.indexOf('async handoff(');
|
||||
expect(handoffStart).toBeGreaterThan(0);
|
||||
const resumeAnchor = src.indexOf('Resume AI control after user handoff', handoffStart);
|
||||
const handoffBody = src.slice(handoffStart, resumeAnchor > 0 ? resumeAnchor : handoffStart + 4000);
|
||||
expect(handoffBody).toContain('applyStealth(');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('EXTENDED_STEALTH_SCRIPT — six detection-vector patches', () => {
|
||||
});
|
||||
|
||||
describe('applyStealth — script wiring', () => {
|
||||
test('default mode applies ONLY the Layer C script (not extended)', async () => {
|
||||
test('default mode applies Layer C + cleanup, not extended', async () => {
|
||||
delete process.env.GSTACK_STEALTH;
|
||||
const calls: string[] = [];
|
||||
const fakeCtx = {
|
||||
@@ -98,14 +98,19 @@ describe('applyStealth — script wiring', () => {
|
||||
},
|
||||
} as unknown as Parameters<typeof applyStealth>[0];
|
||||
await applyStealth(fakeCtx);
|
||||
expect(calls).toHaveLength(1);
|
||||
// Layer C signatures: toString-proxy native-code lie + webdriver mask.
|
||||
expect(calls).toHaveLength(2);
|
||||
// [0] = Layer C (toString-proxy native-code lie + webdriver mask).
|
||||
expect(calls[0]).toContain('[native code]');
|
||||
expect(calls[0]).toContain('webdriver');
|
||||
expect(calls[0]).not.toBe(EXTENDED_STEALTH_SCRIPT);
|
||||
// [1] = automation-artifact cleanup (cdc_ scan + Permissions shim) —
|
||||
// now applied on EVERY launch path, not just the headed one.
|
||||
expect(calls[1]).toContain('cdc_');
|
||||
expect(calls[1]).toContain('setTimeout(cleanup');
|
||||
// Extended script must NOT be applied by default.
|
||||
expect(calls).not.toContain(EXTENDED_STEALTH_SCRIPT);
|
||||
});
|
||||
|
||||
test('extended mode applies BOTH scripts in order (Layer C first, extended second)', async () => {
|
||||
test('extended mode applies Layer C, cleanup, then extended (in order)', async () => {
|
||||
process.env.GSTACK_STEALTH = 'extended';
|
||||
const calls: string[] = [];
|
||||
const fakeCtx = {
|
||||
@@ -114,9 +119,9 @@ describe('applyStealth — script wiring', () => {
|
||||
},
|
||||
} as unknown as Parameters<typeof applyStealth>[0];
|
||||
await applyStealth(fakeCtx);
|
||||
expect(calls).toHaveLength(2);
|
||||
// Layer C first (its native-code lie), extended second.
|
||||
expect(calls[0]).toContain('[native code]');
|
||||
expect(calls[1]).toBe(EXTENDED_STEALTH_SCRIPT);
|
||||
expect(calls).toHaveLength(3);
|
||||
expect(calls[0]).toContain('[native code]'); // Layer C first
|
||||
expect(calls[1]).toContain('cdc_'); // cleanup second
|
||||
expect(calls[2]).toBe(EXTENDED_STEALTH_SCRIPT); // extended last
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user