diff options
| author | Bobby <[email protected]> | 2026-04-22 06:51:57 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-04-22 06:51:57 +0530 |
| commit | 4dfbf7a13120b3b64521fce6de7d5d3f76cd2084 (patch) | |
| tree | 351d2ab064442e29e76a74c7b4fa53c1048fb66b /tests/unit/utils | |
| parent | 4383213a23e27903e1fed270f1dbcc116644c7fc (diff) | |
| download | hollowdark-4dfbf7a13120b3b64521fce6de7d5d3f76cd2084.tar.xz hollowdark-4dfbf7a13120b3b64521fce6de7d5d3f76cd2084.zip | |
Add shared utilities: Result, assert, deepEqual, common types, log
Foundational helpers referenced by most subsequent code. Small, pure,
no dependencies on anything in the project.
utils/result.ts Result<T, E> = Ok<T> | Err<E> for recoverable
failures at system boundaries (save/load, manifest
fetch, content validation). Internal pure logic
still uses throw for programmer errors.
ok / err / isOk / isErr / mapResult / mapErr /
unwrap / unwrapOr.
utils/assert.ts assert (with type-narrowing asserts), assertNever
for exhaustive-switch termination, assertDefined
for narrowing T | null | undefined → T.
utils/equal.ts Structural deepEqual for tests comparing simulation
snapshots. Handles plain objects, arrays, Map, Set,
Date, primitives. Does not handle cyclic graphs
(simulation state is a tree).
utils/types.ts Common type aliases: NonEmptyArray, JsonValue,
ElementOf, DeepReadonly, Brand<T, B> for nominal /
branded types (PersonId vs RelationshipId).
utils/log.ts Structured logging wrapping console. Developer-
facing only; the game ships with no runtime
telemetry.
utils/index.ts Public re-exports.
40 unit tests in tests/unit/utils/ cover Result constructors and
combinators, assert variants, and deepEqual's primitive / array /
object / Map / Set / Date paths plus a realistic simulation-snapshot
comparison.
Diffstat (limited to 'tests/unit/utils')
| -rw-r--r-- | tests/unit/utils/assert.test.ts | 46 | ||||
| -rw-r--r-- | tests/unit/utils/equal.test.ts | 147 | ||||
| -rw-r--r-- | tests/unit/utils/result.test.ts | 83 |
3 files changed, 276 insertions, 0 deletions
diff --git a/tests/unit/utils/assert.test.ts b/tests/unit/utils/assert.test.ts new file mode 100644 index 0000000..c1fc02d --- /dev/null +++ b/tests/unit/utils/assert.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from 'vitest' +import { assert, assertDefined, assertNever } from 'utils/assert' + +describe('assert', () => { + test('passes on truthy condition', () => { + expect(() => assert(true)).not.toThrow() + expect(() => assert(1)).not.toThrow() + expect(() => assert('non-empty')).not.toThrow() + }) + + test('throws on falsy condition', () => { + expect(() => assert(false)).toThrow(/Assertion failed/) + expect(() => assert(0)).toThrow() + expect(() => assert(null)).toThrow() + expect(() => assert(undefined)).toThrow() + expect(() => assert('')).toThrow() + }) + + test('uses supplied message', () => { + expect(() => assert(false, 'custom reason')).toThrow(/custom reason/) + }) +}) + +describe('assertDefined', () => { + test('returns the value when defined', () => { + expect(assertDefined(5)).toBe(5) + expect(assertDefined('')).toBe('') + expect(assertDefined(false)).toBe(false) + expect(assertDefined(0)).toBe(0) + }) + + test('throws on null / undefined', () => { + expect(() => assertDefined(null)).toThrow() + expect(() => assertDefined(undefined)).toThrow() + }) + + test('uses supplied message', () => { + expect(() => assertDefined(null, 'nope')).toThrow(/nope/) + }) +}) + +describe('assertNever', () => { + test('always throws', () => { + expect(() => assertNever('x' as never)).toThrow() + }) +}) diff --git a/tests/unit/utils/equal.test.ts b/tests/unit/utils/equal.test.ts new file mode 100644 index 0000000..8f07996 --- /dev/null +++ b/tests/unit/utils/equal.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, test } from 'vitest' +import { deepEqual } from 'utils/equal' + +describe('deepEqual — primitives', () => { + test('identical primitives', () => { + expect(deepEqual(1, 1)).toBe(true) + expect(deepEqual('a', 'a')).toBe(true) + expect(deepEqual(true, true)).toBe(true) + expect(deepEqual(null, null)).toBe(true) + expect(deepEqual(undefined, undefined)).toBe(true) + }) + + test('different primitives', () => { + expect(deepEqual(1, 2)).toBe(false) + expect(deepEqual('a', 'b')).toBe(false) + expect(deepEqual(null, undefined)).toBe(false) + }) + + test('NaN is treated as equal to NaN (Object.is semantics)', () => { + expect(deepEqual(NaN, NaN)).toBe(true) + }) + + test('+0 and -0 are distinct (Object.is semantics)', () => { + expect(deepEqual(0, -0)).toBe(false) + }) +}) + +describe('deepEqual — arrays', () => { + test('same-length equal arrays', () => { + expect(deepEqual([1, 2, 3], [1, 2, 3])).toBe(true) + }) + + test('different-length arrays', () => { + expect(deepEqual([1, 2], [1, 2, 3])).toBe(false) + }) + + test('different elements', () => { + expect(deepEqual([1, 2, 3], [1, 2, 4])).toBe(false) + }) + + test('nested arrays', () => { + expect( + deepEqual( + [ + [1, 2], + [3, 4] + ], + [ + [1, 2], + [3, 4] + ] + ) + ).toBe(true) + }) + + test('array vs non-array', () => { + expect(deepEqual([1], { 0: 1, length: 1 })).toBe(false) + }) +}) + +describe('deepEqual — objects', () => { + test('same-shape objects', () => { + expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true) + }) + + test('order of keys does not matter', () => { + expect(deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true) + }) + + test('missing key on one side', () => { + expect(deepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false) + }) + + test('nested objects', () => { + expect(deepEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } })).toBe(true) + expect(deepEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 2 } } })).toBe(false) + }) +}) + +describe('deepEqual — Map', () => { + test('equal maps', () => { + expect( + deepEqual( + new Map([ + ['a', 1], + ['b', 2] + ]), + new Map([ + ['a', 1], + ['b', 2] + ]) + ) + ).toBe(true) + }) + + test('different sizes', () => { + expect(deepEqual(new Map([['a', 1]]), new Map([['a', 1], ['b', 2]]))).toBe(false) + }) + + test('different values', () => { + expect(deepEqual(new Map([['a', 1]]), new Map([['a', 2]]))).toBe(false) + }) +}) + +describe('deepEqual — Set', () => { + test('equal sets', () => { + expect(deepEqual(new Set([1, 2, 3]), new Set([3, 2, 1]))).toBe(true) + }) + + test('different sets', () => { + expect(deepEqual(new Set([1, 2]), new Set([1, 2, 3]))).toBe(false) + }) +}) + +describe('deepEqual — Date', () => { + test('same timestamp is equal', () => { + expect(deepEqual(new Date(1000), new Date(1000))).toBe(true) + }) + + test('different timestamps', () => { + expect(deepEqual(new Date(1000), new Date(2000))).toBe(false) + }) + + test('Date vs non-Date', () => { + expect(deepEqual(new Date(0), 0)).toBe(false) + }) +}) + +describe('deepEqual — mixed', () => { + test('deep structure typical of simulation state', () => { + const a = { + id: 'p1', + name: { given: 'Meera', surname: 'Sarwath' }, + traits: new Map([['openness', 72]]), + friends: new Set(['p2', 'p3']), + events: [{ t: 10, kind: 'birth' }] + } + const b = { + id: 'p1', + name: { given: 'Meera', surname: 'Sarwath' }, + traits: new Map([['openness', 72]]), + friends: new Set(['p3', 'p2']), + events: [{ t: 10, kind: 'birth' }] + } + expect(deepEqual(a, b)).toBe(true) + }) +}) diff --git a/tests/unit/utils/result.test.ts b/tests/unit/utils/result.test.ts new file mode 100644 index 0000000..ef33e74 --- /dev/null +++ b/tests/unit/utils/result.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from 'vitest' +import { + err, + isErr, + isOk, + mapErr, + mapResult, + ok, + unwrap, + unwrapOr, + type Result +} from 'utils/result' + +describe('Result constructors and predicates', () => { + test('ok wraps a value', () => { + const r = ok(42) + expect(r).toEqual({ ok: true, value: 42 }) + expect(isOk(r)).toBe(true) + expect(isErr(r)).toBe(false) + }) + + test('err wraps an error', () => { + const e = new Error('boom') + const r = err(e) + expect(r).toEqual({ ok: false, error: e }) + expect(isErr(r)).toBe(true) + expect(isOk(r)).toBe(false) + }) +}) + +describe('mapResult / mapErr', () => { + test('mapResult transforms the value of an ok', () => { + const r: Result<number> = ok(3) + expect(mapResult(r, (n) => n * 2)).toEqual(ok(6)) + }) + + test('mapResult leaves an error untouched', () => { + const e = new Error('x') + const r: Result<number> = err(e) + expect(mapResult(r, (n: number) => n * 2)).toEqual(err(e)) + }) + + test('mapErr transforms the error of a failure', () => { + const r: Result<number, string> = err('oops') + expect(mapErr(r, (s: string) => s.length)).toEqual(err(4)) + }) + + test('mapErr leaves an ok untouched', () => { + const r: Result<number, string> = ok(5) + expect(mapErr(r, (s: string) => s.length)).toEqual(ok(5)) + }) +}) + +describe('unwrap / unwrapOr', () => { + test('unwrap returns the value when ok', () => { + expect(unwrap(ok('hello'))).toBe('hello') + }) + + test('unwrap throws when error', () => { + expect(() => unwrap(err(new Error('bad')))).toThrow(/bad/) + }) + + test('unwrapOr returns value when ok', () => { + expect(unwrapOr(ok(1), 0)).toBe(1) + }) + + test('unwrapOr returns fallback when error', () => { + expect(unwrapOr(err<Error>(new Error('e')), 0)).toBe(0) + }) +}) + +describe('type-narrowing', () => { + test('isOk narrows to Ok<T>', () => { + const r: Result<number, string> = ok(10) + if (isOk(r)) { + // Type-level check — if isOk doesn't narrow, this line fails to compile. + const n: number = r.value + expect(n).toBe(10) + } else { + expect.fail('isOk should narrow to ok branch') + } + }) +}) |
