← back to Charge And Explore
backend/test/history.test.ts
113 lines
// Vehicle-keyed history (multi-car, TK-10227). Proves two cars' samples never
// interleave, the legacy single-vin map stays readable as the default vehicle,
// and the dedupe/retention rules hold per vehicle.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
appendHistoryPoint,
HISTORY_MAX_POINTS,
HISTORY_RETENTION_MS,
type HistoryPoint,
type HistorySample,
} from "../src/core/history.ts";
const MIN = 60 * 1000;
const T0 = 1_700_000_000_000;
function sample(vin: string, soc: number, over: Partial<HistorySample> = {}): HistorySample {
return { vin, soc, rangeMiles: soc * 3, chargingState: "Disconnected", ...over };
}
describe("appendHistoryPoint — vehicle keying", () => {
it("keys points under their own VIN — two cars never interleave", () => {
const h = new Map<string, HistoryPoint[]>();
assert.equal(appendHistoryPoint(h, sample("VINAAA", 80), T0), true);
assert.equal(appendHistoryPoint(h, sample("VINBBB", 40), T0 + 1 * MIN), true);
assert.equal(appendHistoryPoint(h, sample("VINAAA", 79), T0 + 10 * MIN), true);
assert.equal(appendHistoryPoint(h, sample("VINBBB", 39), T0 + 11 * MIN), true);
assert.deepEqual([...h.keys()].sort(), ["VINAAA", "VINBBB"]);
assert.deepEqual(h.get("VINAAA")!.map((p) => p.soc), [80, 79]);
assert.deepEqual(h.get("VINBBB")!.map((p) => p.soc), [40, 39]);
});
it("car B arriving within car A's dedupe window still records (independent spacing)", () => {
const h = new Map<string, HistoryPoint[]>();
appendHistoryPoint(h, sample("VINAAA", 80), T0);
// 1 min later, same SOC on car A → deduped; brand-new car B → recorded.
assert.equal(appendHistoryPoint(h, sample("VINAAA", 80), T0 + 1 * MIN), false);
assert.equal(appendHistoryPoint(h, sample("VINBBB", 80), T0 + 1 * MIN), true);
assert.equal(h.get("VINAAA")!.length, 1);
assert.equal(h.get("VINBBB")!.length, 1);
});
it("a pre-multi-car map (one vin) keeps its points untouched when a second car appears", () => {
const legacy: HistoryPoint[] = [
{ t: T0 - 2 * 24 * 60 * MIN, soc: 55, range: 165, charging: false },
{ t: T0 - 1 * 24 * 60 * MIN, soc: 52, range: 156, charging: false },
];
const h = new Map<string, HistoryPoint[]>([["VINAAA", [...legacy]]]);
appendHistoryPoint(h, sample("VINBBB", 90), T0);
assert.deepEqual(h.get("VINAAA"), legacy); // untouched — non-destructive
assert.equal(h.get("VINBBB")!.length, 1);
});
it("retention prunes per vehicle — one car's old points never trim another's", () => {
const h = new Map<string, HistoryPoint[]>();
appendHistoryPoint(h, sample("VINAAA", 70), T0 - HISTORY_RETENTION_MS - 24 * 60 * MIN);
appendHistoryPoint(h, sample("VINBBB", 60), T0 - 60 * MIN);
// fresh point on A prunes ONLY A's expired point; B untouched
appendHistoryPoint(h, sample("VINAAA", 68), T0);
assert.deepEqual(h.get("VINAAA")!.map((p) => p.soc), [68]);
assert.deepEqual(h.get("VINBBB")!.map((p) => p.soc), [60]);
});
it("caps at HISTORY_MAX_POINTS per vehicle", () => {
const h = new Map<string, HistoryPoint[]>();
const pts: HistoryPoint[] = Array.from({ length: HISTORY_MAX_POINTS }, (_, i) => ({
t: T0 - (HISTORY_MAX_POINTS - i) * MIN, soc: 50, range: 150, charging: false,
}));
h.set("VINAAA", pts);
appendHistoryPoint(h, sample("VINAAA", 51), T0 + 10 * MIN);
assert.equal(h.get("VINAAA")!.length, HISTORY_MAX_POINTS);
assert.equal(h.get("VINAAA")![HISTORY_MAX_POINTS - 1]!.soc, 51);
});
});
describe("appendHistoryPoint — recording rules (unchanged from single-car)", () => {
it("skips when soc is missing or vin is empty", () => {
const h = new Map<string, HistoryPoint[]>();
assert.equal(appendHistoryPoint(h, sample("VINAAA", null as unknown as number), T0), false);
assert.equal(appendHistoryPoint(h, sample("", 50), T0), false);
assert.equal(h.size, 0);
});
it("parked: dedupes within 5 min at the same SOC, records on SOC change", () => {
const h = new Map<string, HistoryPoint[]>();
appendHistoryPoint(h, sample("VINAAA", 80), T0);
assert.equal(appendHistoryPoint(h, sample("VINAAA", 80), T0 + 4 * MIN), false);
assert.equal(appendHistoryPoint(h, sample("VINAAA", 79), T0 + 4 * MIN), true);
});
it("charging: 2-min spacing even when SOC changed, kw/ea ride along", () => {
const h = new Map<string, HistoryPoint[]>();
const chg = (soc: number): HistorySample =>
sample("VINAAA", soc, { chargingState: "Charging", stats: { chargerPowerKw: 48, energyAddedKWh: 7.5 } });
appendHistoryPoint(h, chg(50), T0);
assert.equal(appendHistoryPoint(h, chg(51), T0 + 1 * MIN), false); // <2 min
assert.equal(appendHistoryPoint(h, chg(52), T0 + 3 * MIN), true);
const last = h.get("VINAAA")!.at(-1)!;
assert.equal(last.charging, true);
assert.equal(last.kw, 48);
assert.equal(last.ea, 7.5);
});
it("odometer + sentry ride along on every point when present", () => {
const h = new Map<string, HistoryPoint[]>();
appendHistoryPoint(h, sample("VINAAA", 66, { stats: { odometerMiles: 12345, sentry: true } }), T0);
const p = h.get("VINAAA")![0]!;
assert.equal(p.o, 12345);
assert.equal(p.s, true);
assert.equal(p.kw, undefined); // not charging → no kw/ea
});
});