vpp-ai-platform/packages/services/test/timeseries.test.ts
Thomas Bayes e7196bc88a M1: close gaps — CI, time-series/relational stores, ingestion skeleton, Python 3.11 pin
- .github/workflows/ci.yml: TS job (typecheck, re-export contracts, drift
  check, vitest) and Python job (3.11, regenerate pydantic models, drift
  check, pytest) — the dual-side contract test now actually runs in CI.
- packages/services: MemoryTimeSeriesStore (append-only daily-curve
  revisions), MemoryRepository + ResourceRegistry (schema-validated,
  optimistic versioning), IngestionPipeline (snapshot → quality gate →
  store or quarantine). In-memory reference semantics; persistent adapters
  arrive with M3 like the ledger.
- skills-py: .python-version + pyproject requires-python >=3.11; codegen
  script asserts interpreter version and runs the generator as a module.
- typecheck scripts per package and in root `check`; README status and dev
  setup; CLAUDE.md current-phase note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoYoGYzHkFyv3ALenkRPhA
2026-09-01 21:58:49 -04:00

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'])
})
})