diff --git a/src/extension.ts b/src/extension.ts index d1f1b82..58d03f2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,11 +5,37 @@ import { TruthLogTreeProvider } from './treeProvider'; import type { Execution } from './types'; let outputChannel: vscode.OutputChannel | undefined; + +/** + * Kept only so integration tests can read back what was logged: VS Code has no + * public API for reading an OutputChannel's contents. Bounded because this is + * the one structure here with no natural ceiling, unlike history, which + * truthlog.maxEntries caps. One line per recorded command means it otherwise + * grows for as long as the window is open. + * + * Well above what any single test inspects, and far below what would matter for + * memory. The OutputChannel itself is unaffected and still holds the full log, + * which is what a user actually reads. + */ +const MAX_LOG_LINES = 5000; const logLines: string[] = []; +/** + * Appends to a bounded buffer, dropping the oldest lines once it is full. + * Exported for tests: driving this through log() itself would mean pushing + * thousands of lines into the shared array the other suites read by offset, + * which would shift those offsets out from under them. + */ +export function appendBounded(lines: string[], line: string, max: number): void { + lines.push(line); + if (lines.length > max) { + lines.splice(0, lines.length - max); + } +} + function log(message: string): void { const line = `[${new Date().toISOString()}] ${message}`; - logLines.push(line); + appendBounded(logLines, line, MAX_LOG_LINES); outputChannel?.appendLine(line); } @@ -345,4 +371,9 @@ export function activate(context: vscode.ExtensionContext): TruthLogTestApi { export function deactivate(): void { outputChannel = undefined; + // Module state outlives a deactivate/activate cycle in the same extension + // host. Left in place, a reactivated session starts with the previous + // session's lines already present, and getLogLines() hands tests both + // sessions mixed together. + logLines.length = 0; } diff --git a/src/test/suite/logBuffer.test.ts b/src/test/suite/logBuffer.test.ts new file mode 100644 index 0000000..c974324 --- /dev/null +++ b/src/test/suite/logBuffer.test.ts @@ -0,0 +1,44 @@ +import * as assert from 'assert'; +import { appendBounded } from '../../extension'; + +describe('bounded log buffer', () => { + it('keeps everything below the cap', () => { + const lines: string[] = []; + for (let i = 0; i < 10; i++) { + appendBounded(lines, `line ${i}`, 100); + } + assert.strictEqual(lines.length, 10); + assert.strictEqual(lines[0], 'line 0'); + }); + + it('stops growing at the cap and keeps the newest lines', () => { + const lines: string[] = []; + for (let i = 0; i < 250; i++) { + appendBounded(lines, `line ${i}`, 100); + } + assert.strictEqual(lines.length, 100, 'must not grow past the cap'); + assert.strictEqual(lines[99], 'line 249', 'the newest line must survive'); + assert.strictEqual(lines[0], 'line 150', 'the oldest lines are the ones dropped'); + }); + + it('holds at exactly the cap', () => { + const lines: string[] = []; + for (let i = 0; i < 100; i++) { + appendBounded(lines, `line ${i}`, 100); + } + assert.strictEqual(lines.length, 100); + + appendBounded(lines, 'one more', 100); + assert.strictEqual(lines.length, 100); + assert.strictEqual(lines[99], 'one more'); + }); + + it('recovers if the buffer somehow starts over the cap', () => { + // Defensive: a smaller cap applied to an already-long buffer must trim all + // the way down in one go, not one line per call. + const lines = Array.from({ length: 500 }, (_, i) => `old ${i}`); + appendBounded(lines, 'new', 10); + assert.strictEqual(lines.length, 10); + assert.strictEqual(lines[9], 'new'); + }); +});