224 lines
7.1 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
|
|
});
|