not-tonight/tests/idCheck.test.ts
2026-07-20 18:45:50 +10:00

293 lines
10 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest';
import { SeededRNG } from '../src/core/SeededRNG';
import { idDisplayLines, idVerdict, type IdTell } from '../src/rules/idCheck';
import { generatePatron, _resetPatronSerial, type GeneratorContext } from '../src/patrons/generator';
import type { IdCard, Patron } from '../src/data/types';
const NIGHT = new Date('2026-07-18T00:00:00');
const card = (over: Partial<IdCard> = {}): IdCard => ({
name: 'Shazza Nguyen',
dob: '2000-06-01',
expiry: '2030-06-01',
photoSeed: 1,
...over,
});
const on = (iso: string): Date => new Date(`${iso}T00:00:00`);
describe('idVerdict — birthday boundary', () => {
it('18th birthday is tonight: 18, not underage, flagged', () => {
const v = idVerdict(card({ dob: '2008-07-18' }), NIGHT);
expect(v.claimedAge).toBe(18);
expect(v.underage).toBe(false);
expect(v.turnsEighteenTonight).toBe(true);
});
it('18th birthday is tomorrow: still 17, underage', () => {
const v = idVerdict(card({ dob: '2008-07-19' }), NIGHT);
expect(v.claimedAge).toBe(17);
expect(v.underage).toBe(true);
expect(v.turnsEighteenTonight).toBe(false);
});
it('18th birthday was yesterday: 18, no longer the birthday', () => {
const v = idVerdict(card({ dob: '2008-07-17' }), NIGHT);
expect(v.claimedAge).toBe(18);
expect(v.turnsEighteenTonight).toBe(false);
});
it('turnsEighteenTonight only ever fires at exactly 18', () => {
expect(idVerdict(card({ dob: '2007-07-18' }), NIGHT).turnsEighteenTonight).toBe(false);
expect(idVerdict(card({ dob: '2009-07-18' }), NIGHT).turnsEighteenTonight).toBe(false);
});
});
describe('idVerdict — leap day DOB', () => {
it('29 Feb DOB is still 17 on 28 Feb of a non-leap year', () => {
const v = idVerdict(card({ dob: '2008-02-29' }), on('2026-02-28'));
expect(v.claimedAge).toBe(17);
expect(v.underage).toBe(true);
});
it('29 Feb DOB turns 18 on 1 March of a non-leap year', () => {
const v = idVerdict(card({ dob: '2008-02-29' }), on('2026-03-01'));
expect(v.claimedAge).toBe(18);
expect(v.underage).toBe(false);
// Deliberate: leap babies get the flag on the day the age math grants it,
// rather than never getting it in a non-leap year.
expect(v.turnsEighteenTonight).toBe(true);
});
// NB: a leap baby's 18th birthday can never fall on a real 29 Feb (leap years
// are 4 apart, 18 is not a multiple of 4), so the interesting leap-night case
// is a 28 Feb DOB the day AFTER its birthday.
it('28 Feb DOB on a 29 Feb night: birthday was yesterday', () => {
const v = idVerdict(card({ dob: '2010-02-28' }), on('2028-02-29'));
expect(v.claimedAge).toBe(18);
expect(v.turnsEighteenTonight).toBe(false);
});
it('29 Feb DOB evaluated on a real 29 Feb is exactly on its birthday', () => {
const v = idVerdict(card({ dob: '2008-02-29' }), on('2028-02-29'));
expect(v.claimedAge).toBe(20);
expect(v.underage).toBe(false);
});
});
describe('idVerdict — expiry boundary', () => {
it('expiry yesterday is expired', () => {
const v = idVerdict(card({ expiry: '2026-07-17' }), NIGHT);
expect(v.expired).toBe(true);
expect(v.expiresTonight).toBe(false);
expect(v.tells).toEqual(['expired']);
});
it('expiry tonight is valid — the trap', () => {
const v = idVerdict(card({ expiry: '2026-07-18' }), NIGHT);
expect(v.expired).toBe(false);
expect(v.expiresTonight).toBe(true);
expect(v.tells).toEqual([]);
expect(v.looksFake).toBe(false);
});
it('expiry tomorrow is valid and unremarkable', () => {
const v = idVerdict(card({ expiry: '2026-07-19' }), NIGHT);
expect(v.expired).toBe(false);
expect(v.expiresTonight).toBe(false);
});
it('a nightDate carrying a time of day still compares day-to-day', () => {
const v = idVerdict(card({ expiry: '2026-07-18' }), new Date('2026-07-18T02:30:00'));
expect(v.expired).toBe(false);
expect(v.expiresTonight).toBe(true);
});
});
describe('idVerdict — tells', () => {
it('a single visible tell comes back alone', () => {
const v = idVerdict(card({ fake: { peelingLaminate: true } }), NIGHT);
expect(v.tells).toEqual(['peelingLaminate']);
expect(v.looksFake).toBe(true);
});
it('a clean card with a future expiry has no tells', () => {
const v = idVerdict(card(), NIGHT);
expect(v.tells).toEqual([]);
expect(v.looksFake).toBe(false);
});
it('no fake object but a past expiry is still catchable', () => {
const v = idVerdict(card({ expiry: '2024-01-01' }), NIGHT);
expect(v.tells).toEqual(['expired']);
expect(v.looksFake).toBe(true);
});
it('all tells come back in fixed reading order', () => {
const v = idVerdict(
card({
expiry: '2020-05-05',
fake: { photoMismatch: true, jokeName: true, wrongHologram: true, peelingLaminate: true },
}),
NIGHT,
);
expect(v.tells).toEqual([
'peelingLaminate',
'wrongHologram',
'jokeName',
'photoMismatch',
'expired',
]);
});
it('subsets keep that order regardless of how the flags were written', () => {
const v = idVerdict(card({ fake: { photoMismatch: true, wrongHologram: true } }), NIGHT);
expect(v.tells).toEqual(['wrongHologram', 'photoMismatch']);
});
it('an empty fake object alone is not a tell', () => {
const v = idVerdict(card({ fake: {} }), NIGHT);
expect(v.tells).toEqual([]);
expect(v.looksFake).toBe(false);
});
// Guards against a later "if underage, skip the tell scan" shortcut: the two
// are independent readings of the same card and must both survive.
it('a card that both claims underage and has expired reports both', () => {
const v = idVerdict(card({ dob: '2009-07-19', expiry: '2025-01-01' }), NIGHT);
expect(v.claimedAge).toBe(16);
expect(v.underage).toBe(true);
expect(v.expired).toBe(true);
expect(v.tells).toEqual(['expired']);
expect(v.looksFake).toBe(true);
});
it('TELL_ORDER covers every IdTell — no tell can be silently unreportable', () => {
const everyTell: IdTell[] = [
'peelingLaminate',
'wrongHologram',
'jokeName',
'photoMismatch',
'expired',
];
const v = idVerdict(
card({
expiry: '2020-05-05',
fake: { peelingLaminate: true, wrongHologram: true, jokeName: true, photoMismatch: true },
}),
NIGHT,
);
expect([...v.tells].sort()).toEqual([...everyTell].sort());
});
});
describe('idVerdict — calendar edges', () => {
it('DOB 31 Dec against a 1 Jan night', () => {
expect(idVerdict(card({ dob: '2008-12-31' }), on('2027-01-01')).claimedAge).toBe(18);
expect(idVerdict(card({ dob: '2009-01-01' }), on('2027-01-01')).turnsEighteenTonight).toBe(true);
});
it('DOB 31 Jan checked on 28 Feb', () => {
const v = idVerdict(card({ dob: '2008-01-31' }), on('2026-02-28'));
expect(v.claimedAge).toBe(18);
expect(v.turnsEighteenTonight).toBe(false);
});
});
describe('idDisplayLines', () => {
it('formats DD/MM/YYYY with zero padding', () => {
const lines = idDisplayLines(card({ dob: '2008-01-05', expiry: '2029-11-09' }), NIGHT);
expect(lines.dob).toBe('05/01/2008');
expect(lines.expiry).toBe('09/11/2029');
expect(lines.name).toBe('Shazza Nguyen');
expect(lines.age).toBe('18');
});
it('age line agrees with the verdict at the underage boundary', () => {
const id = card({ dob: '2008-07-19' });
expect(idDisplayLines(id, NIGHT).age).toBe('17');
expect(String(idVerdict(id, NIGHT).claimedAge)).toBe('17');
});
});
describe('integration sweep over generated patrons', () => {
const SWEEP_NIGHT = new Date('2026-07-18T00:00:00');
let crowd: Patron[];
let kids: Patron[];
beforeEach(() => {
_resetPatronSerial();
const ctx: GeneratorContext = { rng: new SeededRNG(11), nightDate: SWEEP_NIGHT };
crowd = Array.from({ length: 800 }, (_, i) => generatePatron(ctx, i % 360));
const kidCtx: GeneratorContext = { rng: new SeededRNG(11), nightDate: SWEEP_NIGHT };
kids = Array.from({ length: 60 }, () => generatePatron(kidCtx, 0, 'almost18'));
});
it('every fake in the crowd is actually catchable', () => {
const suspect = crowd.filter(
(p) => p.idCard.fake || new Date(`${p.idCard.expiry}T00:00:00`) < SWEEP_NIGHT,
);
expect(suspect.length).toBeGreaterThan(0);
for (const p of suspect) {
expect(idVerdict(p.idCard, SWEEP_NIGHT).looksFake).toBe(true);
}
});
it('clean cards in the crowd never read as fake', () => {
const clean = crowd.filter(
(p) => !p.idCard.fake && new Date(`${p.idCard.expiry}T00:00:00`) >= SWEEP_NIGHT,
);
expect(clean.length).toBeGreaterThan(0);
for (const p of clean) {
expect(idVerdict(p.idCard, SWEEP_NIGHT).looksFake).toBe(false);
}
});
it("almost18 cards claim 18+ while the kid is 17 — tells are the only catch", () => {
expect(kids.length).toBe(60);
for (const kid of kids) {
expect(kid.age).toBe(17);
const v = idVerdict(kid.idCard, SWEEP_NIGHT);
expect(v.claimedAge).toBeGreaterThanOrEqual(18);
expect(v.underage).toBe(false);
expect(v.looksFake).toBe(true);
}
});
// The override run above is a forced sample; this checks the almost18s that
// actually turn up in a naturally-weighted crowd, which is what ships.
it('almost18s occurring naturally in the crowd are also lying and catchable', () => {
const inCrowd = crowd.filter((p) => p.archetype === 'almost18');
expect(inCrowd.length).toBeGreaterThan(0);
for (const kid of inCrowd) {
expect(kid.age).toBe(17);
const v = idVerdict(kid.idCard, SWEEP_NIGHT);
expect(v.claimedAge).toBeGreaterThanOrEqual(18);
expect(v.underage).toBe(false);
expect(v.looksFake).toBe(true);
}
});
it('idVerdict never throws across the whole crowd', () => {
expect(() => {
for (const p of [...crowd, ...kids]) {
idVerdict(p.idCard, SWEEP_NIGHT);
idDisplayLines(p.idCard, SWEEP_NIGHT);
}
}).not.toThrow();
});
});
describe('idVerdict — passback', () => {
it('a passback-flagged card looks fake, with passback among the tells', () => {
const v = idVerdict(card({ fake: { passback: true } }), NIGHT);
expect(v.looksFake).toBe(true);
expect(v.tells).toContain('passback');
});
it('the flag defaults off — an unflagged card carries no passback tell', () => {
const v = idVerdict(card(), NIGHT);
expect(v.tells).not.toContain('passback');
});
});