Files
gstack/browse/test/rebrand-signed-bundle.test.ts
T
a30ca53c1a fix(browse): headed mode on macOS 26 — stop mutating the signed Chromium bundle, heal the ones we already broke (#2242, #2138, #2139)
The in-place rebrand rewrote the Chrome-for-Testing bundle's Info.plist
(global name replace — which also renamed CFBundleExecutable to a binary
that doesn't exist) and overwrote its Resources/*.icns, breaking the
codesign seal: GPU process exit_code=5, headed mode dead on macOS 26. The
mutation lived in the SHARED Playwright cache, so it also poisoned the
user's other Playwright projects.

Three layers land together: (1) the rebrand block is gone — branding lives
in the GStack Browser.app wrapper via GSTACK_CHROMIUM_PATH, with a tombstone
and a static tripwire (no plist/icns writes into the bundle; the tripwire
allows the read-only probe below); (2) a launch-time self-heal detects an
already-poisoned cache bundle, removes it, and errors with the exact
re-fetch command — covering deploy paths that never run migrations;
(3) migration v1.64.0.0 sweeps every cached bundle, removes poisoned ones,
and re-fetches clean Chromium immediately (migrations run after ./setup, so
without the re-fetch an upgrade would end with zero working browser).
Functionally verified against fixture caches: poisoned removed, clean
untouched, rerun no-op. Migration filename tracks the final VERSION at ship.

The #2242 watchdog half is the absorbed PR #2565 (thanks @Screddyice).
Tombstone/tripwire ported from time-attack/gstack (GStack 2); self-heal and
migration are ours.

Co-authored-by: Sina Matian <sina@time-attack.dev>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:48:06 -07:00

54 lines
2.2 KiB
TypeScript

/**
* #2242 bug 1 regression tripwire: never mutate the signed Chrome-for-Testing
* bundle.
*
* The old launchHeaded() "rebrand" ran a global
* `.replace(/Google Chrome for Testing/g, 'GStack Browser')` over the
* bundle's Info.plist — which renamed CFBundleExecutable to a binary that
* doesn't exist — and overwrote Resources/*.icns. Both writes broke the
* codesign seal: GPU process exit_code=5, headed mode dead on macOS 26
* (#2242, #2138, #2139).
*
* Static invariant (same pattern as cdp-session-cleanup.test.ts): the
* browser lifecycle code must contain NO write into the Chromium .app
* bundle. Branding lives in the wrapper .app / custom GBrowser build.
* These assertions fail on the pre-fix code.
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
const SRC = fs.readFileSync(
path.join(import.meta.dir, '..', 'src', 'browser-manager.ts'),
'utf-8',
);
describe('#2242: signed Chromium bundle is never mutated', () => {
test('no global Google-Chrome-for-Testing plist replace', () => {
expect(SRC).not.toContain("replace(/Google Chrome for Testing/g");
});
test('no Info.plist write into the Chromium bundle (reads allowed: self-heal probe)', () => {
// The old code built `Info.plist` under the bundle's Contents dir and
// wrote it back. Any reappearance of a WRITE is a regression. The
// launch-time self-heal legitimately READS the plist to detect bundles
// the old code already poisoned (EV4), so the path construction itself
// is allowed — writes into it are not.
expect(SRC).not.toMatch(/writeFileSync\(\s*chromePlist/);
const plistWrites = SRC.match(/writeFileSync\([^)]*[Pp]list/g) || [];
expect(plistWrites).toEqual([]);
});
test('no icon overwrite into the Chromium bundle Resources dir', () => {
expect(SRC).not.toMatch(/copyFileSync\([^)]*destIcon/);
expect(SRC).not.toContain("CFBundleIconFile");
});
test('the tombstone comment documenting why stays put', () => {
// If someone deletes the explanation, the next contributor reintroduces
// the mutation in good faith. Keep the why next to the where.
expect(SRC).toContain('#2242');
});
});