UNPKG

rigjs

Version:

A multi-repos dev tool based on yarn and git.Rigjs is intended to be the simplest way to develop,share and deliver codes between different developers or different projects.

171 lines (150 loc) 7.25 kB
import os from 'os'; import fs from 'fs'; import path from 'path'; import { runCommand, buildEngineInvocation, createTaskWorktree, removeTaskWorktree, runParallel, dispatchTask } from './runtime'; const NODE = process.execPath; // verify mechanics with a harmless command, not real engines describe('runCommand', () => { it('captures stdout and exit code 0 on success', async () => { const r = await runCommand(NODE, ['-e', 'process.stdout.write("hi")']); expect(r.stdout).toBe('hi'); expect(r.code).toBe(0); expect(r.timedOut).toBe(false); }); it('captures stderr and non-zero exit code on failure', async () => { const r = await runCommand(NODE, ['-e', 'process.stderr.write("boom"); process.exit(3)']); expect(r.stderr).toContain('boom'); expect(r.code).toBe(3); }); it('enforces a timeout (kills the process, marks timedOut)', async () => { const r = await runCommand(NODE, ['-e', 'setTimeout(()=>{}, 10000)'], { timeoutMs: 200 }); expect(r.timedOut).toBe(true); expect(r.code).toBeNull(); // killed by signal, no exit code }); it('runs in the given cwd', async () => { const tmp = os.tmpdir(); const r = await runCommand(NODE, ['-e', 'process.stdout.write(process.cwd())'], { cwd: tmp }); // macOS /tmp is a symlink to /private/tmp; just assert it resolved somewhere under tmp's basename expect(r.stdout.length).toBeGreaterThan(0); expect(r.code).toBe(0); }); it('pipes stdin input', async () => { const r = await runCommand( NODE, ['-e', 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>process.stdout.write(d.toUpperCase()))'], { input: 'hi' }, ); expect(r.stdout).toBe('HI'); }); it('rejects when the binary does not exist', async () => { await expect(runCommand('definitely-not-a-real-binary-xyz', [])).rejects.toThrow(); }); it('caps captured output at maxOutputBytes and flags truncated', async () => { const r = await runCommand(NODE, ['-e', 'process.stdout.write("x".repeat(5000))'], { maxOutputBytes: 100 }); expect(r.truncated).toBe(true); expect(r.stdout.length).toBe(100); expect(r.code).toBe(0); }); it('does not flag truncated for small output', async () => { const r = await runCommand(NODE, ['-e', 'process.stdout.write("hi")']); expect(r.truncated).toBe(false); }); it('closes child stdin by default so stdin-reading children get EOF (no hang)', async () => { // Without closing stdin, a child that reads until "end" never fires it and hangs // (this is the codex-exec hang found in the dual-engine smoke). const r = await runCommand( NODE, ['-e', 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>process.stdout.write("EOF:"+d.length))'], { timeoutMs: 5000 }, ); expect(r.stdout).toBe('EOF:0'); expect(r.timedOut).toBe(false); expect(r.code).toBe(0); }); }); describe('buildEngineInvocation', () => { it('maps claude to headless print mode', () => { expect(buildEngineInvocation('claude', 'do X')).toEqual({ cmd: 'claude', args: ['-p', 'do X'] }); }); it('maps codex to exec', () => { expect(buildEngineInvocation('codex', 'do X')).toEqual({ cmd: 'codex', args: ['exec', 'do X'] }); }); it('throws for pi (not yet implemented)', () => { expect(() => buildEngineInvocation('pi' as any, 'x')).toThrow(/not implemented/); }); it('throws for unknown engine', () => { expect(() => buildEngineInvocation('gpt' as any, 'x')).toThrow(/unknown engine/); }); it('adds worktree-write flags in develop autonomy', () => { expect(buildEngineInvocation('claude', 'go', { autonomy: 'develop' })) .toEqual({ cmd: 'claude', args: ['-p', '--permission-mode', 'acceptEdits', 'go'] }); expect(buildEngineInvocation('codex', 'go', { autonomy: 'develop' })) .toEqual({ cmd: 'codex', args: ['exec', '--sandbox', 'workspace-write', 'go'] }); }); it('read-only / default stays bare', () => { expect(buildEngineInvocation('claude', 'go', { autonomy: 'read-only' })).toEqual({ cmd: 'claude', args: ['-p', 'go'] }); expect(buildEngineInvocation('codex', 'go')).toEqual({ cmd: 'codex', args: ['exec', 'go'] }); }); }); describe('worktree lifecycle', () => { let repo: string; beforeAll(async () => { repo = fs.mkdtempSync(path.join(os.tmpdir(), 'rig-wt-')); await runCommand('git', ['init', '-q'], { cwd: repo }); fs.writeFileSync(path.join(repo, 'f.txt'), 'hello'); await runCommand('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', 'add', '-A'], { cwd: repo }); await runCommand('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-q', '-m', 'init'], { cwd: repo }); }); afterAll(() => { try { fs.rmSync(repo, { recursive: true, force: true }); } catch { /* best-effort */ } }); it('creates and removes a task worktree on a task/<id> branch', async () => { const wt = await createTaskWorktree(repo, 'demo-001'); expect(wt.branch).toBe('task/demo-001'); expect(fs.existsSync(wt.path)).toBe(true); const br = await runCommand('git', ['branch', '--list', 'task/demo-001'], { cwd: repo }); expect(br.stdout).toContain('task/demo-001'); await removeTaskWorktree(repo, wt.path, { force: true }); expect(fs.existsSync(wt.path)).toBe(false); }); it('throws when the worktree/branch already exists', async () => { const wt = await createTaskWorktree(repo, 'dup-001'); await expect(createTaskWorktree(repo, 'dup-001')).rejects.toThrow(/worktree add failed/); await removeTaskWorktree(repo, wt.path, { force: true }); }); it('dispatchTask runs the invocation inside the task worktree (kept on success)', async () => { const inv = { cmd: process.execPath, args: ['-e', 'process.stdout.write(process.cwd())'] }; const { worktree, result } = await dispatchTask(repo, 'disp-001', inv); expect(result.code).toBe(0); // ran inside the worktree (cwd echoed); realpath to dodge macOS /var symlink expect(fs.realpathSync(result.stdout.trim())).toBe(fs.realpathSync(worktree.path)); expect(fs.existsSync(worktree.path)).toBe(true); // kept until merge await removeTaskWorktree(repo, worktree.path, { force: true }); }); it('dispatchTask removes the worktree on spawn failure', async () => { await expect(dispatchTask(repo, 'disp-fail', { cmd: 'definitely-not-a-real-binary-xyz', args: [] })) .rejects.toThrow(); expect(fs.existsSync(path.join(repo, '.worktrees', 'task-disp-fail'))).toBe(false); }); }); describe('runParallel', () => { it('preserves order and returns all results', async () => { const out = await runParallel([1, 2, 3, 4, 5], async n => n * 2, 2); expect(out).toEqual([2, 4, 6, 8, 10]); }); it('never exceeds the concurrency limit', async () => { let active = 0; let maxActive = 0; await runParallel( Array.from({ length: 8 }, (_, i) => i), async () => { active++; maxActive = Math.max(maxActive, active); await new Promise(r => setTimeout(r, 15)); active--; }, 3, ); expect(maxActive).toBeLessThanOrEqual(3); expect(maxActive).toBeGreaterThan(1); // actually ran concurrently }); });