79 lines
3.3 KiB
TypeScript
79 lines
3.3 KiB
TypeScript
|
|
import { describe, expect, it } from 'vitest'
|
||
|
|
import type { Curve96 } from '@vpp/domain'
|
||
|
|
import { MemoryTimeSeriesStore } from '../src/timeseries.js'
|
||
|
|
|
||
|
|
const curve = (date: string, fill = '1.0'): Curve96 => ({
|
||
|
|
interval_minutes: 15,
|
||
|
|
date,
|
||
|
|
values: Array.from({ length: 96 }, () => fill),
|
||
|
|
})
|
||
|
|
|
||
|
|
let tick = 0
|
||
|
|
const clock = () => `2026-03-14T08:00:${String(tick++).padStart(2, '0')}Z`
|
||
|
|
|
||
|
|
describe('time-series store', () => {
|
||
|
|
it('writes a revision and reads it back as latest', () => {
|
||
|
|
const store = new MemoryTimeSeriesStore(clock)
|
||
|
|
const rec = store.write('load:agg-01', curve('2026-03-14'), 'meter-file')
|
||
|
|
expect(rec.version).toBe(1)
|
||
|
|
expect(store.latest('load:agg-01', '2026-03-14')).toEqual(rec)
|
||
|
|
expect(store.latest('load:agg-01', '2026-03-15')).toBeUndefined()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('keeps revision history instead of overwriting (late corrections)', () => {
|
||
|
|
const store = new MemoryTimeSeriesStore(clock)
|
||
|
|
store.write('load:agg-01', curve('2026-03-14', '1.0'), 'edge')
|
||
|
|
const corrected = store.write('load:agg-01', curve('2026-03-14', '1.1'), 'settlement-meter')
|
||
|
|
expect(corrected.version).toBe(2)
|
||
|
|
const history = store.revisions('load:agg-01', '2026-03-14')
|
||
|
|
expect(history.map((r) => r.version)).toEqual([1, 2])
|
||
|
|
expect(history[0]!.curve.values[0]).toBe('1.0')
|
||
|
|
expect(store.latest('load:agg-01', '2026-03-14')!.curve.values[0]).toBe('1.1')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('is idempotent for identical content', () => {
|
||
|
|
const store = new MemoryTimeSeriesStore(clock)
|
||
|
|
const a = store.write('pv:site-7', curve('2026-03-14'), 'edge')
|
||
|
|
const b = store.write('pv:site-7', curve('2026-03-14'), 'edge-retry')
|
||
|
|
expect(b).toBe(a)
|
||
|
|
expect(store.revisions('pv:site-7', '2026-03-14')).toHaveLength(1)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('returns an inclusive date range with latest revision per date, sorted', () => {
|
||
|
|
const store = new MemoryTimeSeriesStore(clock)
|
||
|
|
store.write('load:agg-01', curve('2026-03-16'), 'edge')
|
||
|
|
store.write('load:agg-01', curve('2026-03-14', '1.0'), 'edge')
|
||
|
|
store.write('load:agg-01', curve('2026-03-14', '2.0'), 'edge')
|
||
|
|
store.write('load:agg-01', curve('2026-03-13'), 'edge')
|
||
|
|
store.write('load:agg-01', curve('2026-03-17'), 'edge')
|
||
|
|
store.write('pv:site-7', curve('2026-03-15'), 'edge')
|
||
|
|
const out = store.range('load:agg-01', '2026-03-14', '2026-03-16')
|
||
|
|
expect(out.map((r) => r.date)).toEqual(['2026-03-14', '2026-03-16'])
|
||
|
|
expect(out[0]!.version).toBe(2)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('rejects malformed curves and empty series ids', () => {
|
||
|
|
const store = new MemoryTimeSeriesStore(clock)
|
||
|
|
expect(() => store.write('', curve('2026-03-14'), 'edge')).toThrow(/series_id/)
|
||
|
|
expect(() =>
|
||
|
|
store.write('load:x', { ...curve('2026-03-14'), values: ['1.0'] }, 'edge'),
|
||
|
|
).toThrow()
|
||
|
|
expect(() => store.range('load:x', '2026/03/14', '2026-03-15')).toThrow()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('stored curves are isolated from caller mutation', () => {
|
||
|
|
const store = new MemoryTimeSeriesStore(clock)
|
||
|
|
const input = curve('2026-03-14')
|
||
|
|
store.write('load:x', input, 'edge')
|
||
|
|
input.values[0] = '999'
|
||
|
|
expect(store.latest('load:x', '2026-03-14')!.curve.values[0]).toBe('1.0')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('lists series ids', () => {
|
||
|
|
const store = new MemoryTimeSeriesStore(clock)
|
||
|
|
store.write('pv:site-7', curve('2026-03-14'), 'edge')
|
||
|
|
store.write('load:agg-01', curve('2026-03-14'), 'edge')
|
||
|
|
expect(store.seriesIds()).toEqual(['load:agg-01', 'pv:site-7'])
|
||
|
|
})
|
||
|
|
})
|