mirror of https://github.com/garrytan/gstack.git
Merge 94b7895ab3 into 2beb636f7c
This commit is contained in:
commit
b9819950bc
|
|
@ -803,15 +803,14 @@ export class BrowserManager {
|
|||
this.tabSessions.delete(tabId);
|
||||
this.tabOwnership.delete(tabId);
|
||||
|
||||
// Switch to another tab if we closed the active one
|
||||
if (tabId === this.activeTabId) {
|
||||
// Never leave zero tabs, regardless of which tab was closed — activeTabId
|
||||
// can point at an already-removed id, and the old active-only guard then
|
||||
// skipped auto-create when the last tab closed.
|
||||
if (this.pages.size === 0) {
|
||||
await this.newTab();
|
||||
} else if (tabId === this.activeTabId) {
|
||||
const remaining = [...this.pages.keys()];
|
||||
if (remaining.length > 0) {
|
||||
this.activeTabId = remaining[remaining.length - 1];
|
||||
} else {
|
||||
// No tabs left — create a new blank one
|
||||
await this.newTab();
|
||||
}
|
||||
this.activeTabId = remaining[remaining.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -243,6 +243,14 @@ export interface ServerConfig {
|
|||
* reference is the PID record + the file paths.
|
||||
*/
|
||||
ownsTerminalAgent?: boolean;
|
||||
/**
|
||||
* Process-exit seam. shutdown() calls this at the very end of its async
|
||||
* teardown; defaults to process.exit. In-process test runs inject a
|
||||
* recording stub here — the fire-and-forget shutdown path otherwise races
|
||||
* per-test process.exit stubs, and a straggler real exit(0) truncates the
|
||||
* whole bun test run with a green exit code and no failure tally.
|
||||
*/
|
||||
exitFn?: (code?: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -664,6 +672,11 @@ export const __testInternals__ = {
|
|||
// need shutdown to fire. Without this, the second test's shutdown
|
||||
// returns early at the `if (isShuttingDown) return;` guard.
|
||||
resetShutdownState: () => { isShuttingDown = false; },
|
||||
// Stop the module-level 60s idle interval. In-process test runs import this
|
||||
// module once for the whole suite; the interval otherwise outlives the test
|
||||
// file and can fire a real process.exit(0) mid-suite, silently truncating
|
||||
// the run with a green exit code.
|
||||
stopIdleTimer: () => { clearInterval(idleCheckInterval); },
|
||||
};
|
||||
|
||||
// ─── Parent-Process Watchdog ────────────────────────────────────────
|
||||
|
|
@ -1489,6 +1502,9 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
|
|||
// to gstack-owns. Matches the "default-true preserves CLI bit-for-bit"
|
||||
// premise even under malformed cfg.
|
||||
const ownsTerminalAgent = cfg.ownsTerminalAgent === false ? false : true;
|
||||
// Exit seam: production defaults to process.exit; tests inject a stub so
|
||||
// in-process shutdowns can never terminate the test runner itself.
|
||||
const exitFn = cfg.exitFn ?? ((code?: number) => process.exit(code));
|
||||
|
||||
// ─── Terminal-Agent Watchdog (v1.44+) ─────────────────────────────
|
||||
//
|
||||
|
|
@ -1614,7 +1630,7 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
|
|||
|
||||
cleanSingletonLocks(resolveChromiumProfile());
|
||||
safeUnlinkQuiet(config.stateFile);
|
||||
process.exit(exitCode);
|
||||
exitFn(exitCode);
|
||||
}
|
||||
|
||||
// Named lifecycle helper (matches closeTunnel style). Logs failures so
|
||||
|
|
|
|||
|
|
@ -42,9 +42,16 @@ beforeAll(async () => {
|
|||
// The test needs to start a server. Let's use the existing server infrastructure.
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
try { testServer.server.stop(); } catch {}
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
// Close the real browser (capped at 5s — close can hang) instead of a
|
||||
// delayed process.exit(0): that timer fired ~500ms into the NEXT test
|
||||
// file in a full-suite run and killed the whole bun process with a green
|
||||
// exit code and no tally, silently truncating the run.
|
||||
await Promise.race([
|
||||
bm.close().catch(() => {}),
|
||||
new Promise<void>(r => setTimeout(r, 5000)),
|
||||
]);
|
||||
});
|
||||
|
||||
// We need a running browse server for HTTP tests.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { startTestServer } from './test-server';
|
|||
import { BrowserManager } from '../src/browser-manager';
|
||||
|
||||
const TMP_HOME = path.join(os.tmpdir(), `gstack-cdp-e2e-${process.pid}-${Date.now()}`);
|
||||
const ORIG_GSTACK_HOME = process.env.GSTACK_HOME;
|
||||
process.env.GSTACK_HOME = TMP_HOME;
|
||||
process.env.GSTACK_TELEMETRY_OFF = '1'; // don't pollute analytics during tests
|
||||
|
||||
|
|
@ -36,6 +37,11 @@ beforeAll(async () => {
|
|||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Un-leak GSTACK_HOME: it outranks GSTACK_STATE_DIR in bin/gstack-config,
|
||||
// so leaving it set breaks later test files in-process.
|
||||
if (ORIG_GSTACK_HOME === undefined) delete process.env.GSTACK_HOME;
|
||||
else process.env.GSTACK_HOME = ORIG_GSTACK_HOME;
|
||||
|
||||
try { await bm.cleanup?.(); } catch {}
|
||||
try { testServer.server.stop(); } catch {}
|
||||
await fs.rm(TMP_HOME, { recursive: true, force: true });
|
||||
|
|
|
|||
|
|
@ -94,11 +94,18 @@ beforeAll(async () => {
|
|||
await bm.launch();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
// Force kill browser instead of graceful close (avoids hang)
|
||||
try { testServer.server.stop(); } catch {}
|
||||
// bm.close() can hang — just let process exit handle it
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
// Close the real browser (capped at 5s — close can hang) instead of a
|
||||
// delayed process.exit(0): that timer fired ~500ms into the NEXT test
|
||||
// file in a full-suite run and killed the whole bun process with a green
|
||||
// exit code and no tally, silently truncating the run.
|
||||
await Promise.race([
|
||||
bm.close().catch(() => {}),
|
||||
new Promise<void>(r => setTimeout(r, 5000)),
|
||||
]);
|
||||
});
|
||||
|
||||
// ─── Navigation ─────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -69,10 +69,17 @@ beforeAll(async () => {
|
|||
await handleWriteCommand('goto', [boardUrl], bm);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
try { server.stop(); } catch {}
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
// Close the real browser (capped at 5s — close can hang) instead of a
|
||||
// delayed process.exit(0): that timer fired ~500ms into the NEXT test
|
||||
// file in a full-suite run and killed the whole bun process with a green
|
||||
// exit code and no tally, silently truncating the run.
|
||||
await Promise.race([
|
||||
bm.close().catch(() => {}),
|
||||
new Promise<void>(r => setTimeout(r, 5000)),
|
||||
]);
|
||||
});
|
||||
|
||||
// ─── DOM Structure ──────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -460,9 +460,16 @@ describe('Hidden element stripping', () => {
|
|||
await bm.launch();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
try { testServer.server.stop(); } catch {}
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
// Close the real browser (capped at 5s — close can hang) instead of a
|
||||
// delayed process.exit(0): that timer fired ~500ms into the NEXT test
|
||||
// file in a full-suite run and killed the whole bun process with a green
|
||||
// exit code and no tally, silently truncating the run.
|
||||
await Promise.race([
|
||||
bm.close().catch(() => {}),
|
||||
new Promise<void>(r => setTimeout(r, 5000)),
|
||||
]);
|
||||
});
|
||||
|
||||
test('detects CSS-hidden elements on injection-hidden page', async () => {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { startTestServer } from './test-server';
|
|||
import { BrowserManager } from '../src/browser-manager';
|
||||
|
||||
const TMP_HOME = path.join(os.tmpdir(), `gstack-domain-e2e-${process.pid}-${Date.now()}`);
|
||||
const ORIG_GSTACK_HOME = process.env.GSTACK_HOME;
|
||||
process.env.GSTACK_HOME = TMP_HOME;
|
||||
process.env.GSTACK_PROJECT_SLUG = 'e2e-test-slug';
|
||||
|
||||
|
|
@ -41,6 +42,11 @@ beforeAll(async () => {
|
|||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Un-leak GSTACK_HOME: it outranks GSTACK_STATE_DIR in bin/gstack-config,
|
||||
// so leaving it set breaks later test files in-process.
|
||||
if (ORIG_GSTACK_HOME === undefined) delete process.env.GSTACK_HOME;
|
||||
else process.env.GSTACK_HOME = ORIG_GSTACK_HOME;
|
||||
|
||||
try { await bm.cleanup?.(); } catch {}
|
||||
try { testServer.server.stop(); } catch {}
|
||||
await fs.rm(TMP_HOME, { recursive: true, force: true });
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||
import { describe, it, expect, beforeEach, afterAll } from 'bun:test';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
const TMP_HOME = path.join(os.tmpdir(), `gstack-test-${process.pid}-${Date.now()}`);
|
||||
const ORIG_GSTACK_HOME = process.env.GSTACK_HOME;
|
||||
process.env.GSTACK_HOME = TMP_HOME;
|
||||
|
||||
afterAll(() => {
|
||||
// Un-leak GSTACK_HOME: it outranks GSTACK_STATE_DIR in bin/gstack-config,
|
||||
// so leaving it set breaks later test files in-process.
|
||||
if (ORIG_GSTACK_HOME === undefined) delete process.env.GSTACK_HOME;
|
||||
else process.env.GSTACK_HOME = ORIG_GSTACK_HOME;
|
||||
});
|
||||
|
||||
// Re-import after env var set so module reads updated GSTACK_HOME
|
||||
async function freshImport() {
|
||||
// Bun caches modules; force reload by appending a query-string-like hack via dynamic import URL
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ function run(args: string[] = [], extraEnv: Record<string, string> = {}) {
|
|||
const result = Bun.spawnSync(['bash', SCRIPT, ...args], {
|
||||
env: {
|
||||
...process.env,
|
||||
// Set all three state-dir vars: other test files leak GSTACK_HOME /
|
||||
// GSTACK_STATE_ROOT into process.env at module scope, and those
|
||||
// outrank GSTACK_STATE_DIR in the script's resolution order.
|
||||
GSTACK_STATE_DIR: stateDir,
|
||||
GSTACK_STATE_ROOT: stateDir,
|
||||
GSTACK_HOME: stateDir,
|
||||
...extraEnv,
|
||||
},
|
||||
stdout: 'pipe',
|
||||
|
|
|
|||
|
|
@ -26,9 +26,16 @@ beforeAll(async () => {
|
|||
await bm.launch();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
try { testServer.server.stop(); } catch {}
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
// Close the real browser (capped at 5s — close can hang) instead of a
|
||||
// delayed process.exit(0): that timer fired ~500ms into the NEXT test
|
||||
// file in a full-suite run and killed the whole bun process with a green
|
||||
// exit code and no tally, silently truncating the run.
|
||||
await Promise.race([
|
||||
bm.close().catch(() => {}),
|
||||
new Promise<void>(r => setTimeout(r, 5000)),
|
||||
]);
|
||||
});
|
||||
|
||||
// ─── Unit Tests: Failure Tracking (no browser needed) ────────────
|
||||
|
|
|
|||
|
|
@ -56,9 +56,16 @@ describe('defense-in-depth — live Playwright fixture', () => {
|
|||
await bm.launch();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
try { testServer.server.stop(); } catch {}
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
// Close the real browser (capped at 5s — close can hang) instead of a
|
||||
// delayed process.exit(0): that timer fired ~500ms into the NEXT test
|
||||
// file in a full-suite run and killed the whole bun process with a green
|
||||
// exit code and no tally, silently truncating the run.
|
||||
await Promise.race([
|
||||
bm.close().catch(() => {}),
|
||||
new Promise<void>(r => setTimeout(r, 5000)),
|
||||
]);
|
||||
});
|
||||
|
||||
test('L2 — content-security.ts hidden-element stripper detects the .sneaky div', async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, test, expect, beforeEach, mock } from 'bun:test';
|
||||
import { describe, test, expect, beforeAll, beforeEach, afterAll, mock } from 'bun:test';
|
||||
import {
|
||||
resolveConfigFromEnv,
|
||||
buildFetchHandler,
|
||||
|
|
@ -411,6 +411,27 @@ function makeMockBrowserManager(mode: 'launched' | 'headed') {
|
|||
}
|
||||
|
||||
describe('idle timer + onDisconnect dual-instance fix', () => {
|
||||
// File-scoped process.exit guard. The shutdown path is fire-and-forget
|
||||
// async: a test's per-call exit stub can be restored while a second
|
||||
// shutdown invocation is still in flight, and that straggler then hits the
|
||||
// REAL process.exit(0) — truncating the whole bun test run mid-suite with
|
||||
// a green exit code and no failure tally. Per-test stubs installed below
|
||||
// capture-and-restore against this file stub, never the real exit.
|
||||
const fileExitStub = mock((_code?: number) => {});
|
||||
let realProcessExit: typeof process.exit;
|
||||
beforeAll(() => {
|
||||
realProcessExit = process.exit;
|
||||
(process as any).exit = fileExitStub;
|
||||
});
|
||||
afterAll(async () => {
|
||||
// Kill the leaked module-level 60s interval so it can't fire shutdown
|
||||
// (with the real process.exit) during a later test file.
|
||||
__testInternals__.stopIdleTimer();
|
||||
// Grace period: let any straggler async shutdown land on the stub.
|
||||
await new Promise<void>(r => setTimeout(r, 300));
|
||||
(process as any).exit = realProcessExit;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__resetRegistry();
|
||||
// Reset module state every test. Bun memoizes the server.ts module
|
||||
|
|
@ -424,87 +445,68 @@ describe('idle timer + onDisconnect dual-instance fix', () => {
|
|||
});
|
||||
|
||||
test('CRITICAL — REGRESSION: headed embedder does not auto-shutdown at idle', () => {
|
||||
const exitMock = mock((_code?: number) => { throw new Error('process.exit called'); });
|
||||
const originalExit = process.exit;
|
||||
(process as any).exit = exitMock;
|
||||
try {
|
||||
const mockBM = makeMockBrowserManager('headed');
|
||||
buildFetchHandler(makeMinimalConfig({ browserManager: mockBM as any }));
|
||||
// Drive lastActivity past the idle threshold via the test seam instead
|
||||
// of mutating Date.now — the leaked module-level setInterval would
|
||||
// see fake-time and could fire shutdown if the timing aligned.
|
||||
__testInternals__.setLastActivity(Date.now() - (31 * 60 * 1000));
|
||||
__testInternals__.idleCheckTick();
|
||||
expect(exitMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
(process as any).exit = originalExit;
|
||||
}
|
||||
const exitSpy = mock((_code?: number) => {});
|
||||
const mockBM = makeMockBrowserManager('headed');
|
||||
buildFetchHandler(makeMinimalConfig({ browserManager: mockBM as any, exitFn: exitSpy }));
|
||||
// Drive lastActivity past the idle threshold via the test seam instead
|
||||
// of mutating Date.now — the leaked module-level setInterval would
|
||||
// see fake-time and could fire shutdown if the timing aligned.
|
||||
__testInternals__.setLastActivity(Date.now() - (31 * 60 * 1000));
|
||||
__testInternals__.idleCheckTick();
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('headless still auto-shuts down at idle (paired defensive)', async () => {
|
||||
// Non-throwing mock: idleCheckTick fires shutdown as a fire-and-forget
|
||||
// async call. Throwing from process.exit becomes an unhandled rejection
|
||||
// that the test runner catches. Recording the call is enough.
|
||||
const exitMock = mock((_code?: number) => {});
|
||||
const originalExit = process.exit;
|
||||
(process as any).exit = exitMock;
|
||||
try {
|
||||
const mockBM = makeMockBrowserManager('launched');
|
||||
buildFetchHandler(makeMinimalConfig({ browserManager: mockBM as any }));
|
||||
__testInternals__.setLastActivity(Date.now() - (31 * 60 * 1000));
|
||||
__testInternals__.idleCheckTick();
|
||||
// Drain microtasks: shutdown awaits flushBuffers + cfgBrowserManager.close
|
||||
// before reaching process.exit.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await new Promise<void>(r => setImmediate(r));
|
||||
await new Promise<void>(r => setImmediate(r));
|
||||
expect(exitMock).toHaveBeenCalled();
|
||||
} finally {
|
||||
(process as any).exit = originalExit;
|
||||
// Shutdown is fire-and-forget async and hard-calling process.exit from it
|
||||
// is what this seam exists to avoid: a per-test process.exit stub can be
|
||||
// restored while a straggler shutdown is still in flight, and the real
|
||||
// exit(0) then kills the whole bun run mid-suite with a green exit code.
|
||||
// The injected exitFn spy makes the assertion race-free and harmless.
|
||||
const exitSpy = mock((_code?: number) => {});
|
||||
const mockBM = makeMockBrowserManager('launched');
|
||||
buildFetchHandler(makeMinimalConfig({ browserManager: mockBM as any, exitFn: exitSpy }));
|
||||
__testInternals__.setLastActivity(Date.now() - (31 * 60 * 1000));
|
||||
__testInternals__.idleCheckTick();
|
||||
// Poll until the async shutdown reaches the exit seam.
|
||||
for (let i = 0; i < 500 && exitSpy.mock.calls.length === 0; i++) {
|
||||
await new Promise<void>(r => setTimeout(r, 10));
|
||||
}
|
||||
expect(exitSpy).toHaveBeenCalled();
|
||||
// Reset activity so the module-level 60s idle interval cannot re-fire
|
||||
// shutdown later in the suite.
|
||||
__testInternals__.setLastActivity(Date.now());
|
||||
});
|
||||
|
||||
test('buildFetchHandler chains cfgBrowserManager.onDisconnect, preserving caller-set handler', async () => {
|
||||
const mockBM = makeMockBrowserManager('headed');
|
||||
const callerCb = mock(async (_code?: number) => {});
|
||||
mockBM.onDisconnect = callerCb;
|
||||
buildFetchHandler(makeMinimalConfig({ browserManager: mockBM as any }));
|
||||
const exitSpy = mock((_code?: number) => {});
|
||||
buildFetchHandler(makeMinimalConfig({ browserManager: mockBM as any, exitFn: exitSpy }));
|
||||
// gstack should have wrapped the caller-installed handler instead of
|
||||
// clobbering it (Codex finding: BrowserManager.onDisconnect is a public
|
||||
// field; gbrowser may set it before calling buildFetchHandler).
|
||||
expect(typeof mockBM.onDisconnect).toBe('function');
|
||||
expect(mockBM.onDisconnect).not.toBe(callerCb);
|
||||
// Verify the chain: invoking the wrapped handler runs the caller
|
||||
// callback AND reaches activeShutdown (which calls process.exit at the
|
||||
// very end of its async path). Stubbing process.exit to throw aborts
|
||||
// the chain before isShuttingDown can leak into later tests.
|
||||
const exitMock = mock((_code?: number) => { throw new Error('process.exit called'); });
|
||||
const originalExit = process.exit;
|
||||
(process as any).exit = exitMock;
|
||||
try {
|
||||
await expect((mockBM.onDisconnect as any)(0)).rejects.toThrow('process.exit called');
|
||||
expect(callerCb).toHaveBeenCalledWith(0);
|
||||
expect(exitMock).toHaveBeenCalledWith(0);
|
||||
} finally {
|
||||
(process as any).exit = originalExit;
|
||||
// callback AND reaches activeShutdown, whose teardown ends at the
|
||||
// injected exit seam instead of the real process.exit.
|
||||
await (mockBM.onDisconnect as any)(0);
|
||||
for (let i = 0; i < 500 && exitSpy.mock.calls.length === 0; i++) {
|
||||
await new Promise<void>(r => setTimeout(r, 10));
|
||||
}
|
||||
expect(callerCb).toHaveBeenCalledWith(0);
|
||||
expect(exitSpy).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
test('tunnelActive blocks idle-shutdown even in headless mode', () => {
|
||||
const exitMock = mock((_code?: number) => { throw new Error('process.exit called'); });
|
||||
const originalExit = process.exit;
|
||||
(process as any).exit = exitMock;
|
||||
try {
|
||||
const mockBM = makeMockBrowserManager('launched');
|
||||
buildFetchHandler(makeMinimalConfig({ browserManager: mockBM as any }));
|
||||
__testInternals__.setTunnelActive(true);
|
||||
__testInternals__.setLastActivity(Date.now() - (31 * 60 * 1000));
|
||||
__testInternals__.idleCheckTick();
|
||||
expect(exitMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
(process as any).exit = originalExit;
|
||||
}
|
||||
const exitSpy = mock((_code?: number) => {});
|
||||
const mockBM = makeMockBrowserManager('launched');
|
||||
buildFetchHandler(makeMinimalConfig({ browserManager: mockBM as any, exitFn: exitSpy }));
|
||||
__testInternals__.setTunnelActive(true);
|
||||
__testInternals__.setLastActivity(Date.now() - (31 * 60 * 1000));
|
||||
__testInternals__.idleCheckTick();
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('lifecycle handlers (idleCheckTick + parent watchdog + SIGTERM) read activeBrowserManager, not module-level browserManager', () => {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,13 @@ beforeAll(async () => {
|
|||
await bm.launch();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
try { testServer.server.stop(); } catch {}
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
// Close the real browser instead of scheduling a delayed process.exit(0):
|
||||
// in a full-suite run that timer fired ~500ms into the NEXT test file and
|
||||
// killed the entire bun process with a green exit code and no tally —
|
||||
// silently truncating the run and hiding every failure after this file.
|
||||
await bm.close().catch(() => {});
|
||||
});
|
||||
|
||||
// ─── Snapshot Output ────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const TELEMETRY_FILE = path.join(TMP_HOME, 'analytics', 'browse-telemetry.jsonl'
|
|||
|
||||
// Use GSTACK_HOME env to redirect telemetry writes (read each call,
|
||||
// not cached at module-load).
|
||||
const ORIG_GSTACK_HOME = process.env.GSTACK_HOME;
|
||||
process.env.GSTACK_HOME = TMP_HOME;
|
||||
process.env.GSTACK_TELEMETRY_OFF = '0';
|
||||
|
||||
|
|
@ -17,6 +18,10 @@ beforeEach(async () => {
|
|||
|
||||
afterAll(async () => {
|
||||
await fs.rm(TMP_HOME, { recursive: true, force: true });
|
||||
// Un-leak GSTACK_HOME: it outranks GSTACK_STATE_DIR in bin/gstack-config's
|
||||
// resolution order, so leaving it set breaks later test files in-process.
|
||||
if (ORIG_GSTACK_HOME === undefined) delete process.env.GSTACK_HOME;
|
||||
else process.env.GSTACK_HOME = ORIG_GSTACK_HOME;
|
||||
});
|
||||
|
||||
async function readEvents(): Promise<any[]> {
|
||||
|
|
|
|||
|
|
@ -121,10 +121,15 @@ beforeAll(async () => {
|
|||
await bm.launch();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
try { server.stop(); } catch {}
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
// Close the browser (capped) instead of a delayed process.exit(0): that
|
||||
// timer killed the whole bun process mid-suite with a green exit code.
|
||||
await Promise.race([
|
||||
bm.close().catch(() => {}),
|
||||
new Promise(r => setTimeout(r, 5000)),
|
||||
]);
|
||||
});
|
||||
|
||||
// ─── The critical test: browser click → file on disk ─────────────
|
||||
|
|
|
|||
Loading…
Reference in New Issue