mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
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:
+152
-13
@@ -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 ──────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user