Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions packages/engine/src/__tests__/engine/clock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { describe, it, expect, vi } from 'vitest'
import { RealClock, TestClock } from '../../engine/clock.js'
import { parseDuration } from '../../engine/duration.js'

describe('RealClock', () => {
it('now() returns a number close to Date.now()', () => {
const clock = new RealClock()
const before = Date.now()
const result = clock.now()
const after = Date.now()
expect(result).toBeGreaterThanOrEqual(before)
expect(result).toBeLessThanOrEqual(after)
})

it('setTimeout and clearTimeout delegate to global timers', () => {
vi.useFakeTimers()
try {
const clock = new RealClock()
const fn = vi.fn()
clock.setTimeout(fn, 1000)
vi.advanceTimersByTime(500)
expect(fn).not.toHaveBeenCalled()
vi.advanceTimersByTime(500)
expect(fn).toHaveBeenCalledOnce()

// clearTimeout prevents execution
const fn2 = vi.fn()
const id2 = clock.setTimeout(fn2, 1000)
clock.clearTimeout(id2)
vi.advanceTimersByTime(2000)
expect(fn2).not.toHaveBeenCalled()
} finally {
vi.useRealTimers()
}
})
})

describe('TestClock', () => {
it('starts at time 0', () => {
const clock = new TestClock()
expect(clock.now()).toBe(0)
})

it('advance() moves time forward', () => {
const clock = new TestClock()
clock.advance(5000)
expect(clock.now()).toBe(5000)
})

it('advance() fires timers in chronological order', () => {
const clock = new TestClock()
const order: number[] = []

clock.setTimeout(() => order.push(1), 100)
clock.setTimeout(() => order.push(2), 50)
clock.setTimeout(() => order.push(3), 200)

clock.advance(200)

expect(order).toEqual([2, 1, 3])
})

it('advance() does not fire timers beyond the window', () => {
const clock = new TestClock()
const fn = vi.fn()

clock.setTimeout(fn, 1000)
clock.advance(500)

expect(fn).not.toHaveBeenCalled()
expect(clock.now()).toBe(500)
})

it('advance() fires timers at exact boundary', () => {
const clock = new TestClock()
const fn = vi.fn()

clock.setTimeout(fn, 100)
clock.advance(100)

expect(fn).toHaveBeenCalledOnce()
})

it('clearTimeout prevents a timer from firing', () => {
const clock = new TestClock()
const fn = vi.fn()

const id = clock.setTimeout(fn, 100)
clock.clearTimeout(id)
clock.advance(200)

expect(fn).not.toHaveBeenCalled()
})

it('multiple advance() calls accumulate time', () => {
const clock = new TestClock()
const fn = vi.fn()

clock.setTimeout(fn, 150)
clock.advance(100)
expect(fn).not.toHaveBeenCalled()
clock.advance(100)
expect(fn).toHaveBeenCalledOnce()
expect(clock.now()).toBe(200)
})

it('timer fn runs at the correct currentTime', () => {
const clock = new TestClock()
let capturedTime = -1

clock.setTimeout(() => {
capturedTime = clock.now()
}, 75)

clock.advance(100)

expect(capturedTime).toBe(75)
})
})

describe('parseDuration', () => {
it('parses PT24H', () => {
expect(parseDuration('PT24H')).toBe(24 * 3600 * 1000)
})

it('parses PT30M', () => {
expect(parseDuration('PT30M')).toBe(30 * 60 * 1000)
})

it('parses PT60S', () => {
expect(parseDuration('PT60S')).toBe(60 * 1000)
})

it('parses combined PT1H30M15S', () => {
expect(parseDuration('PT1H30M15S')).toBe((3600 + 1800 + 15) * 1000)
})

it('parses legacy shorthand 7d', () => {
expect(parseDuration('7d')).toBe(7 * 86_400_000)
})

it('parses legacy shorthand 24h', () => {
expect(parseDuration('24h')).toBe(24 * 3_600_000)
})

it('parses legacy shorthand 30m', () => {
expect(parseDuration('30m')).toBe(30 * 60_000)
})

it('parses legacy shorthand 60s', () => {
expect(parseDuration('60s')).toBe(60 * 1000)
})

it('throws on invalid format', () => {
expect(() => parseDuration('invalid')).toThrow('Invalid duration')
})

it('throws on empty PT', () => {
expect(() => parseDuration('PT')).toThrow('Invalid duration')
})

it('throws on P1D (day-level ISO not supported)', () => {
expect(() => parseDuration('P1D')).toThrow('Invalid duration')
})
})
Loading