Merge codex/gstack-2 into gstack2-runtime-integration

Reconcile the four integrated v2 runtime implementations (unified execution
result contract, execution profiles, capability readiness, GitHub security)
with main's browser-provider hardening.

Conflict resolutions:
- runtimeContract() generator: keep new execution-result + doctor-capability
  paragraphs, adopt main's `[matching browser flags]` fallback wording;
  regenerate the six RUNTIME.md.
- package.json: keep the strict isolated test:gstack2 runner and marked 18.0.6
  security bump; adopt main's playwright-core alias.
- bun.lock: regenerated via bun install.
- release-hardening.test.ts: adopt main's browser-provider assertions
  (resolveServerLaunchTarget, --browser managed smoke loop).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sinabina
2026-07-21 13:15:03 -07:00
co-authored by Claude Opus 4.8
160 changed files with 4397 additions and 522 deletions
+34 -1
View File
@@ -7,7 +7,7 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { startTestServer } from './test-server';
import { BrowserManager } from '../src/browser-manager';
import { BrowserManager, assertHeadedBrowserProvider, configuredChromiumExecutable } from '../src/browser-manager';
import { resolveServerScript } from '../src/cli';
import { handleReadCommand as _handleReadCommand, parseOutArgs, hasOutArg, resultToString } from '../src/read-commands';
import { handleWriteCommand as _handleWriteCommand } from '../src/write-commands';
@@ -23,6 +23,25 @@ const handleReadCommand = (cmd: string, args: string[], b: BrowserManager) =>
const handleWriteCommand = (cmd: string, args: string[], b: BrowserManager) =>
_handleWriteCommand(cmd, args, b.getActiveSession(), b);
describe('configuredChromiumExecutable', () => {
test('returns and trims an explicitly selected system browser', () => {
expect(configuredChromiumExecutable({
GSTACK_CHROMIUM_PATH: ' /Applications/Google Chrome.app/Contents/MacOS/Google Chrome ',
})).toBe('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
});
test('keeps the managed-browser path when no override is selected', () => {
expect(configuredChromiumExecutable({})).toBeUndefined();
expect(configuredChromiumExecutable({ GSTACK_CHROMIUM_PATH: ' ' })).toBeUndefined();
});
test('rejects headed launch when setup selected an installed system browser', () => {
expect(() => assertHeadedBrowserProvider({ GSTACK_BROWSER_PROVIDER: 'installed' }))
.toThrow('Visible GStack Browser requires managed Chromium');
expect(() => assertHeadedBrowserProvider({ GSTACK_BROWSER_PROVIDER: 'managed' })).not.toThrow();
});
});
// ─── Pure arg-parser + result-conversion unit tests (no browser) ───
describe('parseOutArgs / hasOutArg', () => {
test('--out <path> splits the flag from the positional', () => {
@@ -459,6 +478,20 @@ describe('Interaction', () => {
}
}, 15000);
test('click on a missing selector does not start a second locator wait', async () => {
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
const started = performance.now();
try {
await handleWriteCommand('click', ['#definitely-missing-regression-node'], bm);
expect(true).toBe(false); // Should not reach here
} catch (err: any) {
expect(err.message).toContain('#definitely-missing-regression-node');
}
// click() intentionally retains Playwright's 5s auto-wait. The regression
// was a second default locator wait that pushed the total beyond 8s.
expect(performance.now() - started).toBeLessThan(6500);
}, 8000);
test('hover works', async () => {
const result = await handleWriteCommand('hover', ['h1'], bm);
expect(result).toContain('Hovered');
@@ -92,6 +92,47 @@ describe('D2 daemon-mismatch refuse (CLI integration)', () => {
}
}, 15000);
test('refuses to reuse a same-version daemon from a different browser provider', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-provider-mismatch-'));
const stateFile = path.join(tmpDir, 'browse.json');
const fakeServer = await startFakeHealthServer('fake-token');
const { computeConfigHash } = await import('../src/proxy-config');
const managedHash = computeConfigHash({
proxyUrl: null,
headed: false,
browserProvider: 'managed',
});
fs.writeFileSync(stateFile, JSON.stringify({
pid: process.pid,
port: fakeServer.port,
token: 'fake-token',
startedAt: new Date().toISOString(),
serverPath: '',
mode: 'launched',
configHash: managedHash,
}, null, 2));
const cliEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) cliEnv[key] = value;
}
cliEnv.BROWSE_STATE_FILE = stateFile;
cliEnv.GSTACK_BROWSER_PROVIDER = 'installed';
cliEnv.GSTACK_CHROMIUM_PATH = process.execPath;
try {
const result = await runCli(['status'], cliEnv);
expect(result.code).toBe(1);
expect(result.stderr).toContain('different config');
expect(result.stderr).toContain('browse disconnect');
} finally {
await fakeServer.close();
try { fs.unlinkSync(stateFile); } catch { /* ignore */ }
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}, 15000);
test('refuses when existing plain daemon meets a --proxy invocation', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-plain-'));
const stateFile = path.join(tmpDir, 'browse.json');
+16
View File
@@ -186,4 +186,20 @@ describe('extractGlobalFlags', () => {
);
expect(a.configHash).not.toBe(b.configHash);
});
test('configHash changes with browser provider and installed executable', () => {
const managed = extractGlobalFlags(['goto', 'x'], {
GSTACK_BROWSER_PROVIDER: 'managed',
} as NodeJS.ProcessEnv);
const installedA = extractGlobalFlags(['goto', 'x'], {
GSTACK_BROWSER_PROVIDER: 'installed',
GSTACK_CHROMIUM_PATH: '/browser/a',
} as NodeJS.ProcessEnv);
const installedB = extractGlobalFlags(['goto', 'x'], {
GSTACK_BROWSER_PROVIDER: 'installed',
GSTACK_CHROMIUM_PATH: '/browser/b',
} as NodeJS.ProcessEnv);
expect(managed.configHash).not.toBe(installedA.configHash);
expect(installedA.configHash).not.toBe(installedB.configHash);
});
});