import { describe, expect, it } from "vitest"; import { CarTelemetryStore } from "../src/car-telemetry.js"; import { openDatabase } from "../src/database.js"; import type { CarTelemetry } from "../src/adb.js"; function sample( recordedAt: number, values: { speed?: number; soc?: number; range?: number; charging?: number; volts?: number } = {} ): CarTelemetry { const available = (value: T) => ({ available: true as const, value }); const missing = { available: false as const, reason: "Unavailable in test" }; return { available: true, collectedAt: new Date(recordedAt).toISOString(), speedKph: values.speed === undefined ? missing : available(values.speed), gear: available("4"), wheelAngleDegrees: available(0), batteryPercent: values.soc === undefined ? missing : available(values.soc), rangeKm: values.range === undefined ? missing : available(values.range), batteryVoltage: values.volts === undefined ? missing : available(values.volts), totalConsumptionKwh: available(4.2), chargingStatus: values.charging === undefined ? missing : available(values.charging) }; } describe("car telemetry history", () => { it("keeps bounded raw samples, hourly rollups, lifetime records, and an observed range estimate", () => { const database = openDatabase(":memory:"); const store = new CarTelemetryStore(database); const startedAt = Date.now() - 2 * 60 * 60_000; store.record(sample(startedAt, { speed: 0, soc: 80, range: 320, charging: 0, volts: 398 })); for (let index = 1; index <= 400; index += 1) { store.record(sample(startedAt + index * 10_000, { speed: 100, soc: 80 - index * 0.02, range: 320 - index * 0.3, charging: 0, volts: 390 })); } const chargingAt = startedAt + 401 * 10_000; for (let index = 0; index <= 13; index += 1) { store.record(sample(chargingAt + index * 10_000, { speed: 0, soc: 60 + index * 0.2, range: 240 + index, charging: 1, volts: 405 })); } const overview = store.overview(); expect(overview.records.peakSpeedKph).toBe(100); expect(overview.records.peakChargingPowerKw).toBeGreaterThan(30); expect(overview.records.maxObservedRangeKm).toBe(320); expect(overview.lifetime.trackedDistanceKm).toBeGreaterThan(10); expect(overview.lifetime.ownRangeEstimateKm).toBeGreaterThan(100); expect(overview.lifetime.chargeSessions).toBe(1); expect(overview.storage.rawSamples).toBe(415); expect(store.history("24h").points.length).toBeGreaterThan(1); expect(store.history("all").points.length).toBeGreaterThan(0); store.prune(chargingAt + 91 * 24 * 60 * 60_000); const pruned = store.overview(); expect(pruned.storage.rawSamples).toBe(0); expect(pruned.records.peakSpeedKph).toBe(100); expect(pruned.lifetime.trackedDistanceKm).toBeGreaterThan(10); database.close(); }); it("leaves estimates empty until enough real observations exist", () => { const database = openDatabase(":memory:"); const store = new CarTelemetryStore(database); store.record(sample(Date.now(), { speed: 0, soc: 75, range: 280, charging: 0 })); expect(store.overview().lifetime.ownRangeEstimateKm).toBeNull(); expect(store.history("24h").points).toHaveLength(1); database.close(); }); it("clears history independently from all-time statistics", () => { const database = openDatabase(":memory:"); const store = new CarTelemetryStore(database); const startedAt = Date.now() - 10 * 60_000; store.record(sample(startedAt, { speed: 20, soc: 80, range: 320, charging: 0 })); for (let index = 1; index <= 40; index += 1) { store.record(sample(startedAt + index * 10_000, { speed: 110, soc: 80 - index * 0.15, range: 320 - index, charging: 0 })); } const before = store.overview(); expect(before.lifetime.trackedDistanceKm).toBeGreaterThan(10); expect(before.lifetime.ownRangeEstimateKm).not.toBeNull(); expect(store.clear("history")).toMatchObject({ mode: "history", deletedSamples: 41 }); const kept = store.overview(); expect(store.history("24h").points).toEqual([]); expect(kept.storage.rawSamples).toBe(0); expect(kept.storage.hourlyRollups).toBe(0); expect(kept.records).toEqual(before.records); expect(kept.lifetime).toEqual(before.lifetime); store.record(sample(Date.now(), { speed: 30, soc: 70, range: 260, charging: 0 })); expect(store.clear("all")).toMatchObject({ mode: "all", deletedSamples: 1 }); const reset = store.overview(); expect(store.history("all").points).toEqual([]); expect(reset.records).toEqual({ peakSpeedKph: null, peakSpeedAt: null, peakChargingPowerKw: null, peakChargingPowerAt: null, maxObservedRangeKm: null, maxObservedRangeAt: null }); expect(reset.lifetime).toMatchObject({ firstSampleAt: null, lastSampleAt: null, sampleCount: 0, trackedDistanceKm: 0, drivingSeconds: 0, chargingSeconds: 0, chargeSessions: 0, socUsedPercent: 0, averageMovingSpeedKph: null, ownRangeEstimateKm: null }); database.close(); }); });