test: add coverage for resolveServerScript, CONDUCTOR_PORT, setup rebuild

- Add withTempDir helper for test cleanup
- Test all 4 resolveServerScript paths (env var priority, dev mode, compiled binary, legacy fallback)
- Fix flaky CLI lifecycle test: strip CONDUCTOR_PORT to avoid port collision
- Add CONDUCTOR_PORT deterministic port derivation test
- Add setup script rebuild detection integration test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-12 07:34:38 -07:00
parent 0593ae0201
commit 806f32e2cd
+152 -13
View File
@@ -17,6 +17,15 @@ import * as fs from 'fs';
import { spawn } from 'child_process';
import * as path from 'path';
function withTempDir(fn: (root: string) => void): void {
const root = fs.mkdtempSync('/tmp/gstack-cli-');
try {
fn(root);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}
let testServer: ReturnType<typeof startTestServer>;
let bm: BrowserManager;
let baseUrl: string;
@@ -425,23 +434,62 @@ describe('Status', () => {
describe('CLI server script resolution', () => {
test('prefers adjacent browse/src/server.ts for compiled project installs', () => {
const root = fs.mkdtempSync('/tmp/gstack-cli-');
const execPath = path.join(root, '.claude/skills/gstack/browse/dist/browse');
const serverPath = path.join(root, '.claude/skills/gstack/browse/src/server.ts');
withTempDir((root) => {
const execPath = path.join(root, '.claude/skills/gstack/browse/dist/browse');
const serverPath = path.join(root, '.claude/skills/gstack/browse/src/server.ts');
fs.mkdirSync(path.dirname(execPath), { recursive: true });
fs.mkdirSync(path.dirname(serverPath), { recursive: true });
fs.writeFileSync(serverPath, '// test server\n');
fs.mkdirSync(path.dirname(execPath), { recursive: true });
fs.mkdirSync(path.dirname(serverPath), { recursive: true });
fs.writeFileSync(serverPath, '// test server\n');
const resolved = resolveServerScript(
{ HOME: path.join(root, 'empty-home') },
'$bunfs/root',
execPath
);
expect(resolved).toBe(serverPath);
});
});
test('BROWSE_SERVER_SCRIPT env var takes priority over valid dev-mode path', () => {
withTempDir((root) => {
const serverPath = path.join(root, 'server.ts');
fs.writeFileSync(serverPath, '// test server\n');
const resolved = resolveServerScript(
{ BROWSE_SERVER_SCRIPT: '/custom/server.ts' },
root,
'/fake/exec'
);
expect(resolved).toBe('/custom/server.ts');
});
});
test('dev mode resolves from metaDir when server.ts exists', () => {
withTempDir((root) => {
const serverPath = path.join(root, 'server.ts');
fs.writeFileSync(serverPath, '// test server\n');
const resolved = resolveServerScript(
{},
root,
'/fake/exec'
);
expect(resolved).toBe(serverPath);
});
});
test('falls back to $HOME legacy path when nothing matches', () => {
const resolved = resolveServerScript(
{ HOME: path.join(root, 'empty-home') },
{ HOME: '/fake/home' },
'$bunfs/root',
execPath
'/nonexistent/browse'
);
expect(resolved).toBe(serverPath);
fs.rmSync(root, { recursive: true, force: true });
expect(resolved).toBe('/fake/home/.claude/skills/gstack/browse/src/server.ts');
});
});
@@ -458,12 +506,13 @@ describe('CLI lifecycle', () => {
const cliPath = path.resolve(__dirname, '../src/cli.ts');
const result = await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
const { CONDUCTOR_PORT, BROWSE_PORT, ...cleanEnv } = process.env;
const proc = spawn('bun', ['run', cliPath, 'status'], {
timeout: 15000,
env: {
...process.env,
...cleanEnv,
BROWSE_STATE_FILE: stateFile,
BROWSE_PORT_START: '9520',
BROWSE_PORT_START: String(19000 + Math.floor(Math.random() * 500)),
},
});
let stdout = '';
@@ -486,6 +535,96 @@ describe('CLI lifecycle', () => {
expect(result.stdout).toContain('Status: healthy');
expect(result.stderr).toContain('Starting server');
}, 20000);
test('CONDUCTOR_PORT derives deterministic browse port', async () => {
const conductorPort = 64000;
const expectedBrowsePort = conductorPort - 45600; // 18400
const stateFile = `/tmp/browse-test-conductor-${Date.now()}.json`;
const cliPath = path.resolve(__dirname, '../src/cli.ts');
const { CONDUCTOR_PORT, BROWSE_PORT, ...cleanEnv } = process.env;
const result = await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
const proc = spawn('bun', ['run', cliPath, 'status'], {
timeout: 15000,
env: {
...cleanEnv,
CONDUCTOR_PORT: String(conductorPort),
BROWSE_STATE_FILE: stateFile,
},
});
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 }));
});
// Read state file to verify the server started on the derived port
let serverPort: number | null = null;
let serverPid: number | null = null;
if (fs.existsSync(stateFile)) {
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
serverPort = state.port;
serverPid = state.pid;
fs.unlinkSync(stateFile);
}
if (serverPid) {
try { process.kill(serverPid, 'SIGTERM'); } catch {}
}
expect(result.code).toBe(0);
expect(serverPort).toBe(expectedBrowsePort);
}, 20000);
});
// ─── Setup script rebuild detection ─────────────────────────────
describe('Setup script', () => {
const setupPath = path.resolve(__dirname, '../../setup');
const binaryPath = path.resolve(__dirname, '../dist/browse');
function runSetup(): Promise<{ code: number; stdout: string; stderr: string }> {
return new Promise((resolve) => {
const proc = spawn('bash', [setupPath], {
timeout: 60000,
cwd: path.resolve(__dirname, '../..'),
});
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 }));
});
}
test('skips rebuild when binary is fresh, rebuilds when source is newer', async () => {
// First run — ensure binary exists
const first = await runSetup();
expect(first.code).toBe(0);
expect(fs.existsSync(binaryPath)).toBe(true);
const freshMtime = fs.statSync(binaryPath).mtimeMs;
// Second run — nothing changed, should skip
const second = await runSetup();
expect(second.code).toBe(0);
const output = second.stdout + second.stderr;
expect(output).not.toContain('Building');
// Touch a source file to trigger rebuild
const srcFile = path.resolve(__dirname, '../src/server.ts');
const now = new Date();
fs.utimesSync(srcFile, now, now);
// Third run — source is newer, should rebuild
const third = await runSetup();
expect(third.code).toBe(0);
const rebuildOutput = third.stdout + third.stderr;
expect(rebuildOutput).toContain('Building');
const rebuiltMtime = fs.statSync(binaryPath).mtimeMs;
expect(rebuiltMtime).toBeGreaterThan(freshMtime);
}, 60000);
});
// ─── Buffer bounds ──────────────────────────────────────────────