From bf96303bf972373017a748814f163e1be132ff64 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Thu, 7 May 2026 07:29:36 -0500 Subject: [PATCH] Update tests for BufferLogAppender and its utilities. AI-Assisted: yes AI-Tool: OpenAI Codex / gpt-5.5 xhigh --- test/buffer-log-appender.test.ts | 275 ++++++++++++++++++++++--------- test/util.test.ts | 59 +++++++ 2 files changed, 258 insertions(+), 76 deletions(-) create mode 100644 test/util.test.ts diff --git a/test/buffer-log-appender.test.ts b/test/buffer-log-appender.test.ts index f6a7a22..342100d 100644 --- a/test/buffer-log-appender.test.ts +++ b/test/buffer-log-appender.test.ts @@ -1,104 +1,227 @@ -import { describe, test, expect } from "bun:test"; -import { BufferLogAppender, LogLevel, LogMessage } from "../src"; +import { describe, expect, test } from 'bun:test' +import { + BufferLogAppender, + LogLevel, + type LogMessage, +} from '../src' function makeMsg(overrides: Partial = {}): LogMessage { return { - scope: "test-scope", + scope: 'test-scope', level: LogLevel.INFO, - msg: "test message", - stacktrace: "", + msg: 'test message', + stacktrace: '', ts: new Date(), ...overrides, - }; + } } -describe("BufferLogAppender", () => { - test("defaults to empty buffer and ALL threshold", () => { - const appender = new BufferLogAppender(); +describe('BufferLogAppender', () => { + test('defaults to empty buffer, ALL threshold, and bounded size', () => { + const appender = new BufferLogAppender() - expect(appender.buffer).toEqual([]); - expect(appender.threshold).toBe(LogLevel.ALL); - }); + expect(appender.buffer).toEqual([]) + expect(appender.threshold).toBe(LogLevel.ALL) + expect(appender.size).toEqual({ target: 1000, max: 1200 }) + }) - test("accepts initial buffer", () => { - const existing: LogMessage[] = [makeMsg({ msg: "pre-existing" })]; - const appender = new BufferLogAppender(existing); + test('accepts threshold and size constructor options', () => { + const appender = new BufferLogAppender(LogLevel.WARN, { + target: 20, + max: 30, + }) - expect(appender.buffer.length).toBe(1); - expect(appender.buffer[0].msg).toBe("pre-existing"); - }); + expect(appender.threshold).toBe(LogLevel.WARN) + expect(appender.size).toEqual({ target: 20, max: 30 }) + }) - test("appends messages to buffer", () => { - const appender = new BufferLogAppender(); + test('appends messages to buffer', () => { + const appender = new BufferLogAppender() - appender.appendMessage(makeMsg({ msg: "first" })); - appender.appendMessage(makeMsg({ msg: "second" })); + appender.appendMessage(makeMsg({ msg: 'first' })) + appender.appendMessage(makeMsg({ msg: 'second' })) - expect(appender.buffer.length).toBe(2); - expect(appender.buffer[0].msg).toBe("first"); - expect(appender.buffer[1].msg).toBe("second"); - }); + expect(appender.buffer.length).toBe(2) + expect(appender.buffer[0].msg).toBe('first') + expect(appender.buffer[1].msg).toBe('second') + }) - test("defaults to an unbounded buffer", () => { - const appender = new BufferLogAppender(); + test('respects threshold', () => { + const appender = new BufferLogAppender(LogLevel.WARN) - appender.appendMessage(makeMsg({ msg: "first" })); - appender.appendMessage(makeMsg({ msg: "second" })); - appender.appendMessage(makeMsg({ msg: "third" })); + appender.appendMessage(makeMsg({ level: LogLevel.INFO, msg: 'dropped' })) + appender.appendMessage(makeMsg({ level: LogLevel.WARN, msg: 'kept' })) + appender.appendMessage(makeMsg({ level: LogLevel.ERROR, msg: 'also kept' })) - expect(appender.buffer.length).toBe(3); - expect(appender.buffer.map((message) => message.msg)).toEqual([ - "first", - "second", - "third", - ]); - }); + expect(appender.buffer.length).toBe(2) + expect(appender.buffer[0].msg).toBe('kept') + expect(appender.buffer[1].msg).toBe('also kept') + }) - test("trims oldest messages when bufferMax is exceeded", () => { - const appender = new BufferLogAppender(); - appender.bufferMax = 2; + test('threshold of ALL does not drop messages', () => { + const appender = new BufferLogAppender(LogLevel.ALL) - appender.appendMessage(makeMsg({ msg: "first" })); - appender.appendMessage(makeMsg({ msg: "second" })); - appender.appendMessage(makeMsg({ msg: "third" })); + appender.appendMessage(makeMsg({ level: LogLevel.ALL, msg: 'level all' })) + appender.appendMessage(makeMsg({ level: LogLevel.INFO, msg: 'level info' })) - expect(appender.buffer.length).toBe(2); - expect(appender.buffer.map((message) => message.msg)).toEqual([ - "second", - "third", - ]); - }); + expect(appender.buffer.length).toBe(2) + expect(appender.buffer[0].msg).toBe('level all') + expect(appender.buffer[1].msg).toBe('level info') + }) - test("respects threshold", () => { - const appender = new BufferLogAppender(undefined, LogLevel.WARN); + test('clearBuffer empties the buffer and reassigns the array', () => { + const appender = new BufferLogAppender() + appender.appendMessage(makeMsg({ msg: 'to clear' })) + const existing = appender.buffer - appender.appendMessage(makeMsg({ level: LogLevel.INFO, msg: "dropped" })); - appender.appendMessage(makeMsg({ level: LogLevel.WARN, msg: "kept" })); - appender.appendMessage(makeMsg({ level: LogLevel.ERROR, msg: "also kept" })); + appender.clearBuffer() - expect(appender.buffer.length).toBe(2); - expect(appender.buffer[0].msg).toBe("kept"); - expect(appender.buffer[1].msg).toBe("also kept"); - }); + expect(appender.buffer).toEqual([]) + expect(appender.buffer).not.toBe(existing) + }) - test("threshold of ALL (0) does not cause falsy check to drop messages", () => { - const appender = new BufferLogAppender(undefined, LogLevel.ALL); + describe('size bounds', () => { + test('derives max as 120 percent of target by default', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { target: 20 }) - appender.appendMessage(makeMsg({ level: LogLevel.ALL, msg: "level all" })); - appender.appendMessage(makeMsg({ level: LogLevel.INFO, msg: "level info" })); + expect(appender.size).toEqual({ target: 20, max: 24 }) + }) - // Both should be kept — ALL (0) should not be treated as falsy - expect(appender.buffer.length).toBe(2); - expect(appender.buffer[0].msg).toBe("level all"); - expect(appender.buffer[1].msg).toBe("level info"); - }); + test('keeps max at least one greater than target', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { + target: 20, + max: 10, + }) - test("clearBuffer empties the buffer", () => { - const appender = new BufferLogAppender(); - appender.appendMessage(makeMsg({ msg: "to clear" })); - expect(appender.buffer.length).toBe(1); + expect(appender.size).toEqual({ target: 20, max: 21 }) + }) - appender.clearBuffer(); - expect(appender.buffer.length).toBe(0); - }); -}); + test('normalizes fractional and too-small target values', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { target: 0.5 }) + + expect(appender.size).toEqual({ target: 1, max: 2 }) + }) + + test('normalizes fractional max values', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { + target: 3, + max: 4.9, + }) + + expect(appender.size).toEqual({ target: 3, max: 4 }) + }) + + test('can be disabled for unbounded buffering', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { + target: 2, + max: 3, + }) + appender.size = undefined + + for (let i = 1; i <= 5; i++) { + appender.appendMessage(makeMsg({ msg: `message ${i}` })) + } + + expect(appender.size).toBeUndefined() + expect(appender.buffer.map((message) => message.msg)).toEqual([ + 'message 1', + 'message 2', + 'message 3', + 'message 4', + 'message 5', + ]) + }) + + test('returns size as a defensive copy', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { + target: 3, + max: 4, + }) + const size = appender.size + + if (size !== undefined) { + size.max = 1 + } + + expect(appender.size).toEqual({ target: 3, max: 4 }) + }) + }) + + describe('buffer trimming', () => { + test('allows buffer to grow until max is exceeded', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { + target: 3, + max: 5, + }) + + for (let i = 1; i <= 5; i++) { + appender.appendMessage(makeMsg({ msg: `message ${i}` })) + } + + expect(appender.buffer.map((message) => message.msg)).toEqual([ + 'message 1', + 'message 2', + 'message 3', + 'message 4', + 'message 5', + ]) + }) + + test('batch trims to target when max is exceeded', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { + target: 3, + max: 5, + }) + + for (let i = 1; i <= 6; i++) { + appender.appendMessage(makeMsg({ msg: `message ${i}` })) + } + + expect(appender.buffer.map((message) => message.msg)).toEqual([ + 'message 4', + 'message 5', + 'message 6', + ]) + }) + + test('reassigns buffer array when trimming', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { + target: 2, + max: 3, + }) + + appender.appendMessage(makeMsg({ msg: 'first' })) + appender.appendMessage(makeMsg({ msg: 'second' })) + appender.appendMessage(makeMsg({ msg: 'third' })) + const existing = appender.buffer + appender.appendMessage(makeMsg({ msg: 'fourth' })) + + expect(appender.buffer).not.toBe(existing) + expect(appender.buffer.map((message) => message.msg)).toEqual([ + 'third', + 'fourth', + ]) + }) + + test('repairs invalid internal bounds before trimming', () => { + const appender = new BufferLogAppender(LogLevel.ALL, { + target: 3, + max: 4, + }) + const unsafeAppender = appender as unknown as { + _size: { target: number, max: number } + } + unsafeAppender._size = { target: 3, max: 2 } + + appender.appendMessage(makeMsg({ msg: 'first' })) + + expect(appender.size).toEqual({ target: 3, max: 4 }) + expect(appender.buffer.length).toBe(2) + expect(appender.buffer[1].level).toBe(LogLevel.ERROR) + expect(appender.buffer[1].msg).toMatchObject({ + oldSize: { target: 3, max: 2 }, + newSize: { target: 3, max: 4 }, + }) + }) + }) +}) diff --git a/test/util.test.ts b/test/util.test.ts new file mode 100644 index 0000000..a6a71c4 --- /dev/null +++ b/test/util.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from 'bun:test' +import { clamp } from '../src/util' + +describe('utils', () => { + describe('clamp', () => { + test("doesn't alter integers within the given range", () => { + expect(clamp(5, { min: 0, max: 10 })).toBe(5) + expect(clamp(-8, { min: -100, max: 100 })).toBe(-8) + }) + + test('keeps values at the range boundaries', () => { + expect(clamp(0, { min: 0, max: 10 })).toBe(0) + expect(clamp(10, { min: 0, max: 10 })).toBe(10) + expect(clamp(-100, { min: -100, max: 100 })).toBe(-100) + expect(clamp(100, { min: -100, max: 100 })).toBe(100) + }) + + test('raises values below the minimum', () => { + expect(clamp(-1, { min: 0, max: 10 })).toBe(0) + expect(clamp(-101, { min: -100, max: 100 })).toBe(-100) + }) + + test('lowers values above the maximum', () => { + expect(clamp(11, { min: 0, max: 10 })).toBe(10) + expect(clamp(101, { min: -100, max: 100 })).toBe(100) + }) + + test('floors floats by default', () => { + expect(clamp(1.5, { min: 0, max: 2 })).toBe(1) + expect(clamp(1.999, { min: 0, max: 2 })).toBe(1) + expect(clamp(-0.3, { min: -2, max: 2 })).toBe(-1) + expect(clamp(-1.1, { min: -2, max: 2 })).toBe(-2) + }) + + test('floors before applying bounds', () => { + expect(clamp(10.5, { min: 0, max: 10 })).toBe(10) + expect(clamp(-10.5, { min: -10, max: 10 })).toBe(-10) + expect(clamp(1.2, { min: 1.5 })).toBe(1.5) + }) + + test('preserves floats when allowed', () => { + expect(clamp(1.5, { min: 0, max: 2 }, true)).toBe(1.5) + expect(clamp(2.5, { min: 0, max: 2 }, true)).toBe(2) + expect(clamp(-0.5, { min: 0, max: 2 }, true)).toBe(0) + }) + + test('supports one-sided bounds', () => { + expect(clamp(-1, { min: 0 })).toBe(0) + expect(clamp(11, { min: 0 })).toBe(11) + expect(clamp(-1, { max: 10 })).toBe(-1) + expect(clamp(11, { max: 10 })).toBe(10) + }) + + test('clamps infinite values to the range boundaries', () => { + expect(clamp(Infinity, { min: 0, max: 10 })).toBe(10) + expect(clamp(-Infinity, { min: 0, max: 10 })).toBe(0) + }) + }) +})