lilith-platform.live/codebase/@packages/@lilith/tour-optimizer/src/optimize-tour.test.ts
2026-05-17 20:04:42 -07:00

342 lines
12 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import { optimizeTour } from './optimize-tour';
import type { OptimizerCandidate } from './types';
const BERKELEY = { slug: 'berkeley', lat: 37.8716, lon: -122.2727 };
function cand(p: Partial<OptimizerCandidate> & { slug: string; lat: number; lon: number; score: number }): OptimizerCandidate {
return {
slug: p.slug,
city: p.city ?? p.slug,
country: p.country ?? 'US',
isoCountryCode: p.isoCountryCode ?? 'US',
lat: p.lat,
lon: p.lon,
wealthScore: p.wealthScore ?? 50,
marketSaturationScore: p.marketSaturationScore ?? 50,
sexPositiveScore: p.sexPositiveScore ?? 50,
swLegalTier: p.swLegalTier ?? 'criminalized-buyer',
personalFitScore: p.personalFitScore ?? 50,
lastVisitAt: p.lastVisitAt ?? null,
lastRevenuePerDayUsd: p.lastRevenuePerDayUsd ?? null,
clearsBar: p.clearsBar ?? true,
gradeStepDelta: p.gradeStepDelta ?? 0,
computedOpportunityScore: p.score,
};
}
const WINDOW_30D = { start: '2026-06-01', end: '2026-06-30' };
describe('optimizeTour — input validation', () => {
it('throws when homebase has non-finite coords', async () => {
const promise = optimizeTour({
candidates: [],
homebase: { slug: 'broken', lat: Number.NaN, lon: -122 },
window: WINDOW_30D,
strategy: 'max-exposure',
});
await expect(promise).rejects.toThrow(/broken/);
});
it('throws when window dates are malformed', async () => {
const promise = optimizeTour({
candidates: [],
homebase: BERKELEY,
window: { start: 'not-a-date', end: '2026-06-30' },
strategy: 'max-exposure',
});
await expect(promise).rejects.toThrow(/invalid window/);
});
it('wraps internal errors in a descriptive top-level error', async () => {
const promise = optimizeTour({
candidates: [],
homebase: { slug: 'broken', lat: Number.NaN, lon: Number.NaN },
window: WINDOW_30D,
strategy: 'max-exposure',
});
await expect(promise).rejects.toThrow(/optimizeTour failed/);
});
});
describe('optimizeTour — empty / degenerate', () => {
it('returns empty itinerary when candidate list is empty', async () => {
const r = await optimizeTour({
candidates: [],
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
refinementBudgetMs: 50,
});
expect(r.visits).toEqual([]);
expect(r.totalPrize).toBe(0);
expect(r.totalTravelHours).toBe(0);
expect(r.skipped).toEqual([]);
expect(r.strategy).toBe('max-exposure');
});
it('routes a single candidate as a round-trip', async () => {
const r = await optimizeTour({
candidates: [cand({ slug: 'sf', lat: 37.7879, lon: -122.4075, score: 50 })],
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
refinementBudgetMs: 50,
});
expect(r.visits.length).toBe(1);
expect(r.visits[0]!.slug).toBe('sf');
expect(r.totalTravelHours).toBeGreaterThan(0);
});
});
describe('optimizeTour — skip reasons', () => {
it('candidates without coords land in skipped (low-value)', async () => {
const candidates: OptimizerCandidate[] = [
cand({ slug: 'sf', lat: 37.7879, lon: -122.4075, score: 50 }),
cand({ slug: 'ghost', lat: Number.NaN, lon: Number.NaN, score: 99 }),
];
const r = await optimizeTour({
candidates,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
refinementBudgetMs: 50,
});
const ghost = r.skipped.find((s) => s.slug === 'ghost');
expect(ghost?.reason).toBe('low-value');
});
it('strategy-filtered candidates appear in skipped with reason strategy-filter', async () => {
const r = await optimizeTour({
candidates: [
cand({ slug: 'dubai', lat: 25.26, lon: 55.29, score: 90, swLegalTier: 'criminalized-full', isoCountryCode: 'AE' }),
],
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'permissive-only',
refinementBudgetMs: 50,
});
expect(r.visits).toEqual([]);
expect(r.skipped.find((s) => s.slug === 'dubai')?.reason).toBe('strategy-filter');
});
it('over-maxStops candidates carry that reason, not no-time', async () => {
const candidates: OptimizerCandidate[] = [
cand({ slug: 'a', lat: 37.78, lon: -122.4, score: 90 }),
cand({ slug: 'b', lat: 37.34, lon: -121.9, score: 85 }),
cand({ slug: 'c', lat: 37.65, lon: -122.1, score: 70 }),
];
const r = await optimizeTour({
candidates,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
maxStops: 1,
refinementBudgetMs: 50,
});
expect(r.visits.length).toBe(1);
const reasons = new Set(r.skipped.map((s) => s.reason));
expect(reasons.has('over-max-stops')).toBe(true);
});
});
describe('optimizeTour — date arithmetic', () => {
it('arrive dates accumulate forward from window.start', async () => {
const candidates: OptimizerCandidate[] = [
cand({ slug: 'sf', lat: 37.7879, lon: -122.4075, score: 50 }),
cand({ slug: 'la', lat: 34.0522, lon: -118.2437, score: 80 }),
];
const r = await optimizeTour({
candidates,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
maxStops: 2,
minDwellNights: 2,
refinementBudgetMs: 50,
});
expect(r.visits.length).toBe(2);
const first = r.visits[0]!;
const second = r.visits[1]!;
expect(first.arrive >= '2026-06-01').toBe(true);
expect(first.depart > first.arrive).toBe(true);
expect(second.arrive >= first.depart).toBe(true);
});
it('depart - arrive is at least minDwellNights days', async () => {
const r = await optimizeTour({
candidates: [cand({ slug: 'sf', lat: 37.7879, lon: -122.4075, score: 50 })],
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
minDwellNights: 5,
refinementBudgetMs: 50,
});
const v = r.visits[0]!;
const arriveMs = Date.parse(v.arrive + 'T00:00:00Z');
const departMs = Date.parse(v.depart + 'T00:00:00Z');
const diffDays = (departMs - arriveMs) / (1000 * 60 * 60 * 24);
expect(diffDays).toBeGreaterThanOrEqual(5);
});
it('itinerary fits within the window when budget allows', async () => {
const candidates: OptimizerCandidate[] = [
cand({ slug: 'sf', lat: 37.7879, lon: -122.4075, score: 50 }),
cand({ slug: 'la', lat: 34.0522, lon: -118.2437, score: 80 }),
cand({ slug: 'vegas', lat: 36.1716, lon: -115.1391, score: 75 }),
];
const r = await optimizeTour({
candidates,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
maxStops: 3,
refinementBudgetMs: 100,
});
for (const v of r.visits) {
expect(v.depart <= '2026-06-30').toBe(true);
}
});
});
describe('optimizeTour — strategy semantics over real-shaped data', () => {
const REGIONS: OptimizerCandidate[] = [
cand({ slug: 'sf', lat: 37.7879, lon: -122.4075, score: 70, wealthScore: 85, swLegalTier: 'criminalized-buyer', clearsBar: true }),
cand({ slug: 'cinci', lat: 39.1031, lon: -84.512, score: 55, wealthScore: 50, swLegalTier: 'criminalized-full', clearsBar: true, gradeStepDelta: 3, lastRevenuePerDayUsd: 1800 }),
cand({ slug: 'dubai', lat: 25.26, lon: 55.29, score: 80, wealthScore: 95, swLegalTier: 'criminalized-full', isoCountryCode: 'AE', clearsBar: false }),
cand({ slug: 'amsterdam', lat: 52.37, lon: 4.89, score: 65, wealthScore: 85, swLegalTier: 'legal-regulated', isoCountryCode: 'NL', clearsBar: true }),
];
it('permissive-only never selects criminalized-full cities', async () => {
const r = await optimizeTour({
candidates: REGIONS,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'permissive-only',
maxStops: 4,
refinementBudgetMs: 100,
});
for (const v of r.visits) {
expect(['dubai', 'cinci']).not.toContain(v.slug);
}
});
it('niche-dominance picks cinci (delta=3) over the others', async () => {
const r = await optimizeTour({
candidates: REGIONS,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'niche-dominance',
maxStops: 4,
refinementBudgetMs: 100,
});
expect(r.visits.map((v) => v.slug)).toContain('cinci');
// The other cities lack gradeStepDelta ≥ 1 / clearsBar=true → filtered out.
const filtered = r.skipped.filter((s) => s.reason === 'strategy-filter').map((s) => s.slug);
expect(filtered).toContain('dubai');
});
it('max-margin biases towards revenue-bearing cinci', async () => {
const r = await optimizeTour({
candidates: REGIONS,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-margin',
maxStops: 2,
refinementBudgetMs: 100,
});
expect(r.visits.map((v) => v.slug)).toContain('cinci');
// dubai fails clears_bar filter
expect(r.skipped.find((s) => s.slug === 'dubai')?.reason).toBe('strategy-filter');
});
});
describe('optimizeTour — pinning', () => {
it('honours multiple forceInclude entries', async () => {
const candidates: OptimizerCandidate[] = [
cand({ slug: 'a', lat: 37.78, lon: -122.4, score: 5 }),
cand({ slug: 'b', lat: 34.05, lon: -118.24, score: 5 }),
cand({ slug: 'c', lat: 36.17, lon: -115.13, score: 90 }),
];
const r = await optimizeTour({
candidates,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'expand-from-anchor',
forceInclude: ['a', 'b'],
maxStops: 5,
refinementBudgetMs: 100,
});
const slugs = new Set(r.visits.map((v) => v.slug));
expect(slugs.has('a')).toBe(true);
expect(slugs.has('b')).toBe(true);
const pinned = r.visits.filter((v) => v.pinned).map((v) => v.slug);
expect(new Set(pinned)).toEqual(new Set(['a', 'b']));
});
it('pinned flag is false for non-pinned visits', async () => {
const candidates: OptimizerCandidate[] = [
cand({ slug: 'a', lat: 37.78, lon: -122.4, score: 90 }),
cand({ slug: 'b', lat: 34.05, lon: -118.24, score: 50 }),
];
const r = await optimizeTour({
candidates,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'edit-existing',
forceInclude: ['b'],
maxStops: 5,
refinementBudgetMs: 100,
});
const a = r.visits.find((v) => v.slug === 'a');
expect(a?.pinned).toBe(false);
});
});
describe('optimizeTour — totals invariants', () => {
const candidates: OptimizerCandidate[] = [
cand({ slug: 'sf', lat: 37.7879, lon: -122.4075, score: 50 }),
cand({ slug: 'la', lat: 34.0522, lon: -118.2437, score: 80 }),
cand({ slug: 'vegas', lat: 36.1716, lon: -115.1391, score: 75 }),
];
it('totalPrize equals sum of visit prizes (within rounding)', async () => {
const r = await optimizeTour({
candidates,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
maxStops: 3,
refinementBudgetMs: 100,
});
const sum = r.visits.reduce((s, v) => s + v.prize, 0);
expect(Math.abs(r.totalPrize - sum)).toBeLessThan(1.5);
});
it('totalTravelHours is non-negative and finite', async () => {
const r = await optimizeTour({
candidates,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
maxStops: 3,
refinementBudgetMs: 100,
});
expect(r.totalTravelHours).toBeGreaterThanOrEqual(0);
expect(Number.isFinite(r.totalTravelHours)).toBe(true);
});
it('totalTravelCostUsd is non-negative and finite', async () => {
const r = await optimizeTour({
candidates,
homebase: BERKELEY,
window: WINDOW_30D,
strategy: 'max-exposure',
maxStops: 3,
refinementBudgetMs: 100,
});
expect(r.totalTravelCostUsd).toBeGreaterThanOrEqual(0);
expect(Number.isFinite(r.totalTravelCostUsd)).toBe(true);
});
});