mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-27 15:11:47 +02:00
* 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
175 lines
12 KiB
TypeScript
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');
|
|
}
|
|
});
|
|
});
|