/** * Unit tests for the mock scenario queue and loader. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { ScenarioQueue, loadScenarioFile, setMockScenario, getScenarioQueue, resetScenario, type ScenarioEntry, } from '../infra/mock/index.js'; describe('ScenarioQueue', () => { it('should consume entries in order when no agent specified', () => { const queue = new ScenarioQueue([ { status: 'done', content: 'first' }, { status: 'done', content: 'second' }, ]); expect(queue.consume('any-agent')?.content).toBe('first'); expect(queue.consume('any-agent')?.content).toBe('second'); expect(queue.consume('any-agent')).toBeUndefined(); }); it('should match persona-specific entries first', () => { const queue = new ScenarioQueue([ { status: 'done', content: 'generic' }, { persona: 'coder', status: 'done', content: 'coder response' }, { status: 'done', content: 'second generic' }, ]); // Coder should get its specific entry expect(queue.consume('coder')?.content).toBe('coder response'); // Other agents get generic entries in order expect(queue.consume('reviewer')?.content).toBe('generic'); expect(queue.consume('planner')?.content).toBe('second generic'); expect(queue.remaining).toBe(0); }); it('should fall back to unspecified entries when no persona match', () => { const queue = new ScenarioQueue([ { persona: 'coder', status: 'done', content: 'coder only' }, { status: 'done', content: 'fallback' }, ]); // Reviewer has no specific entry -> gets the unspecified one expect(queue.consume('reviewer')?.content).toBe('fallback'); // Coder still gets its own expect(queue.consume('coder')?.content).toBe('coder only'); expect(queue.remaining).toBe(0); }); it('should return undefined when queue is exhausted', () => { const queue = new ScenarioQueue([ { status: 'done', content: 'only' }, ]); queue.consume('agent'); expect(queue.consume('agent')).toBeUndefined(); }); it('should track remaining count', () => { const queue = new ScenarioQueue([ { status: 'done', content: 'a' }, { status: 'done', content: 'b' }, { status: 'done', content: 'c' }, ]); expect(queue.remaining).toBe(3); queue.consume('x'); expect(queue.remaining).toBe(2); }); it('should not modify the original array', () => { const entries: ScenarioEntry[] = [ { status: 'done', content: 'a' }, { status: 'done', content: 'b' }, ]; const queue = new ScenarioQueue(entries); queue.consume('x'); expect(entries).toHaveLength(2); }); it('should handle mixed persona and unspecified entries correctly', () => { const queue = new ScenarioQueue([ { persona: 'plan', status: 'done', content: '[PLAN:1]\nPlan done' }, { persona: 'implement', status: 'done', content: '[IMPLEMENT:1]\nCode written' }, { persona: 'ai_review', status: 'done', content: '[AI_REVIEW:1]\nNo issues' }, { persona: 'supervise', status: 'done', content: '[SUPERVISE:1]\nAll good' }, ]); expect(queue.consume('plan')?.content).toContain('[PLAN:1]'); expect(queue.consume('implement')?.content).toContain('[IMPLEMENT:1]'); expect(queue.consume('ai_review')?.content).toContain('[AI_REVIEW:1]'); expect(queue.consume('supervise')?.content).toContain('[SUPERVISE:1]'); expect(queue.remaining).toBe(0); }); }); describe('loadScenarioFile', () => { let tempDir: string; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), 'takt-scenario-')); }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); it('should load valid scenario JSON', () => { const scenario = [ { persona: 'plan', status: 'done', content: 'Plan done' }, { status: 'blocked', content: 'Blocked' }, ]; const filePath = join(tempDir, 'scenario.json'); writeFileSync(filePath, JSON.stringify(scenario)); const entries = loadScenarioFile(filePath); expect(entries).toHaveLength(2); expect(entries[0]).toEqual({ persona: 'plan', status: 'done', content: 'Plan done' }); expect(entries[1]).toEqual({ persona: undefined, status: 'blocked', content: 'Blocked' }); }); it('should default status to "done" if omitted', () => { const scenario = [{ content: 'Simple response' }]; const filePath = join(tempDir, 'scenario.json'); writeFileSync(filePath, JSON.stringify(scenario)); const entries = loadScenarioFile(filePath); expect(entries[0].status).toBe('done'); }); it('should throw for non-existent file', () => { expect(() => loadScenarioFile('/nonexistent/file.json')).toThrow('Scenario file not found'); }); it('should throw for invalid JSON', () => { const filePath = join(tempDir, 'bad.json'); writeFileSync(filePath, 'not json at all'); expect(() => loadScenarioFile(filePath)).toThrow('not valid JSON'); }); it('should throw for non-array JSON', () => { const filePath = join(tempDir, 'object.json'); writeFileSync(filePath, '{"key": "value"}'); expect(() => loadScenarioFile(filePath)).toThrow('must contain a JSON array'); }); it('should throw for entry without content', () => { const filePath = join(tempDir, 'no-content.json'); writeFileSync(filePath, '[{"status": "done"}]'); expect(() => loadScenarioFile(filePath)).toThrow('must have a "content" string'); }); it('should throw for invalid status', () => { const filePath = join(tempDir, 'bad-status.json'); writeFileSync(filePath, '[{"content": "test", "status": "invalid"}]'); expect(() => loadScenarioFile(filePath)).toThrow('invalid status'); }); }); describe('setMockScenario / getScenarioQueue / resetScenario', () => { afterEach(() => { resetScenario(); }); it('should set and retrieve scenario queue', () => { setMockScenario([ { status: 'done', content: 'test' }, ]); const queue = getScenarioQueue(); expect(queue).not.toBeNull(); expect(queue!.remaining).toBe(1); }); it('should return null when no scenario is set', () => { expect(getScenarioQueue()).toBeNull(); }); it('should clear scenario when null is passed', () => { setMockScenario([{ status: 'done', content: 'test' }]); setMockScenario(null); expect(getScenarioQueue()).toBeNull(); }); it('should reset scenario state', () => { setMockScenario([{ status: 'done', content: 'test' }]); resetScenario(); expect(getScenarioQueue()).toBeNull(); }); });