takt/src/__tests__/it-mock-scenario.test.ts
2026-02-07 08:52:37 +09:00

209 lines
6.6 KiB
TypeScript

/**
* 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();
});
});