diff --git a/browse/test/commands.test.ts b/browse/test/commands.test.ts index 8e6325673..ffffa9c49 100644 --- a/browse/test/commands.test.ts +++ b/browse/test/commands.test.ts @@ -725,6 +725,47 @@ describe('CLI lifecycle', () => { expect(result.stdout).toContain('Status: healthy'); expect(result.stderr).toContain('Starting server'); }, 20000); + + test('sequential CLI invocations reuse the same daemon and page state', async () => { + const stateFile = `/tmp/browse-test-persist-${Date.now()}.json`; + const cliPath = path.resolve(__dirname, '../src/cli.ts'); + const cliEnv: Record = {}; + for (const [k, v] of Object.entries(process.env)) { + if (v !== undefined) cliEnv[k] = v; + } + cliEnv.BROWSE_STATE_FILE = stateFile; + + const runCli = (args: string[]) => + new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => { + const proc = spawn('bun', ['run', cliPath, ...args], { + timeout: 15000, + env: cliEnv, + }); + 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 })); + }); + + const gotoResult = await runCli(['goto', `${baseUrl}/basic.html`]); + expect(gotoResult.code).toBe(0); + expect(gotoResult.stdout).toContain('Navigated to'); + + const pid1 = JSON.parse(fs.readFileSync(stateFile, 'utf-8')).pid; + + const textResult = await runCli(['text']); + expect(textResult.code).toBe(0); + expect(textResult.stdout).toContain('Hello World'); + + const pid2 = JSON.parse(fs.readFileSync(stateFile, 'utf-8')).pid; + + try { fs.unlinkSync(stateFile); } catch {} + try { process.kill(pid2, 'SIGTERM'); } catch {} + + expect(pid1).toBe(pid2); + expect(textResult.stderr).not.toContain('Starting server'); + }, 20000); }); // ─── Buffer bounds ────────────────────────────────────────────── diff --git a/test/setup-codex-smoke.test.ts b/test/setup-codex-smoke.test.ts new file mode 100644 index 000000000..fdb456b5c --- /dev/null +++ b/test/setup-codex-smoke.test.ts @@ -0,0 +1,94 @@ +import { describe, test, expect } from 'bun:test'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { spawnSync } from 'child_process'; + +const ROOT = path.resolve(import.meta.dir, '..'); + +function makeTempDir(prefix: string): string { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); +} + +function copyRepoWithoutGit(dest: string): void { + fs.cpSync(ROOT, dest, { + recursive: true, + force: true, + preserveTimestamps: true, + filter: (src) => { + const rel = path.relative(ROOT, src); + if (!rel) return true; + const top = rel.split(path.sep)[0]; + return top !== '.git' && top !== 'node_modules' && top !== '.agents'; + }, + }); + + const nodeModulesSrc = path.join(ROOT, 'node_modules'); + const nodeModulesDest = path.join(dest, 'node_modules'); + fs.symlinkSync( + nodeModulesSrc, + nodeModulesDest, + process.platform === 'win32' ? 'junction' : 'dir' + ); +} + +function runSetup(cwd: string, homeDir: string): ReturnType { + return spawnSync('bash', ['./setup', '--host', 'codex'], { + cwd, + env: { ...process.env, HOME: homeDir }, + encoding: 'utf-8', + timeout: 120_000, + }); +} + +describe('setup --host codex smoke', () => { + test('global install creates Codex runtime root and generated skills', () => { + const repoDir = makeTempDir('gstack-codex-global-repo-'); + const homeDir = makeTempDir('gstack-codex-global-home-'); + + try { + copyRepoWithoutGit(repoDir); + const result = runSetup(repoDir, homeDir); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('gstack ready (codex).'); + + const runtimeRoot = path.join(homeDir, '.codex', 'skills', 'gstack'); + const reviewSkill = path.join(homeDir, '.codex', 'skills', 'gstack-review', 'SKILL.md'); + + expect(fs.existsSync(path.join(runtimeRoot, 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(runtimeRoot, 'browse', 'dist'))).toBe(true); + expect(fs.existsSync(path.join(reviewSkill))).toBe(true); + expect(fs.lstatSync(path.join(homeDir, '.codex', 'skills', 'gstack-review')).isSymbolicLink()).toBe(true); + } finally { + fs.rmSync(repoDir, { recursive: true, force: true }); + fs.rmSync(homeDir, { recursive: true, force: true }); + } + }, 120_000); + + test('repo-local install writes generated skills next to .agents checkout only', () => { + const projectDir = makeTempDir('gstack-codex-local-project-'); + const homeDir = makeTempDir('gstack-codex-local-home-'); + const repoDir = path.join(projectDir, '.agents', 'skills', 'gstack'); + + try { + fs.mkdirSync(path.dirname(repoDir), { recursive: true }); + copyRepoWithoutGit(repoDir); + const result = runSetup(repoDir, homeDir); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('gstack ready (codex).'); + + const localSkill = path.join(projectDir, '.agents', 'skills', 'gstack-review', 'SKILL.md'); + const localSidecar = path.join(projectDir, '.agents', 'skills', 'gstack', 'bin'); + const globalSkill = path.join(homeDir, '.codex', 'skills', 'gstack-review'); + + expect(fs.existsSync(localSkill)).toBe(true); + expect(fs.existsSync(localSidecar)).toBe(true); + expect(fs.existsSync(globalSkill)).toBe(false); + } finally { + fs.rmSync(projectDir, { recursive: true, force: true }); + fs.rmSync(homeDir, { recursive: true, force: true }); + } + }, 120_000); +});