From f5406130dfa456347b8e39c467aad68d1cba176b Mon Sep 17 00:00:00 2001 From: Pabloader Date: Thu, 12 Mar 2026 14:02:13 +0000 Subject: [PATCH] Add some tests --- package.json | 3 +- test/common/errors.test.ts | 56 +++++++ test/common/lock.test.ts | 46 ++++++ test/common/utils.test.ts | 303 +++++++++++++++++++++++++++++++++++++ 4 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 test/common/errors.test.ts create mode 100644 test/common/lock.test.ts create mode 100644 test/common/utils.test.ts diff --git a/package.json b/package.json index cf2ff3c..0c87c7c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "start": "bun --hot build/server.ts", - "bake": "bun build/build.ts" + "bake": "bun build/build.ts", + "test": "bun test" }, "dependencies": { "@huggingface/gguf": "0.3.4", diff --git a/test/common/errors.test.ts b/test/common/errors.test.ts new file mode 100644 index 0000000..d5b6bc6 --- /dev/null +++ b/test/common/errors.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'bun:test'; +import { formatError, formatErrorMessage } from '@common/errors'; + +describe('errors', () => { + describe('formatErrorMessage', () => { + it('should extract message from Error objects', () => { + const error = new Error('Test error message'); + expect(formatErrorMessage(error)).toBe('Test error message'); + }); + + it('should handle objects with message property', () => { + const obj = { message: 'Custom error' }; + expect(formatErrorMessage(obj)).toBe('Custom error'); + }); + + it('should convert other values to string', () => { + expect(formatErrorMessage('string error')).toBe('string error'); + expect(formatErrorMessage(123)).toBe('123'); + expect(formatErrorMessage(null)).toBe('Unknown error'); + expect(formatErrorMessage(undefined)).toBe('Unknown error'); + }); + + it('should handle plain objects without message', () => { + expect(formatErrorMessage({ foo: 'bar' })).toBe('[object Object]'); + }); + }); + + describe('formatError', () => { + it('should format error with custom message', () => { + const error = new Error('Test error'); + const result = formatError(error, 'Context'); + expect(result).toContain('Context: Test error'); + }); + + it('should format error without custom message', () => { + const error = new Error('Test error'); + const result = formatError(error); + expect(result).toContain('Test error'); + expect(result).toContain('Error: Test error'); + }); + + it('should handle string errors', () => { + const result = formatError('error string', 'Context'); + expect(result).toBe('Context: error string'); + }); + + it('should handle null/undefined errors', () => { + expect(formatError(null, 'Context')).toBe('Context: Unknown error'); + expect(formatError(undefined, 'Context')).toBe('Context: Unknown error'); + }); + + it('should handle number errors', () => { + expect(formatError(42, 'Context')).toBe('Context: 42'); + }); + }); +}); diff --git a/test/common/lock.test.ts b/test/common/lock.test.ts new file mode 100644 index 0000000..d906225 --- /dev/null +++ b/test/common/lock.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'bun:test'; +import Lock from '@common/lock'; + +describe('Lock', () => { + describe('constructor', () => { + it('should create an unlocked lock', () => { + const lock = new Lock(); + expect(lock.locked).toBe(false); + }); + }); + + describe('locked', () => { + it('should return false when lock is not acquired', () => { + const lock = new Lock(); + expect(lock.locked).toBe(false); + }); + + it('should return true after wait is called', () => { + const lock = new Lock(); + lock.wait(); + expect(lock.locked).toBe(true); + }); + }); + + describe('wait', () => { + it('should return a promise', () => { + const lock = new Lock(); + const result = lock.wait(); + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('release', () => { + it('should not throw when releasing an unlocked lock', () => { + const lock = new Lock(); + expect(() => lock.release()).not.toThrow(); + }); + + it('should unlock the lock after wait', () => { + const lock = new Lock(); + lock.wait(); + lock.release(); + expect(lock.locked).toBe(false); + }); + }); +}); diff --git a/test/common/utils.test.ts b/test/common/utils.test.ts new file mode 100644 index 0000000..a5595b2 --- /dev/null +++ b/test/common/utils.test.ts @@ -0,0 +1,303 @@ +import { describe, it, expect } from 'bun:test'; +import { + randInt, + randBool, + choice, + weightedChoice, + shuffle, + mapNumber, + zip, + enumerate, + range, + clamp, + lerp, + prevent, + intHash, + sinHash, + throttle, + callUpdater, +} from '@common/utils'; + +describe('utils', () => { + describe('randInt', () => { + it('should return a number within the specified range', () => { + for (let i = 0; i < 100; i++) { + const result = randInt(1, 10); + expect(result).toBeGreaterThanOrEqual(1); + expect(result).toBeLessThanOrEqual(10); + } + }); + + it('should return integers only (no floats)', () => { + for (let i = 0; i < 100; i++) { + const result = randInt(0, 100); + expect(Number.isInteger(result)).toBe(true); + } + }); + + it('should handle single value range', () => { + expect(randInt(5, 6)).toBe(5); + }); + }); + + describe('randBool', () => { + it('should return boolean values', () => { + for (let i = 0; i < 100; i++) { + const result = randBool(); + expect(typeof result).toBe('boolean'); + } + }); + + it('should return both true and false over multiple calls', () => { + const results = new Set(); + for (let i = 0; i < 100; i++) { + results.add(randBool()); + } + expect(results.has(true)).toBe(true); + expect(results.has(false)).toBe(true); + }); + }); + + describe('choice', () => { + it('should return an element from the array', () => { + const array = [1, 2, 3, 4, 5]; + const result = choice(array); + expect(array).toContain(result); + }); + + it('should work with string arrays', () => { + const array = ['a', 'b', 'c']; + const result = choice(array); + expect(array).toContain(result); + }); + + it('should work with object arrays', () => { + const array = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const result = choice(array); + expect(array).toContain(result); + }); + }); + + describe('weightedChoice', () => { + it('should return an item based on weights (array input)', () => { + const options: [number, number][] = [[1, 10], [2, 1], [3, 1]]; + const results = new Set(); + for (let i = 0; i < 100; i++) { + const result = weightedChoice(options); + if (result !== null) results.add(result); + } + // Item 1 has much higher weight, should appear most often + expect(results.has(1)).toBe(true); + }); + + it('should return an item based on weights (object input)', () => { + const options = { a: 10, b: 1, c: 1 }; + const results = new Set(); + for (let i = 0; i < 100; i++) { + const result = weightedChoice(options); + if (result !== null) results.add(result); + } + expect(results.has('a')).toBe(true); + }); + + it('should return null for empty options', () => { + expect(weightedChoice([])).toBe(null); + expect(weightedChoice({})).toBe(null); + }); + }); + + describe('shuffle', () => { + it('should return an array of the same length', () => { + const array = [1, 2, 3, 4, 5]; + const shuffled = shuffle(array); + expect(shuffled.length).toBe(array.length); + }); + + it('should contain the same elements', () => { + const array = [1, 2, 3, 4, 5]; + const shuffled = shuffle(array); + expect(shuffled.sort((a, b) => a - b)).toEqual(array); + }); + + it('should not modify the original array', () => { + const array = [1, 2, 3, 4, 5]; + const original = [...array]; + shuffle(array); + expect(array).toEqual(original); + }); + + it('should handle empty arrays', () => { + expect(shuffle([])).toEqual([]); + }); + + it('should handle single element arrays', () => { + expect(shuffle([1])).toEqual([1]); + }); + }); + + describe('mapNumber', () => { + it('should map a number from one range to another', () => { + expect(mapNumber(5, 0, 10, 0, 100)).toBe(50); + }); + + it('should handle reverse ranges', () => { + expect(mapNumber(5, 0, 10, 100, 0)).toBe(50); + }); + + it('should handle negative ranges', () => { + expect(mapNumber(0, -10, 10, 0, 100)).toBe(50); + }); + }); + + describe('zip', () => { + it('should zip two arrays together', () => { + const result = [...zip([1, 2, 3], ['a', 'b', 'c'])]; + expect(result).toEqual([[1, 'a'], [2, 'b'], [3, 'c']]); + }); + + it('should stop at the shortest array', () => { + const result = [...zip([1, 2], ['a', 'b', 'c', 'd'])]; + expect(result).toEqual([[1, 'a'], [2, 'b']]); + }); + + it('should zip three arrays', () => { + const result = [...zip([1, 2], ['a', 'b'], [true, false])]; + expect(result).toEqual([[1, 'a', true], [2, 'b', false]]); + }); + + it('should handle empty arrays', () => { + expect([...zip([], [])]).toEqual([]); + }); + }); + + describe('enumerate', () => { + it('should enumerate an array with indices', () => { + const result = [...enumerate(['a', 'b', 'c'])]; + expect(result).toEqual([[0, 'a'], [1, 'b'], [2, 'c']]); + }); + + it('should handle empty arrays', () => { + expect([...enumerate([])]).toEqual([]); + }); + }); + + describe('range', () => { + it('should create a range from a number', () => { + expect(range(5)).toEqual([0, 1, 2, 3, 4]); + }); + + it('should create a range from a string number', () => { + expect(range('5')).toEqual([0, 1, 2, 3, 4]); + }); + + it('should return empty array for 0', () => { + expect(range(0)).toEqual([]); + }); + }); + + describe('clamp', () => { + it('should clamp a value within range', () => { + expect(clamp(5, 0, 10)).toBe(5); + }); + + it('should clamp values below minimum', () => { + expect(clamp(-5, 0, 10)).toBe(0); + }); + + it('should clamp values above maximum', () => { + expect(clamp(15, 0, 10)).toBe(10); + }); + }); + + describe('lerp', () => { + it('should linearly interpolate between two values', () => { + expect(lerp(0, 100, 0.5)).toBe(50); + }); + + it('should return start value at t=0', () => { + expect(lerp(0, 100, 0)).toBe(0); + }); + + it('should return end value at t=1', () => { + expect(lerp(0, 100, 1)).toBe(100); + }); + + it('should handle values outside 0-1 range', () => { + expect(lerp(0, 100, -0.5)).toBe(-50); + expect(lerp(0, 100, 1.5)).toBe(150); + }); + }); + + describe('prevent', () => { + it('should call preventDefault and return false', () => { + let preventDefaultCalled = false; + const mockEvent = { + preventDefault: () => { preventDefaultCalled = true; } + } as Event; + const result = prevent(mockEvent); + expect(preventDefaultCalled).toBe(true); + expect(result).toBe(false); + }); + }); + + describe('intHash', () => { + it('should return a number', () => { + expect(typeof intHash(1, 2, 3)).toBe('number'); + }); + + it('should be deterministic', () => { + const hash1 = intHash(1, 2, 3); + const hash2 = intHash(1, 2, 3); + expect(hash1).toBe(hash2); + }); + + it('should produce different hashes for different inputs', () => { + const hash1 = intHash(1); + const hash2 = intHash(2); + expect(hash1).not.toBe(hash2); + }); + }); + + describe('sinHash', () => { + it('should return a number between 0 and 1', () => { + const result = sinHash(1, 2, 3); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(1); + }); + + it('should be deterministic', () => { + const hash1 = sinHash(1, 2, 3); + const hash2 = sinHash(1, 2, 3); + expect(hash1).toBe(hash2); + }); + }); + + describe('throttle', () => { + it('should call the function immediately', () => { + let callCount = 0; + const fn = throttle(() => { callCount++; }, 1000); + fn(); + expect(callCount).toBe(1); + }); + + it('should throttle subsequent calls', () => { + let callCount = 0; + const fn = throttle(() => { callCount++; }, 1000); + fn(); + fn(); + fn(); + expect(callCount).toBe(1); + }); + }); + + describe('callUpdater', () => { + it('should call a function updater', () => { + const updater = (prev: number) => prev + 1; + expect(callUpdater(updater, 5)).toBe(6); + }); + + it('should return a direct value updater', () => { + expect(callUpdater(10, 5)).toBe(10); + }); + }); +});