diff --git a/browse/test/commands.test.ts b/browse/test/commands.test.ts index 312b8cee..1f6ad2f2 100644 --- a/browse/test/commands.test.ts +++ b/browse/test/commands.test.ts @@ -457,14 +457,11 @@ describe('CLI lifecycle', () => { })); const cliPath = path.resolve(__dirname, '../src/cli.ts'); - // Build env without CONDUCTOR_PORT/BROWSE_PORT so BROWSE_PORT_START takes effect const cliEnv: Record = {}; for (const [k, v] of Object.entries(process.env)) { - if (k !== 'CONDUCTOR_PORT' && k !== 'BROWSE_PORT' && v !== undefined) cliEnv[k] = v; + if (v !== undefined) cliEnv[k] = v; } cliEnv.BROWSE_STATE_FILE = stateFile; - // Use a random high port to avoid conflicts with running servers - cliEnv.BROWSE_PORT_START = String(9600 + Math.floor(Math.random() * 100)); const result = await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => { const proc = spawn('bun', ['run', cliPath, 'status'], { timeout: 15000, diff --git a/browse/test/config.test.ts b/browse/test/config.test.ts new file mode 100644 index 00000000..780385f4 --- /dev/null +++ b/browse/test/config.test.ts @@ -0,0 +1,125 @@ +import { describe, test, expect } from 'bun:test'; +import { resolveConfig, ensureStateDir, readVersionHash, getGitRoot } from '../src/config'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +describe('config', () => { + describe('getGitRoot', () => { + test('returns a path when in a git repo', () => { + const root = getGitRoot(); + expect(root).not.toBeNull(); + expect(fs.existsSync(path.join(root!, '.git'))).toBe(true); + }); + }); + + describe('resolveConfig', () => { + test('uses git root by default', () => { + const config = resolveConfig({}); + const gitRoot = getGitRoot(); + expect(gitRoot).not.toBeNull(); + expect(config.projectDir).toBe(gitRoot); + expect(config.stateDir).toBe(path.join(gitRoot!, '.gstack')); + expect(config.stateFile).toBe(path.join(gitRoot!, '.gstack', 'browse.json')); + }); + + test('derives paths from BROWSE_STATE_FILE when set', () => { + const stateFile = '/tmp/test-config/.gstack/browse.json'; + const config = resolveConfig({ BROWSE_STATE_FILE: stateFile }); + expect(config.stateFile).toBe(stateFile); + expect(config.stateDir).toBe('/tmp/test-config/.gstack'); + expect(config.projectDir).toBe('/tmp/test-config'); + }); + + test('log paths are in stateDir', () => { + const config = resolveConfig({}); + expect(config.consoleLog).toBe(path.join(config.stateDir, 'browse-console.log')); + expect(config.networkLog).toBe(path.join(config.stateDir, 'browse-network.log')); + expect(config.dialogLog).toBe(path.join(config.stateDir, 'browse-dialog.log')); + }); + }); + + describe('ensureStateDir', () => { + test('creates directory if it does not exist', () => { + const tmpDir = path.join(os.tmpdir(), `browse-config-test-${Date.now()}`); + const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') }); + expect(fs.existsSync(config.stateDir)).toBe(false); + ensureStateDir(config); + expect(fs.existsSync(config.stateDir)).toBe(true); + // Cleanup + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + test('is a no-op if directory already exists', () => { + const tmpDir = path.join(os.tmpdir(), `browse-config-test-${Date.now()}`); + const stateDir = path.join(tmpDir, '.gstack'); + fs.mkdirSync(stateDir, { recursive: true }); + const config = resolveConfig({ BROWSE_STATE_FILE: path.join(stateDir, 'browse.json') }); + ensureStateDir(config); // should not throw + expect(fs.existsSync(config.stateDir)).toBe(true); + // Cleanup + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + }); + + describe('readVersionHash', () => { + test('returns null when .version file does not exist', () => { + const result = readVersionHash('/nonexistent/path/browse'); + expect(result).toBeNull(); + }); + + test('reads version from .version file adjacent to execPath', () => { + const tmpDir = path.join(os.tmpdir(), `browse-version-test-${Date.now()}`); + fs.mkdirSync(tmpDir, { recursive: true }); + const versionFile = path.join(tmpDir, '.version'); + fs.writeFileSync(versionFile, 'abc123def\n'); + const result = readVersionHash(path.join(tmpDir, 'browse')); + expect(result).toBe('abc123def'); + // Cleanup + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + }); +}); + +describe('resolveServerScript', () => { + // Import the function from cli.ts + const { resolveServerScript } = require('../src/cli'); + + test('uses BROWSE_SERVER_SCRIPT env when set', () => { + const result = resolveServerScript({ BROWSE_SERVER_SCRIPT: '/custom/server.ts' }, '', ''); + expect(result).toBe('/custom/server.ts'); + }); + + test('finds server.ts adjacent to cli.ts in dev mode', () => { + const srcDir = path.resolve(__dirname, '../src'); + const result = resolveServerScript({}, srcDir, ''); + expect(result).toBe(path.join(srcDir, 'server.ts')); + }); + + test('throws when server.ts cannot be found', () => { + expect(() => resolveServerScript({}, '/nonexistent/$bunfs', '/nonexistent/browse')) + .toThrow('Cannot find server.ts'); + }); +}); + +describe('version mismatch detection', () => { + test('detects when versions differ', () => { + const stateVersion = 'abc123'; + const currentVersion = 'def456'; + expect(stateVersion !== currentVersion).toBe(true); + }); + + test('no mismatch when versions match', () => { + const stateVersion = 'abc123'; + const currentVersion = 'abc123'; + expect(stateVersion !== currentVersion).toBe(false); + }); + + test('no mismatch when either version is null', () => { + const currentVersion: string | null = null; + const stateVersion: string | undefined = 'abc123'; + // Version mismatch only triggers when both are present + const shouldRestart = currentVersion !== null && stateVersion !== undefined && currentVersion !== stateVersion; + expect(shouldRestart).toBe(false); + }); +});