mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
- Remove unused validateTokenCounts/validateTokensAndCost exports and PlanPhase type alias - Add execPattern() helper to eliminate 8 repetitive .lastIndex=0 + exec() loops - Centralize 11 magic number constants into config/ai-defaults.ts and config/server-timing.ts - Remove stale src/tui from tsconfig.json exclude - Fix CLAUDE.md inaccuracies (session helpers, app.js line count, module count) - Add 316 new tests: LRUMap (38), StaleExpirationMap (42), BufferAccumulator (33), respawn helpers (142), system-routes expansion (11→61) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
280 lines
9.3 KiB
TypeScript
280 lines
9.3 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { BufferAccumulator } from '../src/utils/buffer-accumulator.js';
|
|
|
|
describe('BufferAccumulator', () => {
|
|
describe('construction', () => {
|
|
it('creates with simple size parameters', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
expect(buf.isEmpty).toBe(true);
|
|
expect(buf.length).toBe(0);
|
|
expect(buf.value).toBe('');
|
|
});
|
|
|
|
it('creates with BufferConfig object', () => {
|
|
const buf = new BufferAccumulator({ maxSize: 1000, trimSize: 800 });
|
|
expect(buf.isEmpty).toBe(true);
|
|
});
|
|
|
|
it('creates with BufferConfig including onTrim', () => {
|
|
const onTrim = vi.fn();
|
|
const buf = new BufferAccumulator({ maxSize: 100, trimSize: 50, onTrim });
|
|
buf.append('x'.repeat(101));
|
|
expect(onTrim).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('append', () => {
|
|
it('appends data and updates length', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('hello');
|
|
expect(buf.length).toBe(5);
|
|
expect(buf.isEmpty).toBe(false);
|
|
buf.append(' world');
|
|
expect(buf.length).toBe(11);
|
|
expect(buf.value).toBe('hello world');
|
|
});
|
|
|
|
it('ignores empty strings', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('');
|
|
expect(buf.length).toBe(0);
|
|
expect(buf.isEmpty).toBe(true);
|
|
});
|
|
|
|
it('handles multiple chunks efficiently', () => {
|
|
const buf = new BufferAccumulator(10000, 8000);
|
|
for (let i = 0; i < 100; i++) {
|
|
buf.append(`chunk${i}`);
|
|
}
|
|
expect(buf.length).toBeGreaterThan(0);
|
|
const value = buf.value;
|
|
expect(value).toContain('chunk0');
|
|
expect(value).toContain('chunk99');
|
|
});
|
|
});
|
|
|
|
describe('value', () => {
|
|
it('returns empty string when empty', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
expect(buf.value).toBe('');
|
|
});
|
|
|
|
it('returns single chunk without joining', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('single');
|
|
expect(buf.value).toBe('single');
|
|
});
|
|
|
|
it('consolidates chunks on access', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('a');
|
|
buf.append('b');
|
|
buf.append('c');
|
|
expect(buf.value).toBe('abc');
|
|
// Access again should be same (consolidated)
|
|
expect(buf.value).toBe('abc');
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
it('resets buffer to empty', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('some data');
|
|
buf.clear();
|
|
expect(buf.isEmpty).toBe(true);
|
|
expect(buf.length).toBe(0);
|
|
expect(buf.value).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('set', () => {
|
|
it('replaces buffer content', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('old data');
|
|
buf.set('new data');
|
|
expect(buf.value).toBe('new data');
|
|
expect(buf.length).toBe(8);
|
|
});
|
|
|
|
it('handles empty string', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('some data');
|
|
buf.set('');
|
|
expect(buf.isEmpty).toBe(true);
|
|
expect(buf.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('tail', () => {
|
|
it('returns last N characters', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('hello world');
|
|
expect(buf.tail(5)).toBe('world');
|
|
});
|
|
|
|
it('returns entire buffer when N >= length', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('short');
|
|
expect(buf.tail(100)).toBe('short');
|
|
});
|
|
|
|
it('returns empty string on empty buffer', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
expect(buf.tail(5)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('endsWith', () => {
|
|
it('returns true when buffer ends with suffix', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('hello world');
|
|
expect(buf.endsWith('world')).toBe(true);
|
|
});
|
|
|
|
it('returns false when buffer does not end with suffix', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('hello world');
|
|
expect(buf.endsWith('hello')).toBe(false);
|
|
});
|
|
|
|
it('returns true for empty suffix', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('hello');
|
|
expect(buf.endsWith('')).toBe(true);
|
|
});
|
|
|
|
it('returns false when suffix is longer than buffer', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('hi');
|
|
expect(buf.endsWith('hello world')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('contains', () => {
|
|
it('finds string pattern in buffer', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('hello world foo bar');
|
|
expect(buf.contains('world')).toBe(true);
|
|
expect(buf.contains('baz')).toBe(false);
|
|
});
|
|
|
|
it('finds regex pattern in buffer', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('hello world 123');
|
|
expect(buf.contains(/\d+/)).toBe(true);
|
|
expect(buf.contains(/[A-Z]{3}/)).toBe(false);
|
|
});
|
|
|
|
it('searches within last N characters when fromEnd provided', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('hello world');
|
|
expect(buf.contains('hello', 5)).toBe(false); // last 5 = 'world'
|
|
expect(buf.contains('world', 5)).toBe(true);
|
|
});
|
|
|
|
it('regex search with fromEnd', () => {
|
|
const buf = new BufferAccumulator(1000, 800);
|
|
buf.append('abc 123 xyz');
|
|
expect(buf.contains(/\d+/, 4)).toBe(false); // last 4 = ' xyz'
|
|
expect(buf.contains(/\d+/, 8)).toBe(true); // last 8 = '123 xyz'
|
|
});
|
|
});
|
|
|
|
describe('trimming', () => {
|
|
it('trims when maxSize is exceeded', () => {
|
|
const buf = new BufferAccumulator(100, 50);
|
|
buf.append('x'.repeat(60));
|
|
buf.append('y'.repeat(50)); // total 110, triggers trim
|
|
expect(buf.length).toBeLessThanOrEqual(100);
|
|
// After trim, should keep most recent data
|
|
expect(buf.value).toContain('y');
|
|
});
|
|
|
|
it('calls onTrim callback with trimmed byte count', () => {
|
|
const onTrim = vi.fn();
|
|
const buf = new BufferAccumulator({ maxSize: 100, trimSize: 50, onTrim });
|
|
buf.append('x'.repeat(101)); // triggers trim
|
|
expect(onTrim).toHaveBeenCalledTimes(1);
|
|
expect(onTrim).toHaveBeenCalledWith(expect.any(Number));
|
|
const trimmedBytes = onTrim.mock.calls[0][0] as number;
|
|
expect(trimmedBytes).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('keeps most recent data after trim', () => {
|
|
const buf = new BufferAccumulator(100, 50);
|
|
buf.append('A'.repeat(60));
|
|
buf.append('B'.repeat(50)); // total 110, triggers trim to ~50 chars
|
|
const value = buf.value;
|
|
// The end should be B's
|
|
expect(value.endsWith('B')).toBe(true);
|
|
});
|
|
|
|
it('advances past first newline within 4KB to avoid mid-ANSI-escape', () => {
|
|
const buf = new BufferAccumulator(100, 80);
|
|
// Create content that when trimmed, has a newline near the start of the kept portion
|
|
const prefix = 'A'.repeat(30);
|
|
const newlineSection = 'X'.repeat(10) + '\n' + 'Y'.repeat(10);
|
|
const suffix = 'B'.repeat(60);
|
|
buf.append(prefix + newlineSection + suffix);
|
|
// After trimming, the buffer should start after the first newline in the kept portion
|
|
const value = buf.value;
|
|
// Should not start with partial X's before the newline
|
|
if (value.includes('\n')) {
|
|
// If the newline fell within the kept window, it should be skipped
|
|
expect(value.startsWith('X')).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('handles trim with no newline in first 4KB', () => {
|
|
const buf = new BufferAccumulator(100, 50);
|
|
// No newlines at all
|
|
buf.append('A'.repeat(110));
|
|
// Should still trim without error
|
|
expect(buf.length).toBeLessThanOrEqual(100);
|
|
});
|
|
|
|
it('does not call onTrim when trimmedBytes is 0 or negative', () => {
|
|
const onTrim = vi.fn();
|
|
// trimSize equal to maxSize means nothing actually gets trimmed in bytes
|
|
// But this scenario is unusual; just test that onTrim is only called with positive values
|
|
const buf = new BufferAccumulator({ maxSize: 50, trimSize: 50, onTrim });
|
|
buf.append('x'.repeat(51)); // triggers trim, but trimSize == 50 so 1 byte trimmed
|
|
// The trim logic: full.length(51) - trimSize(50) = 1 > 0, so onTrim IS called
|
|
expect(onTrim).toHaveBeenCalledWith(expect.any(Number));
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('works with very small maxSize', () => {
|
|
const buf = new BufferAccumulator(5, 3);
|
|
buf.append('abcdef'); // 6 chars, triggers trim to 3
|
|
expect(buf.length).toBeLessThanOrEqual(5);
|
|
});
|
|
|
|
it('handles rapid appends', () => {
|
|
const buf = new BufferAccumulator(10000, 8000);
|
|
for (let i = 0; i < 1000; i++) {
|
|
buf.append('data');
|
|
}
|
|
expect(buf.length).toBeLessThanOrEqual(10000);
|
|
expect(buf.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('set after trim works correctly', () => {
|
|
const buf = new BufferAccumulator(100, 50);
|
|
buf.append('x'.repeat(110)); // triggers trim
|
|
buf.set('fresh start');
|
|
expect(buf.value).toBe('fresh start');
|
|
expect(buf.length).toBe(11);
|
|
});
|
|
|
|
it('clear after trim works correctly', () => {
|
|
const buf = new BufferAccumulator(100, 50);
|
|
buf.append('x'.repeat(110)); // triggers trim
|
|
buf.clear();
|
|
expect(buf.isEmpty).toBe(true);
|
|
expect(buf.value).toBe('');
|
|
});
|
|
});
|
|
});
|