utils/ts_core/src/clock.test.ts
Krzysztof kuhy Rudnicki 41f014e7a7
Some checks failed
palette drift / palette (push) Waiting to run
ts_core tests / test (push) Has been cancelled
ts_core: a framework-free TypeScript utility package
`Clock` and a seeded mulberry32 RNG had each been reimplemented across the
game and web repos -- Clock twice (konbini-67, iron-and-anvil, doc comments
near-verbatim), the RNG four times. None of it is UI, so no existing shared
package owned it: `web_ui` ships `tokens.css` and React components, and a
game importing a UI package to get a seeded RNG is exactly the coupling
that makes people vendor their own copy instead.

The RNG deliberately exports **two** interfaces rather than reconciling
them into one. konbini-67 threaded mutable state through free functions;
sims3-clone and europe-county-map returned an object of closures. The
sequence is the contract here -- these repos have seeded tests and "same
seed -> identical world" guarantees, so changing how many times a helper
advances the generator silently re-rolls every saved world. A shared
package that did that would be worse than the duplication it replaces. The
core is shared, so both interfaces yield the same float sequence; that is
asserted, not assumed.

`rng.test.ts` pins the first five floats for seed 12345, captured from the
pre-extraction konbini-67 implementation rather than from this one -- a
golden test generated from the code under test only asserts that the code
matches itself. It earned its place immediately: the first constants I
wrote came from a truncated `toFixed(9)` probe and the test failed, which
is precisely the class of silent precision drift it exists to catch.

`hotline3d` is not a consumer and should not become one. Its generator uses
`%` and `Math.trunc` where mulberry32 uses `>>>`/`>>`; measured, it emits a
different sequence, so adopting this package would change its output.

29 tests, 100% branch/function/line/statement coverage, type-aware eslint,
and a committed dist/ with a CI freshness check -- the same bar web_ui
holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:42:41 +02:00

81 lines
2.0 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import {
createManualClock,
createRealClock,
realClock,
type Clock,
} from './clock.ts'
describe('createRealClock', () => {
it('reads from performance.now()', () => {
const spy = vi.spyOn(performance, 'now').mockReturnValue(1234)
expect(createRealClock().now()).toBe(1234)
spy.mockRestore()
})
it('advances as real time passes', () => {
const clock = createRealClock()
const first = clock.now()
expect(clock.now()).toBeGreaterThanOrEqual(first)
})
})
describe('realClock', () => {
it('is a stable shared instance', () => {
// The whole reason this export exists: a fresh object per render would
// re-fire every effect that takes the clock as a dependency.
expect(realClock).toBe(realClock)
expect(realClock).not.toBe(createRealClock())
})
it('satisfies the Clock interface', () => {
const asClock: Clock = realClock
expect(typeof asClock.now()).toBe('number')
})
})
describe('createManualClock', () => {
it('starts at 0 by default', () => {
expect(createManualClock().now()).toBe(0)
})
it('starts at the given time', () => {
expect(createManualClock(500).now()).toBe(500)
})
it('only moves when told to', () => {
const clock = createManualClock(100)
expect(clock.now()).toBe(100)
expect(clock.now()).toBe(100)
clock.advance(50)
expect(clock.now()).toBe(150)
})
it('accumulates repeated advances', () => {
const clock = createManualClock()
clock.advance(10)
clock.advance(15)
expect(clock.now()).toBe(25)
})
it('moves backwards on a negative advance', () => {
const clock = createManualClock(100)
clock.advance(-40)
expect(clock.now()).toBe(60)
})
it('jumps to an absolute time with set()', () => {
const clock = createManualClock(100)
clock.set(7)
expect(clock.now()).toBe(7)
})
it('keeps two instances independent', () => {
const a = createManualClock()
const b = createManualClock()
a.advance(99)
expect(b.now()).toBe(0)
})
})