Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions browse/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ async function ensureServer(): Promise<ServerState> {
}
}

// The lockfile lives under config.stateDir. Ensure the directory exists
// before taking the lock so first run in a fresh repo doesn't misread
// ENOENT as "another instance is starting".
ensureStateDir(config);

// Acquire lock to prevent concurrent restart races (TOCTOU)
const releaseLock = acquireServerLock();
if (!releaseLock) {
Expand Down
31 changes: 31 additions & 0 deletions browse/test/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,37 @@ describe('CLI server script resolution', () => {
// ─── CLI lifecycle ──────────────────────────────────────────────

describe('CLI lifecycle', () => {
test('missing default state dir triggers a clean first start', async () => {
const root = fs.mkdtempSync('/tmp/browse-first-start-');
const cliPath = path.resolve(__dirname, '../src/cli.ts');
const result = await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
const proc = spawn('bun', ['run', cliPath, 'status'], {
cwd: root,
timeout: 15000,
env: process.env,
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', (d) => stdout += d.toString());
proc.stderr.on('data', (d) => stderr += d.toString());
proc.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
});

let startedPid: number | null = null;
const stateFile = path.join(root, '.gstack', 'browse.json');
if (fs.existsSync(stateFile)) {
startedPid = JSON.parse(fs.readFileSync(stateFile, 'utf-8')).pid;
}
if (startedPid) {
try { process.kill(startedPid, 'SIGTERM'); } catch {}
}
fs.rmSync(root, { recursive: true, force: true });

expect(result.code).toBe(0);
expect(result.stdout).toContain('Status: healthy');
expect(result.stderr).toContain('Starting server');
}, 20000);

test('dead state file triggers a clean restart', async () => {
const stateFile = `/tmp/browse-test-state-${Date.now()}.json`;
fs.writeFileSync(stateFile, JSON.stringify({
Expand Down