/** * @claude-flow/codex - Dual-Mode Tests * * Covers parseWorkerSpecs (W2), CollaborationTemplates, the `dual` * command wiring, and DualModeOrchestrator dependency leveling. * No real workers are spawned — only pure logic is exercised. */ import { describe, it, expect } from 'vitest'; import { parseWorkerSpecs, createDualModeCommand } from '../src/dual-mode/cli.js'; import { DualModeOrchestrator, CollaborationTemplates } from '../src/dual-mode/index.js'; import type { WorkerConfig } from '../src/dual-mode/index.js'; describe('parseWorkerSpecs', () => { it('parses a single spec into a WorkerConfig', () => { const [w] = parseWorkerSpecs(['claude:architect:Design the API'], false); expect(w).toMatchObject({ id: 'architect', platform: 'claude', role: 'architect', prompt: 'Design the API' }); expect(w.dependsOn).toBeUndefined(); }); it('chains workers sequentially by default', () => { const ws = parseWorkerSpecs(['claude:architect:Design', 'codex:coder:Build', 'codex:tester:Test'], false); expect(ws.map(w => w.id)).toEqual(['architect', 'coder', 'tester']); expect(ws.map(w => w.platform)).toEqual(['claude', 'codex', 'codex']); expect(ws[0].dependsOn).toBeUndefined(); expect(ws[1].dependsOn).toEqual(['architect']); expect(ws[2].dependsOn).toEqual(['coder']); }); it('runs workers in parallel when parallel=true (no dependsOn)', () => { const ws = parseWorkerSpecs(['claude:a:x', 'codex:b:y'], true); expect(ws.every(w => w.dependsOn === undefined)).toBe(true); }); it('keeps colons inside the prompt (splits on the first two only)', () => { const [w] = parseWorkerSpecs(['codex:coder:Fix bug: handle null in foo:bar'], false); expect(w.prompt).toBe('Fix bug: handle null in foo:bar'); expect(w.role).toBe('coder'); }); it('deduplicates ids for repeated roles', () => { const ws = parseWorkerSpecs(['codex:coder:a', 'codex:coder:b', 'codex:coder:c'], true); expect(ws.map(w => w.id)).toEqual(['coder', 'coder-2', 'coder-3']); }); it('trims whitespace around platform/role/prompt', () => { const [w] = parseWorkerSpecs([' claude : architect : Design it '], false); expect(w).toMatchObject({ platform: 'claude', role: 'architect', prompt: 'Design it' }); }); it('throws on a spec with fewer than two colons', () => { expect(() => parseWorkerSpecs(['claude:architect'], false)).toThrow(/Expected/); expect(() => parseWorkerSpecs(['justaprompt'], false)).toThrow(/Expected/); }); it('throws on an empty prompt', () => { expect(() => parseWorkerSpecs(['claude:architect: '], false)).toThrow(/Missing prompt/); }); it('throws on an unknown platform', () => { expect(() => parseWorkerSpecs(['gemini:coder:do it'], false)).toThrow(/claude.*codex/); }); }); describe('CollaborationTemplates', () => { it('featureDevelopment: architect -> coder -> tester -> reviewer', () => { const ws = CollaborationTemplates.featureDevelopment('Add OAuth'); expect(ws.map(w => w.id)).toEqual(['architect', 'coder', 'tester', 'reviewer']); expect(ws.find(w => w.id === 'architect')!.platform).toBe('claude'); expect(ws.find(w => w.id === 'coder')!.platform).toBe('codex'); expect(ws.find(w => w.id === 'coder')!.dependsOn).toEqual(['architect']); expect(ws.find(w => w.id === 'tester')!.dependsOn).toEqual(['coder']); expect(ws.find(w => w.id === 'reviewer')!.dependsOn).toEqual(['coder', 'tester']); expect(ws.some(w => w.prompt.includes('Add OAuth'))).toBe(true); }); it('securityAudit: scanner -> analyzer -> fixer', () => { const ws = CollaborationTemplates.securityAudit('./src'); expect(ws.map(w => w.id)).toEqual(['scanner', 'analyzer', 'fixer']); expect(ws.find(w => w.id === 'analyzer')!.dependsOn).toEqual(['scanner']); expect(ws.find(w => w.id === 'fixer')!.dependsOn).toEqual(['analyzer']); }); it('refactoring: analyzer -> planner -> refactorer -> validator', () => { const ws = CollaborationTemplates.refactoring('./src/legacy'); expect(ws.map(w => w.id)).toEqual(['analyzer', 'planner', 'refactorer', 'validator']); expect(ws.find(w => w.id === 'planner')!.dependsOn).toEqual(['analyzer']); expect(ws.find(w => w.id === 'validator')!.dependsOn).toEqual(['refactorer']); }); }); describe('dual command wiring', () => { it('exposes run / templates / status subcommands', () => { const cmd = createDualModeCommand(); expect(cmd.name()).toBe('dual'); const subs = cmd.commands.map(c => c.name()).sort(); expect(subs).toEqual(['run', 'status', 'templates']); }); it('`run` accepts a positional [template] and a repeatable --worker', () => { const run = createDualModeCommand().commands.find(c => c.name() === 'run')!; const optionNames = run.options.map(o => o.long); expect(optionNames).toContain('--worker'); expect(optionNames).toContain('--parallel-workers'); expect(optionNames).toContain('--template'); // a positional argument is registered for [template] expect((run as unknown as { _args: unknown[] })._args.length).toBeGreaterThan(0); }); }); describe('DualModeOrchestrator', () => { const orch = () => new DualModeOrchestrator({ projectPath: '/tmp' }); it('uses safe defaults (codex command, not claude)', () => { const o = orch() as unknown as { config: Record }; expect(o.config.codexCommand).toBe('codex'); expect(o.config.claudeCommand).toBe('claude'); expect(o.config.maxConcurrent).toBe(4); expect(o.config.sharedNamespace).toBe('collaboration'); }); it('buildDependencyLevels groups a linear pipeline one-per-level', () => { const ws: WorkerConfig[] = [ { id: 'a', platform: 'claude', role: 'a', prompt: 'x' }, { id: 'b', platform: 'codex', role: 'b', prompt: 'y', dependsOn: ['a'] }, { id: 'c', platform: 'codex', role: 'c', prompt: 'z', dependsOn: ['b'] }, ]; const levels = (orch() as unknown as { buildDependencyLevels(w: WorkerConfig[]): WorkerConfig[][] }).buildDependencyLevels(ws); expect(levels.map(l => l.map(w => w.id))).toEqual([['a'], ['b'], ['c']]); }); it('buildDependencyLevels puts independent workers in the same level', () => { const ws: WorkerConfig[] = [ { id: 'a', platform: 'claude', role: 'a', prompt: 'x' }, { id: 'b', platform: 'codex', role: 'b', prompt: 'y' }, { id: 'c', platform: 'codex', role: 'c', prompt: 'z', dependsOn: ['a', 'b'] }, ]; const levels = (orch() as unknown as { buildDependencyLevels(w: WorkerConfig[]): WorkerConfig[][] }).buildDependencyLevels(ws); expect(levels.length).toBe(2); expect(new Set(levels[0].map(w => w.id))).toEqual(new Set(['a', 'b'])); expect(levels[1].map(w => w.id)).toEqual(['c']); }); it('buildDependencyLevels breaks a circular dependency instead of looping forever', () => { const ws: WorkerConfig[] = [ { id: 'a', platform: 'claude', role: 'a', prompt: 'x', dependsOn: ['b'] }, { id: 'b', platform: 'codex', role: 'b', prompt: 'y', dependsOn: ['a'] }, ]; const levels = (orch() as unknown as { buildDependencyLevels(w: WorkerConfig[]): WorkerConfig[][] }).buildDependencyLevels(ws); expect(levels.flat().map(w => w.id).sort()).toEqual(['a', 'b']); }); });