← back to Charge And Explore
backend/test/recommend.test.ts
99 lines
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { recommendStops, type StopCandidate } from "../src/recommend.ts";
import { defaultChargeCurve } from "../src/core/charge-estimate.ts";
const future = new Date("2026-08-01T12:00:00Z");
const candidate = (over: Partial<StopCandidate>): StopCandidate => ({
stationId: "s1",
name: "Stop 1",
detourMinutes: 3,
charger: { power: 90, stalls: 80, reliability: 80, dynamicAvailability: 70 },
amenity: 80,
nighttime: 70,
walkability: 80,
basicNeeds: 90,
dataCompleteness: 0.9,
charge: {
// A realistic ~35-min road-trip stop (slower charger / bigger fill) so a
// café run legitimately fits the conservative window.
arrivalSocPercent: 20,
targetSocPercent: 80,
usableBatteryKWh: 80,
vehicleMaxDcKw: 250,
stationMaxKw: 120,
chargingCurve: defaultChargeCurve,
environmentFactor: 1,
conversionEfficiency: 0.92,
connectionOverheadMinutes: 2,
},
activities: [
{
placeId: "cafe",
distanceMeters: 320,
walkOutMinutes: 4,
walkBackMinutes: 4,
suggestedVisitMinutes: 15,
openThrough: null,
preferenceScore: 80,
qualityScore: 80,
accessibilityScore: 80,
weatherFitScore: 80,
dataFreshnessScore: 80,
},
],
expectedReturnAt: future,
...over,
});
describe("recommendStops", () => {
it("ranks a low-detour, high-charger stop above a bad one", () => {
const ranked = recommendStops([
candidate({ stationId: "good", detourMinutes: 2 }),
candidate({
stationId: "bad",
detourMinutes: 40,
charger: { power: 20, stalls: 20, reliability: 20, dynamicAvailability: 0 },
basicNeeds: 0,
}),
]);
assert.equal(ranked[0]?.stationId, "good");
assert.ok((ranked[0]?.stop.score ?? 0) > (ranked[1]?.stop.score ?? 0));
});
it("surfaces a charge range and a fitting activity with distance", () => {
const [r] = recommendStops([candidate({})]);
assert.ok(r && r.chargeMinutes.low <= r.chargeMinutes.high);
assert.equal(r?.noActivityFits, false);
assert.equal(r?.activities[0]?.placeId, "cafe");
assert.equal(r?.activities[0]?.fits, true);
assert.equal(r?.activities[0]?.distanceMeters, 320);
});
it("flags when no activity fits the window", () => {
const [r] = recommendStops([
candidate({ activities: [
{
placeId: "toolong",
distanceMeters: 900,
walkOutMinutes: 10,
walkBackMinutes: 10,
suggestedVisitMinutes: 120,
openThrough: null,
preferenceScore: 80,
qualityScore: 80,
accessibilityScore: 80,
weatherFitScore: 80,
dataFreshnessScore: 80,
},
] }),
]);
assert.equal(r?.noActivityFits, true);
// the activity is still LISTED (with distance), just flagged as not fitting
assert.equal(r?.activities.length, 1);
assert.equal(r?.activities[0]?.fits, false);
assert.equal(r?.activities[0]?.distanceMeters, 900);
});
});