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();
});
});