citationBadge.test.ts3.5 KBView on GitHub
/**
 * What a citation badge says.
 *
 * The label is the whole point of the badge — it is what the reader sees without hovering — so
 * these pin the two things that make it useful: the type is never dropped, and the date is
 * never ambiguous about which year it means.
 */
import { eventCitationLabel } from '@/components/ui/citations/CitationPill';
import { formatCitationDate } from '@/components/ui/citations/citationStyles';
import { safeHref } from '@/components/ui/citations/WebCitationPill';
import type { EventCitation } from '@/store/messages/MessageTypes';

const at = (iso: string): number => new Date(iso).getTime();

const citation = (over: Partial<EventCitation> = {}): EventCitation => ({
  eventId: 'e1',
  eventType: 'meeting',
  quote: 'we run Salesforce',
  ...over,
});

describe('eventCitationLabel', () => {
  it('names the type and when it happened', () => {
    const label = eventCitationLabel(citation({ eventTime: at('2026-08-12T17:00:00Z') }));
    expect(label).toBe('Meeting · Aug 12');
  });

  it('falls back to the bare type for a citation saved before dates existed', () => {
    expect(eventCitationLabel(citation())).toBe('Meeting');
  });

  it('survives an unparseable time rather than rendering "Invalid Date"', () => {
    expect(eventCitationLabel(citation({ eventTime: Number.NaN }))).toBe('Meeting');
  });

  it('uses the event type Cedar knows, not a raw string', () => {
    expect(eventCitationLabel(citation({ eventType: 'slack' }))).toBe('Slack');
    expect(eventCitationLabel(citation({ eventType: 'external_crm' }))).toBe('CRM');
  });
});

describe('formatCitationDate', () => {
  it('carries the year for anything outside this one — "Aug 12" would read as this August', () => {
    expect(formatCitationDate(at('2024-08-12T17:00:00Z'))).toBe('Aug 12, 2024');
  });

  it('drops the year within the current one', () => {
    const thisYear = new Date();
    thisYear.setMonth(0, 15);
    thisYear.setHours(12, 0, 0, 0);
    expect(formatCitationDate(thisYear.getTime())).toBe('Jan 15');
  });

  it('says Today and Yesterday rather than a date the reader has to subtract', () => {
    const now = Date.now();
    expect(formatCitationDate(now)).toBe('Today');
    expect(formatCitationDate(now - 24 * 60 * 60 * 1000)).toBe('Yesterday');
  });

  it('has nothing to say without a time', () => {
    expect(formatCitationDate(undefined)).toBeNull();
  });
});

describe('safeHref', () => {
  // A citation's URL is written by whatever the agent searched or fetched and reaches the
  // anchor verbatim — the registry stores the string it was given and the persistence schema
  // types it `z.string()`. A `javascript:` result would otherwise render as a trusted Cedar
  // badge that executes on click.
  it('passes an ordinary web address through', () => {
    expect(safeHref('https://ashbyhq.com/careers')).toBe('https://ashbyhq.com/careers');
    expect(safeHref('http://example.com')).toBe('http://example.com');
  });

  it('refuses every scheme a badge must not navigate to', () => {
    expect(safeHref('javascript:alert(1)')).toBeNull();
    expect(safeHref('JavaScript:alert(1)')).toBeNull();
    expect(safeHref('data:text/html,<script>alert(1)</script>')).toBeNull();
    expect(safeHref('vbscript:msgbox(1)')).toBeNull();
    expect(safeHref('file:///etc/passwd')).toBeNull();
  });

  it('refuses something that is not a URL at all', () => {
    expect(safeHref('')).toBeNull();
    expect(safeHref('/relative/path')).toBeNull();
    expect(safeHref('not a url')).toBeNull();
  });
});