Files
Mavis a457b9d96f
CI / Lint, typecheck, test, build (pull_request) Failing after 10s
Phase 1: data pipeline (Postgres+Redis with in-memory fallback, mock telemetry publisher, WebSocket fan-out, hub /debug page)
- packages/db: postgres.js + ioredis wrapper, StateStore interface with
  InMemory + Postgres implementations, schema migration runner
- apps/api: refactored to use @kerbal-rt/db; live WebSocket hub subscribes
  to store changes and broadcasts to all clients
- apps/tools/mock-telemetry: Node script that generates realistic KSP
  state and POSTs to /api/v1/ingest at 1Hz (uses same keplerian math
  as the live-map renderer)
- apps/hub: new /debug page that connects to /api/v1/live and shows
  the live state
- ksp/README.md: documents Phase 1c (real kRPC bridge) and the two
  implementation options
- Tests: 17 total (5 kepler math, 5 db store, 5 API health/state,
  2 API WebSocket fan-out)

End-to-end verified: mock publisher → API → hub /debug page,
11 snapshots/5s over WebSocket, vessels advancing in mean anomaly.
2026-06-02 18:54:46 +00:00

107 lines
3.1 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { InMemoryStateStore } from '../src/store.js';
import type { UniverseSnapshot } from '@kerbal-rt/shared-types';
function makeSnapshot(ut: number, vesselId = 'v-1'): UniverseSnapshot {
return {
ut,
capturedAt: new Date().toISOString(),
activeVesselId: vesselId,
bodies: [
{
id: 'kerbin',
name: 'Kerbin',
kind: 'planet',
parentId: 'kerbol',
radius: 600_000,
sphereOfInfluence: 84_159_286,
gravitationalParameter: 3.5316e12,
rotationPeriod: 21_600,
axialTilt: 0,
orbit: {
semiMajorAxis: 13_599_840_256,
eccentricity: 0,
inclination: 0,
longitudeOfAscendingNode: 0,
argumentOfPeriapsis: 0,
meanAnomalyAtEpoch: 0,
epoch: 0,
},
},
],
vessels: [
{
id: vesselId,
name: 'Test',
type: 'Probe',
owner: 'KASA',
situation: 'ORBITING',
status: 'ACTIVE',
orbit: {
semiMajorAxis: 7e6,
eccentricity: 0,
inclination: 0,
longitudeOfAscendingNode: 0,
argumentOfPeriapsis: 0,
meanAnomalyAtEpoch: 0,
epoch: 0,
},
referenceBodyId: 'kerbin',
createdAt: '2026-01-01T00:00:00Z',
retiredAt: null,
},
],
groundStations: [],
};
}
describe('InMemoryStateStore', () => {
it('starts empty', async () => {
const store = new InMemoryStateStore();
expect(await store.latestSnapshot()).toBeNull();
expect(await store.listVessels()).toEqual([]);
});
it('stores and returns snapshots', async () => {
const store = new InMemoryStateStore();
await store.applySnapshot(makeSnapshot(100));
const snap = await store.latestSnapshot();
expect(snap).not.toBeNull();
expect(snap!.ut).toBe(100);
});
it('looks up vessels by id', async () => {
const store = new InMemoryStateStore();
await store.applySnapshot(makeSnapshot(1, 'kasa-1'));
const v = await store.getVessel('kasa-1');
expect(v?.name).toBe('Test');
expect(await store.getVessel('missing')).toBeNull();
});
it('overwrites on subsequent snapshots', async () => {
const store = new InMemoryStateStore();
await store.applySnapshot(makeSnapshot(1));
await store.applySnapshot(makeSnapshot(2));
expect((await store.latestSnapshot())!.ut).toBe(2);
});
it('notifies subscribers when a snapshot is applied', async () => {
const store = new InMemoryStateStore();
const seen: number[] = [];
const unsub = await store.subscribe((snap) => seen.push(snap.ut));
await store.applySnapshot(makeSnapshot(1));
// allow microtask to flush
await new Promise((r) => setTimeout(r, 10));
await store.applySnapshot(makeSnapshot(2));
await new Promise((r) => setTimeout(r, 10));
expect(seen).toEqual([1, 2]);
await unsub();
await store.applySnapshot(makeSnapshot(3));
await new Promise((r) => setTimeout(r, 10));
expect(seen).toEqual([1, 2]); // unsub'd
});
});