import { describe, expect, it } from 'vitest'; import type { PackageState, PlaneState, StateSnapshot } from './game/state'; import { diffSnapshots } from './deriveEvents'; // ISS-008 (v0.17.0): snapshot-diffing event derivation — pure core of the toast/notification // feature, unit-tested in node like advanceSimClock/fetchState. const CATALOG = [ { model: 'atr72', label: 'ATR 72-600', price: 200_000, maxFuel: 6300, burnPerKm: 1.75, emptyWeightKg: 9300, speedKmh: 510, maxPayloadKg: 4800 }, ]; const basePlane = (id: string): PlaneState => ({ id, model: 'atr72', status: 'available', fuel: 6300, maxFuel: 6300, locationIcao: 'EDDF', locationName: 'Frankfurt', destIcao: null, destName: null, departureSimTime: null, lat: 50.03, lon: 8.56, health: 100, // v0.39.0 (ISS-069) — full-health fixture default repairUntilSimTime: null, queue: [], }); const basePackage = (id: string): PackageState => ({ id, originIcao: 'EDDF', originName: 'Frankfurt', destIcao: 'LEMD', destName: 'Madrid–Barajas', reward: 1234.5, cargoType: 'mail', cargoCount: 2, weightKg: 600, special: null, // v0.37.0 (ISS-073) — no special status by default in fixtures distanceKm: 1839, // v0.48.0 (ISS-095) — EDDF→LEMD great-circle distance deadlineSimTime: 0, status: 'open', planeId: null, createdSimTime: 0, }); const snapshot = (over: Partial = {}): StateSnapshot => ({ account: { cash: 20_000, reputation: 10 }, simState: { clockMs: 0 }, planes: [basePlane('plane-starter')], packages: [], catalog: CATALOG, fuelPricePerL: 0.8, sellFraction: 0.5, ...over, }); describe('diffSnapshots', () => { it('returns [] for identical snapshots (only clock/cash drift)', () => { const prev = snapshot(); const next = snapshot({ account: { cash: 21_000, reputation: 11 }, simState: { clockMs: 60_000 } }); expect(diffSnapshots(prev, next)).toEqual([]); }); it('returns [] for the first snapshot (no baseline — no "new package" spam on page load)', () => { const next = snapshot({ packages: [basePackage('pkg-1'), basePackage('pkg-2')] }); expect(diffSnapshots(null, next)).toEqual([]); }); it('emits a landing event when a plane goes flying → available (new locationName is the arrival)', () => { const prev = snapshot({ planes: [ { ...basePlane('plane-starter'), status: 'flying', destIcao: 'LEMD', destName: 'Madrid–Barajas', departureSimTime: 0, }, ], }); const next = snapshot({ planes: [ { ...basePlane('plane-starter'), locationIcao: 'LEMD', locationName: 'Madrid–Barajas' }, ], }); expect(diffSnapshots(prev, next)).toEqual([ { kind: 'info', message: 'ATR 72-600 landed at Madrid–Barajas' }, ]); }); it('emits a delivery event with the reward when a loaded package is delivered on arrival', () => { const prev = snapshot({ packages: [{ ...basePackage('pkg-1'), status: 'loaded', planeId: 'plane-starter' }] }); const next = snapshot({ packages: [{ ...basePackage('pkg-1'), status: 'delivered', planeId: 'plane-starter' }] }); expect(diffSnapshots(prev, next)).toEqual([ { kind: 'info', message: 'Package delivered: +€1,234.50' }, ]); }); it('emits an expiry event for open → expired and loaded → expired', () => { const fromOpen = diffSnapshots( snapshot({ packages: [basePackage('pkg-1')] }), snapshot({ packages: [{ ...basePackage('pkg-1'), status: 'expired' }] }), ); expect(fromOpen).toEqual([{ kind: 'info', message: 'Package expired: Frankfurt → Madrid–Barajas' }]); const fromLoaded = diffSnapshots( snapshot({ packages: [{ ...basePackage('pkg-2'), status: 'loaded', planeId: 'plane-starter' }] }), snapshot({ packages: [{ ...basePackage('pkg-2'), status: 'expired', planeId: 'plane-starter' }] }), ); expect(fromLoaded).toEqual([{ kind: 'info', message: 'Package expired: Frankfurt → Madrid–Barajas' }]); }); it('emits a new-package event for ids only in the next snapshot', () => { const prev = snapshot({ packages: [basePackage('pkg-1')] }); const next = snapshot({ packages: [basePackage('pkg-1'), basePackage('pkg-2')] }); expect(diffSnapshots(prev, next)).toEqual([ { kind: 'info', message: 'New package available: Frankfurt → Madrid–Barajas' }, ]); }); it('emits bought/sold events when the plane id set changes (with the model name)', () => { const bought = diffSnapshots( snapshot(), snapshot({ planes: [basePlane('plane-starter'), basePlane('plane-2')] }), ); expect(bought).toEqual([{ kind: 'info', message: 'Bought ATR 72-600' }]); const sold = diffSnapshots( snapshot({ planes: [basePlane('plane-starter'), basePlane('plane-2')] }), snapshot(), ); expect(sold).toEqual([{ kind: 'info', message: 'Sold ATR 72-600' }]); }); it('reports every transition in one diff (planes first, then packages)', () => { const prev = snapshot({ planes: [ { ...basePlane('plane-starter'), status: 'flying', destIcao: 'LEMD', destName: 'Madrid–Barajas' }, basePlane('plane-2'), ], packages: [{ ...basePackage('pkg-1'), status: 'loaded', planeId: 'plane-starter' }], }); const next = snapshot({ planes: [ { ...basePlane('plane-starter'), locationIcao: 'LEMD', locationName: 'Madrid–Barajas' }, basePlane('plane-3'), ], packages: [{ ...basePackage('pkg-1'), status: 'delivered', planeId: 'plane-starter' }], }); expect(diffSnapshots(prev, next)).toEqual([ { kind: 'info', message: 'ATR 72-600 landed at Madrid–Barajas' }, { kind: 'info', message: 'Bought ATR 72-600' }, { kind: 'info', message: 'Sold ATR 72-600' }, { kind: 'info', message: 'Package delivered: +€1,234.50' }, ]); }); it('ignores transitions that are not events (open → loaded, available → flying)', () => { const prev = snapshot({ packages: [basePackage('pkg-1')] }); const next = snapshot({ planes: [{ ...basePlane('plane-starter'), status: 'flying', destIcao: 'LEMD', destName: 'Madrid–Barajas' }], packages: [{ ...basePackage('pkg-1'), status: 'loaded', planeId: 'plane-starter' }], }); expect(diffSnapshots(prev, next)).toEqual([]); }); });