brainy/tests/unit/db/stableEqual.test.ts
David Snelling 2c84f86815 feat(8.0): temporal range verbs — diff, history, since(gen|Date), asOf{exclusive}, transactionLog window
asOf() answers "state AT a point"; these answer "what happened BETWEEN two
points" and "one entity's whole history", all on the existing generation records.

- Extract resolveAsOfGeneration() as the shared gen|Date→{generation,timestamp}
  chokepoint (reachability + Date semantics identical everywhere). asOf()'s
  inclusive path is byte-identical — db-mvcc still 25/25.
- asOf(target, { exclusive }) — strict-before (resolved−1, clamped at gen 0,
  never a RangeError).
- db.since(Db | generation | Date) — overload; number/Date resolve via the shared
  resolver (new DbHost.resolveGeneration); EXCLUSIVE lower bound;
  since(db) === since(db.generation). Same-store guard via Db.belongsToStore.
- brain.diff(a, b) → { added, removed, modified } split by nouns/verbs. EARNS its
  name: candidate set is ONLY changedBetween(gLow,gHigh), each classified by
  existence at both endpoints + a key-order-insensitive value compare
  (new src/db/stableEqual.ts) — a touched-but-reverted / born-and-died id is in
  no bucket.
- brain.history(id, { from, to }) → every distinct version oldest→newest, each
  value === asOf(version.generation).get(id); null = removal; kind auto-detected
  (throws on UUID-space collision). New store helper generationsTouching().
- brain.transactionLog({ from, to, limit }) — INCLUSIVE generation/Date window
  (contrast since's exclusive lower bound); limit applied last.
- Compaction policy locked: diff/since THROW GenerationCompactedError below the
  horizon; history TRUNCATES to it.

Types DiffResult/HistoryVersion/EntityHistory exported from the package root.
Tests: tests/integration/db-temporal.test.ts (8 proofs incl. diff-earns-its-name,
history↔asOf cross-check, the composition proof, granularity, compaction contrast)
+ tests/unit/db/stableEqual.test.ts (6). docs/guides/snapshots-and-time-travel.md
+ RELEASES updated. 1477 unit + db-temporal 8 + db-mvcc 25 green.
2026-06-19 13:21:02 -07:00

53 lines
2.1 KiB
TypeScript

/**
* @module tests/unit/db/stableEqual
* @description Unit proof for the key-order-insensitive deep-equal that
* {@link Brainy.diff} uses to decide whether an id present at both endpoints
* actually changed. The risk it guards: a naive JSON.stringify comparison is
* key-order-sensitive and would report a no-op re-write as `modified`.
*/
import { describe, it, expect } from 'vitest'
import { stableDeepEqual } from '../../../src/db/stableEqual.js'
describe('stableDeepEqual', () => {
it('ignores object key order (the core diff requirement)', () => {
expect(stableDeepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true)
expect(stableDeepEqual({ a: 1, b: 2, c: { x: 1, y: 2 } }, { c: { y: 2, x: 1 }, b: 2, a: 1 })).toBe(true)
})
it('honors array order (arrays are order-significant)', () => {
expect(stableDeepEqual([1, 2, 3], [1, 2, 3])).toBe(true)
expect(stableDeepEqual([1, 2, 3], [3, 2, 1])).toBe(false)
expect(stableDeepEqual([1, 2], [1, 2, 3])).toBe(false)
})
it('recurses into nested objects and arrays', () => {
expect(
stableDeepEqual(
{ tags: ['a', 'b'], meta: { n: 1, deep: { z: [1, { q: 2 }] } } },
{ meta: { deep: { z: [1, { q: 2 }] }, n: 1 }, tags: ['a', 'b'] }
)
).toBe(true)
expect(
stableDeepEqual({ meta: { deep: { z: [1, { q: 2 }] } } }, { meta: { deep: { z: [1, { q: 3 }] } } })
).toBe(false)
})
it('distinguishes primitives, null, and types', () => {
expect(stableDeepEqual(1, 1)).toBe(true)
expect(stableDeepEqual(1, '1')).toBe(false)
expect(stableDeepEqual(null, null)).toBe(true)
expect(stableDeepEqual(null, {})).toBe(false)
expect(stableDeepEqual({ a: 1 }, { a: 1, b: undefined })).toBe(false) // extra key
expect(stableDeepEqual(true, false)).toBe(false)
})
it('treats NaN as equal to NaN (stable value comparison)', () => {
expect(stableDeepEqual(NaN, NaN)).toBe(true)
expect(stableDeepEqual({ x: NaN }, { x: NaN })).toBe(true)
expect(stableDeepEqual(NaN, 0)).toBe(false)
})
it('an object and an array of the same length are not equal', () => {
expect(stableDeepEqual({ 0: 'a', 1: 'b' }, ['a', 'b'])).toBe(false)
})
})