Initial import from garrytan/gstack@026751e (main snapshot via local relay)
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Source: https://github.com/garrytan/gstack/commit/026751e
This commit is contained in:
106
browse/test/server-no-import-side-effects.test.ts
Normal file
106
browse/test/server-no-import-side-effects.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
|
||||
/**
|
||||
* Guard the core refactor invariant: importing browse/src/server.ts must NOT
|
||||
* auto-start. Before this PR, the module called `start().catch(...)` at module
|
||||
* load time, which made the file impossible for embedders (gbrowser phoenix
|
||||
* overlay) to import without spawning a daemon. The fix wraps that kickoff in
|
||||
* `if (import.meta.main)` so the side effects only run when the module is the
|
||||
* process entry point.
|
||||
*
|
||||
* Approach: spawn a fresh Bun subprocess that imports the module and emits a
|
||||
* structured snapshot (initial vs post-import process state). Parent asserts
|
||||
* that no listeners were bound, no Bun.serve started, and no SIGINT handlers
|
||||
* were registered. The subprocess uses HOME=tmp + GSTACK_HOME=tmp so any
|
||||
* accidental state-dir write lands in a place we can verify is empty.
|
||||
*/
|
||||
describe('server.ts module import has no auto-start side effects', () => {
|
||||
test('importing server.ts does not bind Bun.serve, register signal handlers, or write state', async () => {
|
||||
const tmpHome = path.join(os.tmpdir(), `browse-no-sfx-${Date.now()}-${process.pid}`);
|
||||
fs.mkdirSync(tmpHome, { recursive: true });
|
||||
const tmpGstack = path.join(tmpHome, '.gstack');
|
||||
|
||||
const childScript = `
|
||||
const sigintBefore = process.listenerCount('SIGINT');
|
||||
const sigtermBefore = process.listenerCount('SIGTERM');
|
||||
const uncaughtBefore = process.listenerCount('uncaughtException');
|
||||
|
||||
// Snapshot any keys that look like our state path.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
await import(${JSON.stringify(path.resolve(import.meta.dir, '../src/server.ts'))});
|
||||
|
||||
// After import, sleep a tick so any setTimeout(0)-style init can run.
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
|
||||
const sigintAfter = process.listenerCount('SIGINT');
|
||||
const sigtermAfter = process.listenerCount('SIGTERM');
|
||||
const uncaughtAfter = process.listenerCount('uncaughtException');
|
||||
|
||||
// Check that the gstack home directory wasn't populated as a side effect.
|
||||
let gstackPopulated = false;
|
||||
try {
|
||||
const entries = fs.readdirSync(${JSON.stringify(tmpGstack)});
|
||||
gstackPopulated = entries.length > 0;
|
||||
} catch {
|
||||
// Doesn't exist — that's the win we want.
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
sigintBefore, sigintAfter,
|
||||
sigtermBefore, sigtermAfter,
|
||||
uncaughtBefore, uncaughtAfter,
|
||||
gstackPopulated,
|
||||
}));
|
||||
// Force exit so any background intervals don't keep this child alive
|
||||
// (the test framework would see a hang otherwise — which itself is a
|
||||
// signal that side effects DID run).
|
||||
process.exit(0);
|
||||
`;
|
||||
|
||||
const proc = Bun.spawn(['bun', '-e', childScript], {
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tmpHome,
|
||||
GSTACK_HOME: tmpGstack,
|
||||
// Empty so the AUTH_TOKEN env path doesn't deterministically set a token.
|
||||
AUTH_TOKEN: '',
|
||||
// Force a stub state file so resolveConfig() at module load (if it
|
||||
// happens) won't crawl the host's real .gstack/.
|
||||
BROWSE_STATE_FILE: path.join(tmpGstack, 'browse.json'),
|
||||
},
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
});
|
||||
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
await proc.exited;
|
||||
|
||||
// The last JSON line in stdout is our snapshot.
|
||||
const jsonLine = stdout.trim().split('\n').filter(l => l.startsWith('{')).pop();
|
||||
expect(jsonLine, `child stderr: ${stderr}`).toBeDefined();
|
||||
|
||||
const snapshot = JSON.parse(jsonLine!);
|
||||
|
||||
// No new signal handlers registered (gated on import.meta.main, which
|
||||
// is false in the subprocess because `bun -e` is the entry point).
|
||||
expect(snapshot.sigintAfter).toBe(snapshot.sigintBefore);
|
||||
expect(snapshot.sigtermAfter).toBe(snapshot.sigtermBefore);
|
||||
expect(snapshot.uncaughtAfter).toBe(snapshot.uncaughtBefore);
|
||||
|
||||
// gstack home should remain empty — initRegistry/initAuditLog/etc. side
|
||||
// effects from module load are acceptable (they happen at module level),
|
||||
// but only insofar as they don't bind listeners or write project state.
|
||||
// The presence/absence test here proves we didn't bind Bun.serve (which
|
||||
// would also try to write the state file).
|
||||
expect(snapshot.gstackPopulated).toBe(false);
|
||||
|
||||
// Cleanup
|
||||
try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch { /* best effort */ }
|
||||
}, 30_000);
|
||||
});
|
||||
Reference in New Issue
Block a user