import { describe, expect, it } from 'vitest'; import { detectRecencyIntent } from '../recency'; // Fixed "now" so the month-name branch is deterministic: 2026-08-07. const NOW = new Date('2026-08-07T10:00:00.000Z'); describe('detectRecencyIntent', () => { it('recognises the two questions that actually failed against a populated index', () => { // Both were asked by a real user and both returned nothing useful, because // bm25 matched the WORDS "last"/"July" rather than the dates. expect(detectRecencyIntent('who sent the last email', NOW)).not.toBeNull(); expect(detectRecencyIntent('summarize the input of all mails sent in July', NOW)).not.toBeNull(); }); it('turns a month name into a bounded range, not a top-N', () => { const intent = detectRecencyIntent('summarize all mails sent in July', NOW); // Asserted in LOCAL time on purpose. The bounds are local midnight // rendered as UTC instants, so west of UTC the ISO string reads as the // previous month - and that is correct, not a bug: a mail that arrived // 00:30 local on 1 July belongs to the user's July even though its UTC // timestamp says 30 June. Asserting the ISO prefix would enshrine the // wrong semantics and pass only in UTC. const since = new Date(intent!.since!); const until = new Date(intent!.until!); expect(since.getMonth()).toBe(6); // local July... expect(since.getDate()).toBe(1); // ...starting on the 1st expect(until.getMonth()).toBe(7); // exclusive upper bound = local 1 Aug expect(until.getDate()).toBe(1); }); it('reads a month later in the year as LAST year', () => { // Asked in August, "December" cannot mean four months from now. const intent = detectRecencyIntent('what came in December?', NOW); const since = new Date(intent!.since!); expect(since.getFullYear()).toBe(2025); expect(since.getMonth()).toBe(11); }); it('handles today and yesterday as distinct bounded days', () => { const today = detectRecencyIntent('anything today?', NOW); expect(today?.since).toBeDefined(); expect(today?.until).toBeUndefined(); const yesterday = detectRecencyIntent('what arrived yesterday', NOW); expect(yesterday?.since).toBeDefined(); expect(yesterday?.until).toBeDefined(); expect(new Date(yesterday!.until!).getTime()).toBeGreaterThan(new Date(yesterday!.since!).getTime()); }); it('understands German recency wording — the app ships a German UI', () => { expect(detectRecencyIntent('welche war die letzte Mail?', NOW)).not.toBeNull(); expect(detectRecencyIntent('was kam heute an', NOW)).not.toBeNull(); const juli = detectRecencyIntent('Mails aus Juli zusammenfassen', NOW); expect(new Date(juli!.since!).getMonth()).toBe(6); }); it('gives an unbounded top-N when recency is implied but no period named', () => { const intent = detectRecencyIntent('what is the newest message', NOW); expect(intent?.since).toBeUndefined(); expect(intent?.until).toBeUndefined(); expect(intent?.limit).toBeGreaterThan(0); }); it('does NOT fire on pure content questions — those are keyword search\'s job', () => { expect(detectRecencyIntent('what did Anna say about the invoice?', NOW)).toBeNull(); expect(detectRecencyIntent('when is check-in for the Villa sul Lago booking?', NOW)).toBeNull(); expect(detectRecencyIntent('find the contract with Bechtle', NOW)).toBeNull(); }); it('does not treat a word merely CONTAINING a keyword as recency', () => { // "newsletter" contains "new"; "lastly" contains "last". Word boundaries // matter or half a mailbox reads as a time question. expect(detectRecencyIntent('unsubscribe from the newsletter', NOW)).toBeNull(); }); });