Files
gstack/browse/test/cookie-import-native-qualification.ts
T
Garry Tan a84b0b5b6d v1.90.0.0 feat: make browser cookie imports explicit and safe (#2964)
* fix(browse): prepare reliable cookie import wave for validation

* ci: sequence quality and behavior for validation branch

* fix(browse): isolate Windows qualification and preserve native diagnostics

* test(browse): cover cookie workflow quality and isolate Windows user paths

* test(browse): trace native member startup and initialize fresh folders

* fix(browse): keep Windows member stdin alive through EOF

* fix(browse): latch native timeouts and compare contained Edge startup

* test(browse): verify native version metadata and actual Windows argv

* test(browse): qualify Dia import on isolated macOS CI

* fix(browse): require picker origin for session mutations

* fix(browse): bound credential reads through stream completion

* test(browse): inspect owned Windows process arguments natively

* test(evals): preserve passing coverage during cookie repair reruns

* test(browse): isolate Dia qualification in a fresh macOS account

* test(browse): pass bounded integer timeouts to native Mac probes

* test(browse): distinguish Windows profile initialization from containment

* test(browse): await descendant pipe readiness before parent exit

* test(browse): initialize and restore isolated macOS Keychain state

* test(browse): initialize Windows fixture folders before qualification

* test(ci): pin the same Node runtime across Windows checks

* test(browse): distinguish native macOS browser preflight stages

* test(browse): isolate Windows descendant console lifetime

* test(browse): preserve native receipts and identify fixture lock holders

* test(browse): prepare dependency resolution before native Mac worker startup

* test(ci): include lock and close checks in native diagnostics

* test(browse): preserve native owner probe stages and subprocess deadlines

* fix(browse): classify Chromium profile-in-use exit precisely

* test(browse): retain Mac qualification evidence through cleanup failures

* test(browse): bound Mac fixture paths and retire its owned user domain

* test(browse): accept vanished fixture entries without weakening cleanup

* test(browse): identify probe-created macOS user domains safely

* test(browse): observe Mac user domains without targeting them first

* test(browse): use passive fresh-user ownership throughout Mac qualification

* test(browse): distinguish profile and registered-home Keychain lookups

* test(browse): qualify Dia under one registered account home

* test(browse): identify Dia startup and owned process-group failures

* test(browse): classify bounded Dia startup diagnostics without leaking output

* fix(test): preserve native Mac sandboxing and reap owned browser children

* fix(browse): preserve Chromium sandboxing for native profile imports

* test(browse): inspect signed Mach-O architecture without launching Xcode tools

* test(browse): sample pending Dia startup and reap on all cleanup paths

* test(browse): compare protected Dia launches in fresh Bun and Node accounts

* test(browse): inspect isolated Mac GUI readiness without browser access

* v1.90.0.0 fix: bind cookie picker actions to their document

* test: validate cookie guards and fit nested launch fixtures

* ci: configure the bundled Chromium sandbox helper

* fix(browse): classify Playwright authentication timeouts

* test: retain bounded Windows lifecycle diagnostics

* test(cso): reuse bounded NTFS precision candidates

* test(review): handle explicit preservation choices safely

* test(browse): remove owned fixture directories with explicit primitives

* test(review): distinguish descriptive reuse from edit commitments

* test: admit only the approved unscored cookie workflow refusal

* test: keep the Office Hours judge mock export-complete

* fix: keep dependency-free CI planners independent of the model SDK

* test: observe the exact holder after a native fixture unlink failure

* fix: start seeded PTY observations at owned readiness

* test: acquire identity-bound Windows deletion admission before profile resets

* test: preserve qualified Git index bits without authorizing mutations
2026-09-25 12:06:45 -04:00

129 lines
8.0 KiB
TypeScript

import { spawnSync } from 'node:child_process';
import { existsSync, mkdtempSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { release, tmpdir } from 'node:os';
import path from 'node:path';
import { nativeBrowserPaths } from '../src/cookie-import-native';
import { nativeCookieEnvironment, probeNativeCookieMember } from '../src/cookie-import-native-worker';
import { hashNativeFile, nativeCodeHashes, nativeCodeMatches, NATIVE_BROWSER_VERSION_COMMAND, NATIVE_QUALIFICATION_DATA } from '../src/cookie-import-native-integrity';
import { createNativeCookieJob, NativeCookieJobError, nativeCookieDiagnostic, type NativeCookieDiagnostic, type NativeCookieJob } from '../src/cookie-import-native-job';
if (process.platform !== 'win32') {
console.error('Native cookie qualification requires Windows; no cases ran and no qualification was issued.');
process.exit(1);
}
if (process.env.GITHUB_ACTIONS !== 'true' || process.env.CI !== 'true') {
console.error('Native cookie qualification requires a disposable GitHub Actions runner and never uses an existing browser profile.');
process.exit(1);
}
const output = mkdtempSync(path.join(process.argv[2] || tmpdir(), 'cookie-native-qualification-'));
function incomplete(reason: string, diagnostic?: NativeCookieDiagnostic): never {
const receipt = { status: 'incomplete', reason, ...(diagnostic ? { diagnostic } : {}), counts: { pass: 0, fail: 0, skip: 0 }, activation: 'No build was qualified; production extraction remains disabled.' };
writeFileSync(path.join(output, 'qualification.json'), JSON.stringify(receipt, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
console.log(JSON.stringify({ ...receipt, artifactDirectory: output }));
process.exit(2);
}
if (Bun.version !== '1.4.0') incomplete('bun_1_4_0_required');
let emptyJob: NativeCookieJob | undefined;
try {
emptyJob = await createNativeCookieJob();
if (emptyJob.activeProcesses() !== 0) throw new NativeCookieJobError('job_query');
emptyJob.terminate();
if (emptyJob.activeProcesses() !== 0) throw new NativeCookieJobError('job_query');
emptyJob.close();
const preflight = { status: 'passed', activeProcesses: 0 };
writeFileSync(path.join(output, 'job-preflight.json'), JSON.stringify(preflight, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
console.log(JSON.stringify({ nativeJobPreflight: preflight }));
} catch (error) {
const diagnostic = nativeCookieDiagnostic(error, 'job_create');
try { emptyJob?.close(); } catch {}
writeFileSync(path.join(output, 'job-preflight.json'), JSON.stringify({ status: 'failed', diagnostic }, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
incomplete('native_job_preflight_failed', diagnostic);
}
const memberProbe = await probeNativeCookieMember();
const memberPreflight = 'cookies' in memberProbe ? { status: 'passed', activeProcesses: 0 } : { status: 'failed', error: memberProbe.error, diagnostic: memberProbe.diagnostic };
writeFileSync(path.join(output, 'member-preflight.json'), JSON.stringify(memberPreflight, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
console.log(JSON.stringify({ nativeMemberPreflight: memberPreflight }));
if ('error' in memberProbe) incomplete('native_member_preflight_failed', memberProbe.diagnostic);
const root = path.resolve(import.meta.dir, '../..');
let sourceHashes: Record<string, string>;
try {
sourceHashes = await nativeCodeHashes(root, Date.now() + 25_000);
} catch {
incomplete('source_inputs_unavailable_or_timed_out');
}
const environment = nativeCookieEnvironment(process.env);
const browser = nativeBrowserPaths('Edge', environment);
const executable = browser.executables.find(existsSync);
const node = Bun.which('node');
if (!executable || !node) incomplete('node_or_edge_not_installed');
if (existsSync(browser.userDataDir)) incomplete('existing_default_profile_refused');
const executableSha256 = await hashNativeFile(executable, Date.now() + 25_000);
const nodeProbe = spawnSync(node, ['-p', 'JSON.stringify({ version: process.version, architecture: process.arch })'], { env: environment, encoding: 'utf8', timeout: 10_000, windowsHide: true });
if (nodeProbe.status !== 0) incomplete('node_runtime_preflight_failed');
const nodeInfo = JSON.parse(nodeProbe.stdout);
if (nodeInfo.architecture !== process.arch || !['x64', 'arm64'].includes(process.arch)) incomplete('runtime_architecture_mismatch');
const require = createRequire(import.meta.url);
const playwrightVersion = require('playwright/package.json').version;
if (playwrightVersion !== '1.62.1') incomplete('playwright_1_62_1_required');
const versionProbe = spawnSync(path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'), [
'-NoProfile', '-NonInteractive', '-Command', NATIVE_BROWSER_VERSION_COMMAND,
], { env: { ...environment, GSTACK_QUALIFY_BROWSER_EXE: executable }, encoding: 'utf8', timeout: 10_000, windowsHide: true });
const versionErrorCode = (versionProbe.error as NodeJS.ErrnoException | undefined)?.code;
const versionMetadata = {
exitCode: versionProbe.status,
signal: versionProbe.signal,
spawnError: versionProbe.error ? (['ENOENT', 'EACCES', 'EPERM', 'ETIMEDOUT'].includes(versionErrorCode || '') ? versionErrorCode : 'spawn_failed') : undefined,
stdoutBytes: Buffer.byteLength(versionProbe.stdout || ''),
stderrBytes: Buffer.byteLength(versionProbe.stderr || ''),
versionValid: /^\d+(?:\.\d+){2,3}$/.test((versionProbe.stdout || '').trim()),
};
writeFileSync(path.join(output, 'browser-version-preflight.json'), JSON.stringify(versionMetadata, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
console.log(JSON.stringify({ nativeBrowserVersionPreflight: versionMetadata }));
if (versionProbe.status !== 0 || !versionMetadata.versionValid) incomplete('browser_version_preflight_failed');
const result = spawnSync(process.execPath, ['--no-env-file', '--no-install', '--no-macros', '--config=NUL', 'test', 'browse/test/cookie-import-native-job.test.ts', '--test-name-pattern', '^native Windows process qualification'], {
cwd: root,
env: { ...environment, CI: 'true', GITHUB_ACTIONS: 'true', GSTACK_COOKIE_NATIVE_DEFAULT_FIXTURE: '1', NO_COLOR: '1' },
encoding: 'utf8',
timeout: 300_000,
maxBuffer: 16 * 1024 * 1024,
windowsHide: true,
});
const log = `${result.stdout || ''}\n${result.stderr || ''}`;
writeFileSync(path.join(output, 'qualification.log'), log, { mode: 0o600, flag: 'wx' });
const count = (kind: string) => Number([...log.matchAll(new RegExp(`(?:^|\\n)\\s*(\\d+) ${kind}\\b`, 'g'))].at(-1)?.[1] ?? 0);
const counts = { pass: count('pass'), fail: count('fail'), skip: count('skip') };
let inputsUnchanged = false;
try {
const deadline = Date.now() + 25_000;
inputsUnchanged = await hashNativeFile(executable, deadline) === executableSha256 && nativeCodeMatches(sourceHashes, await nativeCodeHashes(root, deadline));
} catch {}
const passed = result.status === 0 && !result.error && counts.pass === 7 && counts.fail === 0 && counts.skip === 0 && inputsUnchanged && !existsSync(browser.userDataDir);
const receipt = {
status: passed ? 'passed' : 'failed',
scope: 'Edge default-profile v20 extraction, pipe transport, profile identity, and owned-process cleanup',
qualifiedBuild: {
browserName: 'Edge',
architecture: nodeInfo.architecture,
windowsRelease: release(),
executableSha256,
nodeVersion: nodeInfo.version,
bunVersion: Bun.version,
playwrightVersion,
sourceHashes,
},
browserVersion: versionProbe.stdout.trim(),
supervisorArchitecture: process.arch,
jobPreflight: { status: 'passed', activeProcesses: 0 },
memberPreflight,
counts,
inputsUnchanged,
sourceHashes,
activationData: NATIVE_QUALIFICATION_DATA,
activation: 'Review this receipt and add only this exact qualifiedBuild to the separate activation data; the runner never enables extraction and activation does not modify the recorded code hashes.',
};
writeFileSync(path.join(output, 'qualification.json'), JSON.stringify(receipt, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
console.log(JSON.stringify({ status: receipt.status, counts, artifactDirectory: output }));
process.exitCode = passed ? 0 : 1;