Files
gstack/browse/test/dia-gui-readiness.test.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

175 lines
12 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { createHash } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { chmodSync, copyFileSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { parseGuiReadiness, runGuiReadiness, validateGuiReadinessAuthority } from '../../.github/scripts/qualify-dia-macos';
const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'dia-gui-readiness-test-')));
const source = path.resolve(import.meta.dir, '../../.github/scripts/dia-gui-readiness.c');
const executable = path.join(root, 'metadata-probe');
const names = ['chrome', 'chromium', 'arc', 'dia', 'comet', 'brave', 'edge', 'safari', 'cookies'];
const observation = () => ({ protocol: 1, supported: true, identity: { effectiveUidMatches: true, homeMatchesRegistered: true },
security: { status: 0, graphicAccess: true, rootSession: false, tty: false, remote: false },
quartz: { present: true, sameUid: true, loginDone: true, onConsole: true }, browserRoots: null as any });
beforeAll(() => {
const wrapper = path.join(root, 'metadata-probe.c');
writeFileSync(wrapper, `#define main native_readiness_main\n#include ${JSON.stringify(source)}\n#undef main
int main(int argc, char **argv) {
if (argc != 2) return 2;
int home = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_NONBLOCK);
if (home < 0) return 2;
print_browser_roots(home, getuid()); close(home); printf("\\n"); return 0;
}`);
const compiler = Bun.which('clang');
if (!compiler) throw new Error('clang is required for the POSIX metadata regression');
const compiled = spawnSync(compiler, ['-std=c11', '-O2', '-Wall', '-Wextra', wrapper,
...(process.platform === 'darwin' ? ['-framework', 'Security', '-framework', 'ApplicationServices'] : []), '-o', executable],
{ encoding: 'utf8', timeout: 30_000 });
if (compiled.status !== 0) throw new Error('metadata_fixture_compile_failed');
});
afterAll(() => rmSync(root, { recursive: true, force: true }));
const inspect = (home: string) => {
const result = spawnSync(executable, [home], { encoding: 'utf8', timeout: 5000 });
expect(result.status).toBe(0);
expect(result.stderr).toBe('');
return JSON.parse(result.stdout);
};
describe('read-only GUI readiness', () => {
test('metadata inspection reports missing fixed roots without creating state', () => {
const home = realpathSync(mkdtempSync(path.join(root, 'empty-')));
const before = readdirSync(home);
const result = inspect(home);
expect(Object.keys(result)).toEqual(names);
for (const name of names) expect(result[name]).toEqual({ state: 'absent', kind: null, ownerMatches: null, ancestorBlocked: false });
expect(readdirSync(home)).toEqual(before);
expect(lstatSync(home).mode & 0o777).toBe(0o700);
});
test('metadata inspection never follows ancestor or target symlinks', () => {
const outside = realpathSync(mkdtempSync(path.join(root, 'other-')));
writeFileSync(path.join(outside, 'private-sentinel'), 'unchanged fixture contents');
const linked = realpathSync(mkdtempSync(path.join(root, 'linked-')));
symlinkSync(outside, path.join(linked, 'Library'), 'dir');
const blocked = inspect(linked);
for (const name of names) expect(blocked[name]).toEqual({ state: 'unavailable', kind: 'symlink', ownerMatches: true, ancestorBlocked: true });
const home = realpathSync(mkdtempSync(path.join(root, 'target-')));
mkdirSync(path.join(home, 'Library'));
symlinkSync(outside, path.join(home, 'Library/Safari'), 'dir');
expect(inspect(home).safari).toEqual({ state: 'present', kind: 'symlink', ownerMatches: true, ancestorBlocked: false });
expect(readdirSync(outside)).toEqual(['private-sentinel']);
expect(readFileSync(path.join(outside, 'private-sentinel'), 'utf8')).toBe('unchanged fixture contents');
});
test('wrong ancestor types and access failures are unavailable, never absent', () => {
const home = realpathSync(mkdtempSync(path.join(root, 'wrong-type-')));
writeFileSync(path.join(home, 'Library'), 'not a directory');
expect(inspect(home).dia).toEqual({ state: 'unavailable', kind: 'file', ownerMatches: true, ancestorBlocked: true });
const denied = realpathSync(mkdtempSync(path.join(root, 'denied-')));
const library = path.join(denied, 'Library');
mkdirSync(library, { mode: 0o700 });
chmodSync(library, 0);
try {
expect(inspect(denied).dia).toEqual({ state: 'unavailable', kind: 'directory', ownerMatches: true, ancestorBlocked: true });
expect(lstatSync(library).mode & 0o777).toBe(0);
} finally { chmodSync(library, 0o700); }
});
test('present root metadata does not open contents or change them', () => {
const home = realpathSync(mkdtempSync(path.join(root, 'present-')));
const safari = path.join(home, 'Library/Safari');
mkdirSync(safari, { recursive: true });
const file = path.join(safari, 'private-sentinel');
writeFileSync(file, 'private fixture content');
chmodSync(file, 0);
const before = lstatSync(file);
const result = inspect(home);
expect(result.safari).toEqual({ state: 'present', kind: 'directory', ownerMatches: true, ancestorBlocked: false });
expect(lstatSync(file).mode).toBe(before.mode);
expect(lstatSync(file).mtimeMs).toBe(before.mtimeMs);
expect(JSON.stringify(result)).not.toContain('private');
chmodSync(file, 0o600);
expect(readFileSync(file, 'utf8')).toBe('private fixture content');
});
test('GUI usability requires both the security capability and the caller-owned logged-in Quartz session', () => {
const original = observation();
const before = JSON.stringify(original);
Object.freeze(original.identity); Object.freeze(original.security); Object.freeze(original.quartz); Object.freeze(original);
expect(parseGuiReadiness(original, false).usableGui).toBe(true);
expect(JSON.stringify(original)).toBe(before);
for (const changed of [
{ ...observation(), security: { ...observation().security, graphicAccess: false } },
{ ...observation(), quartz: { present: false, sameUid: null, loginDone: null, onConsole: null } },
{ ...observation(), quartz: { present: true, sameUid: false, loginDone: null, onConsole: null } },
{ ...observation(), quartz: { ...observation().quartz, loginDone: false } },
{ ...observation(), identity: { effectiveUidMatches: true, homeMatchesRegistered: false } },
{ ...observation(), security: { status: -60500, graphicAccess: null, rootSession: null, tty: null, remote: null } },
]) expect(parseGuiReadiness(changed, false).usableGui).toBe(false);
});
test('schema rejects extra identity text, cross-UID session details and false absence claims', () => {
for (const changed of [
{ ...observation(), username: 'private-name' },
{ ...observation(), quartz: { ...observation().quartz, uid: 501 } },
{ ...observation(), quartz: { present: true, sameUid: false, loginDone: true, onConsole: true } },
{ ...observation(), security: { ...observation().security, status: -1 } },
{ ...observation(), security: { ...observation().security, graphicAccess: 1 } },
]) expect(() => parseGuiReadiness(changed, false)).toThrow('invalid_gui_readiness_receipt');
const roots = Object.fromEntries(names.map(name => [name, { state: 'absent', kind: null, ownerMatches: null, ancestorBlocked: false }]));
expect(parseGuiReadiness({ ...observation(), browserRoots: roots }, true).browserRoots).toEqual(roots);
expect(() => parseGuiReadiness({ ...observation(), browserRoots: roots }, false)).toThrow('invalid_gui_readiness_receipt');
expect(() => parseGuiReadiness({ ...observation(), browserRoots: { ...roots, dia: { state: 'absent', kind: 'symlink', ownerMatches: true, ancestorBlocked: true } } }, true))
.toThrow('invalid_gui_readiness_receipt');
});
test('GUI authority cannot carry browser/comparison authority or placeholder destination hashes', () => {
const account: any = { work: '/private/tmp/dn-fixture', guiReadiness: { mode: 'gui-readiness-only', executable: '/private/tmp/dn-fixture/bin/gui-readiness',
executableSha256: 'a'.repeat(64), sourceSha256: 'b'.repeat(64) } };
expect(() => validateGuiReadinessAuthority(account, 'coordinator')).not.toThrow();
for (const changed of [{ ...account, launchComparison: {} }, { ...account, destinationExecutable: '' }, { ...account, destinationSha256: 'a'.repeat(64) },
{ ...account, guiReadiness: null }, { ...account, guiReadiness: { ...account.guiReadiness, executableSha256: ['a'.repeat(64)] } },
{ ...account, guiReadiness: { ...account.guiReadiness, executable: '/unowned/probe' } }]) {
expect(() => validateGuiReadinessAuthority(changed, 'coordinator')).toThrow('gui_readiness_authority_invalid');
}
expect(() => validateGuiReadinessAuthority(account, 'comparison-driver')).toThrow('gui_readiness_authority_invalid');
});
test('the registered probe validates hashes, bounds execution, and discards unrecognized output', async () => {
const hash = (file: string) => createHash('sha256').update(readFileSync(file)).digest('hex');
const exeHash = hash(executable);
const sourceHash = hash(source);
const input = observation();
const execute = ((command: string, args: string[], options: any) => {
expect(command).toBe(executable); expect(args).toEqual([]);
expect(options.timeout).toBeGreaterThan(0); expect(options.timeout).toBeLessThanOrEqual(5000);
expect(options.killSignal).toBe('SIGKILL'); expect(options.maxBuffer).toBe(16 * 1024);
return { status: 0, stdout: JSON.stringify(input), stderr: 'private diagnostic text' };
}) as typeof spawnSync;
const result = await runGuiReadiness(executable, exeHash, source, sourceHash, false, { HOME: root }, 5000, execute);
expect(result.available).toBe(true);
expect(JSON.stringify(result)).not.toContain('private diagnostic');
await expect(runGuiReadiness(executable, 'c'.repeat(64), source, sourceHash, false, {}, 5000, execute)).rejects.toThrow('gui_readiness_inputs_changed');
const malformed = (() => ({ status: 0, stdout: JSON.stringify({ ...input, username: 'private-name' }), stderr: '' })) as typeof spawnSync;
expect((await runGuiReadiness(executable, exeHash, source, sourceHash, false, {}, 5000, malformed)).available).toBe(false);
for (const timeout of [0, NaN, Infinity]) await expect(runGuiReadiness(executable, exeHash, source, sourceHash, false, {}, timeout, execute)).rejects.toThrow('gui_readiness_budget_exhausted');
});
test('the readiness launcher loads without node_modules or Playwright and rejects conflicting CLI modes', () => {
const directory = path.join(root, 'no-dependencies/.github/scripts');
mkdirSync(directory, { recursive: true });
for (const file of ['run-dia-native-qualification.ts', 'qualify-dia-macos.ts']) copyFileSync(path.resolve(import.meta.dir, '../../.github/scripts', file), path.join(directory, file));
for (const args of [['--gui-readiness-only'], ['--gui-readiness-only', '--launch-comparison', 'node']]) {
const result = spawnSync(process.execPath, ['--no-env-file', '--no-install', '--no-macros', '--config=/dev/null', path.join(directory, 'run-dia-native-qualification.ts'), ...args],
{ cwd: root, env: { HOME: root, PATH: '/usr/bin:/bin' }, encoding: 'utf8', timeout: 5000 });
expect(result.status).toBe(2); expect(result.stderr).toBe('');
expect(JSON.parse(result.stdout).reason).toBe('fresh_account_launcher_preflight_failed');
}
});
});