feat(config): add resolveGstackHome, resolveChromiumProfile, cleanSingletonLocks

Three new exported helpers in browse/src/config.ts:

- resolveGstackHome(): honors GSTACK_HOME env, falls back to os.homedir()/.gstack
  Matches the existing convention in browse/src/telemetry.ts:26 and
  browse/src/domain-skills.ts:66.

- resolveChromiumProfile(explicit?): explicit arg wins -> CHROMIUM_PROFILE env
  -> resolveGstackHome()/chromium-profile. Lets gbrowser pass per-workspace
  profile paths through ServerConfig instead of relying on ambient env state.

- cleanSingletonLocks(dir): removes SingletonLock/Socket/Cookie via safeUnlinkQuiet.
  Defensive guard refuses to operate unless dir basename is 'chromium-profile'
  OR matches explicit CHROMIUM_PROFILE env value, preventing accidental
  deletion in unrelated directories.

Extends browse/test/config.test.ts with 12 tests covering env precedence,
guard behavior, ENOENT swallowing, and CHROMIUM_PROFILE override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-11 23:19:58 -07:00
parent 74895062fb
commit bed1a9f5ed
2 changed files with 186 additions and 1 deletions
+130 -1
View File
@@ -1,5 +1,5 @@
import { describe, test, expect } from 'bun:test';
import { resolveConfig, ensureStateDir, readVersionHash, getGitRoot, getRemoteSlug } from '../src/config';
import { resolveConfig, ensureStateDir, readVersionHash, getGitRoot, getRemoteSlug, resolveGstackHome, resolveChromiumProfile, cleanSingletonLocks } from '../src/config';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -314,3 +314,132 @@ describe('startup error log', () => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
});
describe('resolveGstackHome', () => {
test('honors GSTACK_HOME env var when set', () => {
const orig = process.env.GSTACK_HOME;
process.env.GSTACK_HOME = '/tmp/custom-gstack-home';
try {
expect(resolveGstackHome()).toBe('/tmp/custom-gstack-home');
} finally {
if (orig === undefined) delete process.env.GSTACK_HOME;
else process.env.GSTACK_HOME = orig;
}
});
test('falls back to os.homedir() + /.gstack when env unset', () => {
const orig = process.env.GSTACK_HOME;
delete process.env.GSTACK_HOME;
try {
expect(resolveGstackHome()).toBe(path.join(os.homedir(), '.gstack'));
} finally {
if (orig !== undefined) process.env.GSTACK_HOME = orig;
}
});
});
describe('resolveChromiumProfile', () => {
test('explicit arg wins over env and default', () => {
const orig = process.env.CHROMIUM_PROFILE;
process.env.CHROMIUM_PROFILE = '/tmp/env-profile';
try {
expect(resolveChromiumProfile('/tmp/explicit-profile')).toBe('/tmp/explicit-profile');
} finally {
if (orig === undefined) delete process.env.CHROMIUM_PROFILE;
else process.env.CHROMIUM_PROFILE = orig;
}
});
test('CHROMIUM_PROFILE env honored when no explicit arg', () => {
const orig = process.env.CHROMIUM_PROFILE;
process.env.CHROMIUM_PROFILE = '/tmp/env-profile';
try {
expect(resolveChromiumProfile()).toBe('/tmp/env-profile');
} finally {
if (orig === undefined) delete process.env.CHROMIUM_PROFILE;
else process.env.CHROMIUM_PROFILE = orig;
}
});
test('falls back to resolveGstackHome()/chromium-profile when nothing set', () => {
const origEnv = process.env.CHROMIUM_PROFILE;
const origHome = process.env.GSTACK_HOME;
delete process.env.CHROMIUM_PROFILE;
process.env.GSTACK_HOME = '/tmp/fallback-gstack';
try {
expect(resolveChromiumProfile()).toBe('/tmp/fallback-gstack/chromium-profile');
} finally {
if (origEnv !== undefined) process.env.CHROMIUM_PROFILE = origEnv;
if (origHome === undefined) delete process.env.GSTACK_HOME;
else process.env.GSTACK_HOME = origHome;
}
});
test('ignores empty-string explicit arg, falls through to env/default', () => {
const orig = process.env.CHROMIUM_PROFILE;
process.env.CHROMIUM_PROFILE = '/tmp/env-profile';
try {
expect(resolveChromiumProfile('')).toBe('/tmp/env-profile');
} finally {
if (orig === undefined) delete process.env.CHROMIUM_PROFILE;
else process.env.CHROMIUM_PROFILE = orig;
}
});
});
describe('cleanSingletonLocks', () => {
test('removes SingletonLock/Socket/Cookie when basename is chromium-profile', () => {
const tmpDir = path.join(os.tmpdir(), `clean-locks-${Date.now()}`, 'chromium-profile');
fs.mkdirSync(tmpDir, { recursive: true });
for (const f of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
fs.writeFileSync(path.join(tmpDir, f), 'stale');
}
cleanSingletonLocks(tmpDir);
for (const f of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
expect(fs.existsSync(path.join(tmpDir, f))).toBe(false);
}
fs.rmSync(path.dirname(tmpDir), { recursive: true, force: true });
});
test('refuses to clean unrecognized profile dir basename', () => {
const tmpDir = path.join(os.tmpdir(), `unrelated-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
const lockFile = path.join(tmpDir, 'SingletonLock');
fs.writeFileSync(lockFile, 'should-survive');
const origWarn = console.warn;
let warned = '';
console.warn = (msg: string) => { warned = msg; };
try {
cleanSingletonLocks(tmpDir);
expect(warned).toContain('refusing to clean unrecognized profile dir');
expect(fs.existsSync(lockFile)).toBe(true); // not deleted
} finally {
console.warn = origWarn;
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('respects explicit CHROMIUM_PROFILE env even with non-standard basename', () => {
const tmpDir = path.join(os.tmpdir(), `custom-name-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
fs.writeFileSync(path.join(tmpDir, 'SingletonLock'), 'stale');
const orig = process.env.CHROMIUM_PROFILE;
process.env.CHROMIUM_PROFILE = tmpDir;
try {
cleanSingletonLocks(tmpDir);
expect(fs.existsSync(path.join(tmpDir, 'SingletonLock'))).toBe(false);
} finally {
if (orig === undefined) delete process.env.CHROMIUM_PROFILE;
else process.env.CHROMIUM_PROFILE = orig;
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('second call on empty dir does not throw (ENOENT swallowed)', () => {
const tmpDir = path.join(os.tmpdir(), `empty-locks-${Date.now()}`, 'chromium-profile');
fs.mkdirSync(tmpDir, { recursive: true });
expect(() => cleanSingletonLocks(tmpDir)).not.toThrow();
expect(() => cleanSingletonLocks(tmpDir)).not.toThrow();
fs.rmSync(path.dirname(tmpDir), { recursive: true, force: true });
});
});