vpp-ai-platform/packages/services/test/ingest.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

69 lines
2.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { Curve96 } from '@vpp/domain'
import { IngestionPipeline } from '../src/ingest.js'
import type { IngestSnapshot } from '../src/ingest.js'
import { MemorySnapshotStore } from '../src/snapshot.js'
import { MemoryTimeSeriesStore } from '../src/timeseries.js'
const curve = (fill: string): Curve96 => ({
interval_minutes: 15,
date: '2026-03-14',
values: Array.from({ length: 96 }, () => fill),
})
const clock = () => '2026-03-14T08:00:00Z'
const make = () => {
const timeseries = new MemoryTimeSeriesStore(clock)
const snapshots = new MemorySnapshotStore()
return { timeseries, snapshots, pipeline: new IngestionPipeline({ timeseries, snapshots, clock }) }
}
describe('ingestion pipeline skeleton', () => {
it('snapshots then stores a curve that passes the quality gate', () => {
const { timeseries, snapshots, pipeline } = make()
const result = pipeline.ingestCurve('load:agg-01', curve('12.5'), 'meter-file')
expect(result.accepted).toBe(true)
if (!result.accepted) throw new Error('unreachable')
expect(result.record.version).toBe(1)
expect(timeseries.latest('load:agg-01', '2026-03-14')).toEqual(result.record)
const snap = snapshots.get(result.snapshot_ref) as IngestSnapshot
expect(snap.series_id).toBe('load:agg-01')
expect(snap.source).toBe('meter-file')
expect(snap.quality.ok).toBe(true)
expect(snap.curve).toEqual(curve('12.5'))
expect(pipeline.quarantined()).toHaveLength(0)
})
it('quarantines a curve that fails the gate and keeps it out of the store', () => {
const { timeseries, snapshots, pipeline } = make()
const result = pipeline.ingestCurve('load:agg-01', curve('0'), 'edge')
expect(result.accepted).toBe(false)
if (result.accepted) throw new Error('unreachable')
expect(result.issues[0]).toMatch(/flat-zero/)
expect(timeseries.latest('load:agg-01', '2026-03-14')).toBeUndefined()
// Quarantined input is still evidence: tagged with its issues and snapshotted.
const q = pipeline.quarantined()
expect(q).toHaveLength(1)
expect(q[0]).toMatchObject({ series_id: 'load:agg-01', date: '2026-03-14', source: 'edge' })
const snap = snapshots.get(q[0]!.snapshot_ref) as IngestSnapshot
expect(snap.quality.ok).toBe(false)
})
it('accepts a custom gate so stricter checks can be plugged in later', () => {
const timeseries = new MemoryTimeSeriesStore(clock)
const snapshots = new MemorySnapshotStore()
const pipeline = new IngestionPipeline({
timeseries,
snapshots,
clock,
gate: () => ({ ok: false, issues: ['always rejected'] }),
})
const result = pipeline.ingestCurve('load:x', curve('1.0'), 'edge')
expect(result.accepted).toBe(false)
expect(timeseries.seriesIds()).toEqual([])
})
})