takt/src/__tests__/task-prefix-writer.test.ts
2026-02-18 22:48:50 +09:00

224 lines
7.1 KiB
TypeScript

/**
* Tests for TaskPrefixWriter
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { TaskPrefixWriter } from '../shared/ui/TaskPrefixWriter.js';
describe('TaskPrefixWriter', () => {
let output: string[];
let writeFn: (text: string) => void;
beforeEach(() => {
output = [];
writeFn = (text: string) => output.push(text);
});
describe('constructor', () => {
it('should use issue number when provided', () => {
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, issue: 123, writeFn });
writer.writeLine('Issue task');
expect(output).toHaveLength(1);
expect(output[0]).toContain('[#123]');
expect(output[0]).not.toContain('[my-t]');
});
it('should cycle colors for different colorIndex values', () => {
const writer0 = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
const writer4 = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 4, writeFn });
writer0.writeLine('hello');
writer4.writeLine('hello');
// Both index 0 and 4 should use cyan (\x1b[36m)
expect(output[0]).toContain('\x1b[36m');
expect(output[1]).toContain('\x1b[36m');
});
it('should use display label when provided', () => {
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, displayLabel: '#12345', writeFn });
writer.writeLine('Hello World');
expect(output).toHaveLength(1);
expect(output[0]).toContain('[#12345]');
expect(output[0]).not.toContain('[my-t]');
});
it('should assign correct colors in order', () => {
const writers = [0, 1, 2, 3].map(
(i) => new TaskPrefixWriter({ taskName: `t${i}`, colorIndex: i, writeFn }),
);
writers.forEach((w) => w.writeLine('x'));
expect(output[0]).toContain('\x1b[36m'); // cyan
expect(output[1]).toContain('\x1b[33m'); // yellow
expect(output[2]).toContain('\x1b[35m'); // magenta
expect(output[3]).toContain('\x1b[32m'); // green
});
});
describe('writeLine', () => {
it('should output single line with truncated task prefix', () => {
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, writeFn });
writer.writeLine('Hello World');
expect(output).toHaveLength(1);
expect(output[0]).toContain('[my-t]');
expect(output[0]).toContain('Hello World');
expect(output[0]).toMatch(/\n$/);
});
it('should output empty line as bare newline', () => {
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, writeFn });
writer.writeLine('');
expect(output).toHaveLength(1);
expect(output[0]).toBe('\n');
});
it('should split multi-line text and prefix each non-empty line', () => {
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, writeFn });
writer.writeLine('Line 1\nLine 2\n\nLine 4');
expect(output).toHaveLength(4);
expect(output[0]).toContain('Line 1');
expect(output[1]).toContain('Line 2');
expect(output[2]).toBe('\n'); // empty line
expect(output[3]).toContain('Line 4');
});
it('should strip ANSI codes from input text', () => {
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, writeFn });
writer.writeLine('\x1b[31mRed Text\x1b[0m');
expect(output).toHaveLength(1);
expect(output[0]).toContain('Red Text');
expect(output[0]).not.toContain('\x1b[31m');
});
});
describe('writeChunk (line buffering)', () => {
it('should buffer partial line and output on newline', () => {
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
writer.writeChunk('Hello');
expect(output).toHaveLength(0);
writer.writeChunk(' World\n');
expect(output).toHaveLength(1);
expect(output[0]).toContain('[task]');
expect(output[0]).toContain('Hello World');
});
it('should handle multiple lines in single chunk', () => {
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
writer.writeChunk('Line 1\nLine 2\n');
expect(output).toHaveLength(2);
expect(output[0]).toContain('Line 1');
expect(output[1]).toContain('Line 2');
});
it('should output empty line without prefix', () => {
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
writer.writeChunk('Hello\n\nWorld\n');
expect(output).toHaveLength(3);
expect(output[0]).toContain('Hello');
expect(output[1]).toBe('\n');
expect(output[2]).toContain('World');
});
it('should keep trailing partial in buffer', () => {
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
writer.writeChunk('Complete\nPartial');
expect(output).toHaveLength(1);
expect(output[0]).toContain('Complete');
writer.flush();
expect(output).toHaveLength(2);
expect(output[1]).toContain('Partial');
});
it('should strip ANSI codes from streamed chunks', () => {
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
writer.writeChunk('\x1b[31mHello');
writer.writeChunk(' World\x1b[0m\n');
expect(output).toHaveLength(1);
expect(output[0]).toContain('Hello World');
expect(output[0]).not.toContain('\x1b[31m');
});
});
describe('flush', () => {
it('should output remaining buffered content with prefix', () => {
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
writer.writeChunk('partial content');
expect(output).toHaveLength(0);
writer.flush();
expect(output).toHaveLength(1);
expect(output[0]).toContain('[task]');
expect(output[0]).toContain('partial content');
expect(output[0]).toMatch(/\n$/);
});
it('should not output anything when buffer is empty', () => {
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
writer.writeChunk('complete line\n');
output.length = 0;
writer.flush();
expect(output).toHaveLength(0);
});
it('should clear buffer after flush', () => {
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
writer.writeChunk('content');
writer.flush();
output.length = 0;
writer.flush();
expect(output).toHaveLength(0);
});
});
describe('setMovementContext', () => {
it('should include movement context in prefix after context update', () => {
const writer = new TaskPrefixWriter({ taskName: 'override-persona-provider', colorIndex: 0, writeFn });
writer.setMovementContext({
movementName: 'implement',
iteration: 4,
maxMovements: 30,
movementIteration: 2,
});
writer.writeLine('content');
expect(output).toHaveLength(1);
expect(output[0]).toContain('[over]');
expect(output[0]).toContain('[implement](4/30)(2)');
expect(output[0]).toContain('content');
});
});
});