From 40f05f83f39df7a850a9a135e0ed5d835a85770b Mon Sep 17 00:00:00 2001 From: Chirag Date: Mon, 17 Aug 2026 10:52:17 +0530 Subject: [PATCH] Bound logLines and reset it on deactivate Fixes #5 logLines had no ceiling. One line per recorded command meant it grew for as long as the window stayed open, in a codebase that otherwise caps what it retains: history has truthlog.maxEntries, output has truthlog.maxOutputBytes, this had nothing. It is also module state that deactivate() did not clear while outputChannel was. Across a deactivate/activate cycle in the same extension host, a new session started holding the previous session's lines, and getLogLines() handed tests both sessions mixed together. Cap at 5000 lines, drop oldest first, and clear on deactivate. The OutputChannel is untouched and still holds the full log, which is what a user actually reads; this array exists only because VS Code has no API for reading a channel back. The trimming lives in an exported appendBounded() so it can be tested against a local array. Driving it through log() would mean pushing thousands of lines into the shared buffer that cmdExeDetection and the activation suite read by offset, which is exactly the sort of index shift that would break them. Co-Authored-By: Claude Opus 5 --- src/extension.ts | 33 +++++++++++++++++++++++- src/test/suite/logBuffer.test.ts | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/test/suite/logBuffer.test.ts 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'); + }); +});