209 lines
6.6 KiB
TypeScript
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();
|
|
});
|
|
});
|