← back to Charge And Explore
backend/test/activity-match.test.ts
85 lines
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
returnBuffer,
scoreActivity,
rankActivities,
type ActivityCandidate,
} from "../src/core/activity-match.ts";
const candidate = (over: Partial<ActivityCandidate>): ActivityCandidate => ({
placeId: "p1",
distanceMeters: 400,
walkOutMinutes: 5,
walkBackMinutes: 5,
suggestedVisitMinutes: 15,
openThrough: null,
preferenceScore: 80,
qualityScore: 80,
accessibilityScore: 80,
weatherFitScore: 80,
dataFreshnessScore: 80,
...over,
});
describe("returnBuffer", () => {
it("is at least 5 minutes", () => {
assert.equal(returnBuffer(10), 5);
});
it("is 20% of a large window", () => {
assert.equal(returnBuffer(60), 12);
});
});
describe("scoreActivity", () => {
const future = new Date("2026-08-01T12:00:00Z");
it("returns null when the activity cannot fit the window", () => {
// 5+5+40 visit + buffer >> 30-min window
const r = scoreActivity(candidate({ suggestedVisitMinutes: 40 }), 30, future);
assert.equal(r, null);
});
it("returns a score when it fits", () => {
const r = scoreActivity(candidate({}), 40, future);
assert.ok(typeof r === "number" && r > 0);
});
it("returns null when the place closes before expected return", () => {
const closesSoon = new Date("2026-08-01T11:00:00Z");
const r = scoreActivity(candidate({ openThrough: closesSoon }), 40, future);
assert.equal(r, null);
});
it("uses the conservative buffer (leaves margin before car is done)", () => {
// window 30, walk 5+5, visit 12, buffer 6 => required 28 <= 30 fits
const fits = scoreActivity(candidate({ suggestedVisitMinutes: 12 }), 30, future);
// visit 15 => required 31 > 30 does NOT fit
const doesnt = scoreActivity(candidate({ suggestedVisitMinutes: 15 }), 30, future);
assert.ok(fits !== null && doesnt === null);
});
});
describe("rankActivities", () => {
const future = new Date("2026-08-01T12:00:00Z");
it("drops non-fitting candidates and sorts best-first", () => {
const ranked = rankActivities(
[
candidate({ placeId: "good", suggestedVisitMinutes: 20, preferenceScore: 100 }),
candidate({ placeId: "toolong", suggestedVisitMinutes: 90 }),
candidate({ placeId: "ok", suggestedVisitMinutes: 20, preferenceScore: 40 }),
],
45,
future,
);
assert.equal(ranked.length, 2);
assert.equal(ranked[0]?.placeId, "good");
});
it("returns empty when nothing fits (caller should suggest a plain break)", () => {
const ranked = rankActivities([candidate({ suggestedVisitMinutes: 120 })], 20, future);
assert.deepEqual(ranked, []);
});
});