import { describe, it, expect, vi, beforeEach } from 'vitest'; import { extractExplicitFromText, detectProblemTicket, TICKET_NUMBER_REGEX, MAX_EXPLICIT_LINKS, discoverExplicitLinks, } from './link-discovery'; import type { RawTicketBundle } from './preprocessor'; vi.mock('@/lib/services/postgres-client', () => ({ default: { query: vi.fn(), }, })); import postgresClient from '@/lib/services/postgres-client'; const mockedQuery = postgresClient.query as unknown as ReturnType; function bundle(partial: Partial = {}): RawTicketBundle { return { ticket: { id: 1, ticket_number: 'T20260430.0084', title: 'Master problem ticket — Hynes', description: null, status: 1, status_label: 'New', priority: 1, priority_label: 'High', queue_id: null, queue_label: null, company_id: 100, company_name: 'Hynes Industries', contact_id: null, contact_name: null, contact_email: null, assigned_resource_id: null, assignee_name: null, assignee_email: null, create_date: '2026-04-30T12:00:00Z', last_activity_date: '2026-04-30T12:00:00Z', resolved_date_time: null, problem_ticket_id: null, ...partial, }, notes: [], time_entries: [], }; } beforeEach(() => { mockedQuery.mockReset(); }); describe('TICKET_NUMBER_REGEX', () => { it('matches the canonical Pulse format', () => { const m = 'see T20260430.0084 and T20260427.0142'.match(TICKET_NUMBER_REGEX); expect(m).toEqual(['T20260430.0084', 'T20260427.0142']); }); it('does not match invalid lengths', () => { expect('T2026.0084'.match(TICKET_NUMBER_REGEX)).toBeNull(); expect('T20260430.84'.match(TICKET_NUMBER_REGEX)).toBeNull(); }); }); describe('extractExplicitFromText', () => { it('returns medium-confidence refs from a free-text mention', () => { const r = extractExplicitFromText( 'See T20260427.0142 for context.', 'note_mention' ); expect(r.refs).toEqual([ { ticket_number: 'T20260427.0142', source: 'note_mention', confidence: 'medium' }, ]); expect(r.hasRelatedTicketsSection).toBe(false); }); it('marks refs in a RELATED TICKETS: block as high confidence', () => { const text = `Master problem ticket. RELATED TICKETS: T20260428.0053 — Allison Leone packet loss (OPEN) T20260427.0142 — George Droder Zoom dropping (OPEN) AFFECTED USERS: Allison, George`; const r = extractExplicitFromText(text, 'description_mention'); expect(r.hasRelatedTicketsSection).toBe(true); expect(r.refs).toEqual([ { ticket_number: 'T20260428.0053', source: 'related_tickets_section', confidence: 'high', }, { ticket_number: 'T20260427.0142', source: 'related_tickets_section', confidence: 'high', }, ]); }); it('does not mark refs after the RELATED TICKETS section ends as high', () => { const text = `RELATED TICKETS: T20260428.0053 — first OTHER NOTES: Background investigation found T20260101.0001 was a duplicate.`; const r = extractExplicitFromText(text, 'description_mention'); const high = r.refs.find((x) => x.ticket_number === 'T20260428.0053'); const other = r.refs.find((x) => x.ticket_number === 'T20260101.0001'); expect(high?.confidence).toBe('high'); expect(other?.confidence).toBe('medium'); expect(other?.source).toBe('description_mention'); }); it('dedupes within a single text', () => { const r = extractExplicitFromText( 'T20260427.0142 first, T20260427.0142 again, and T20260427.0142 once more.', 'note_mention' ); expect(r.refs).toHaveLength(1); }); it('returns empty for empty input', () => { expect(extractExplicitFromText('', 'note_mention').refs).toEqual([]); }); }); describe('detectProblemTicket', () => { it('flags master-problem-ticket title', () => { const r = detectProblemTicket( bundle({ title: 'Master problem ticket — recurring degradation' }), false ); expect(r.isProblemTicket).toBe(true); expect(r.signals).toContain('title:master_problem_ticket'); }); it('flags problem-ticket title', () => { const r = detectProblemTicket( bundle({ title: 'Problem ticket: keyboard outage' }), false ); expect(r.isProblemTicket).toBe(true); expect(r.signals).toContain('title:problem_ticket'); }); it('flags presence of RELATED TICKETS section', () => { const r = detectProblemTicket( bundle({ title: 'Plain ticket' }), true ); expect(r.isProblemTicket).toBe(true); expect(r.signals).toContain('description:related_tickets_section'); }); it('flags problem_ticket_id column', () => { const r = detectProblemTicket( bundle({ title: 'Plain ticket', problem_ticket_id: 999 }), false ); expect(r.isProblemTicket).toBe(true); expect(r.signals).toContain('column:problem_ticket_id'); }); it('returns false when none of the signals are present', () => { const r = detectProblemTicket(bundle({ title: 'Plain ticket' }), false); expect(r.isProblemTicket).toBe(false); expect(r.signals).toEqual([]); }); }); describe('discoverExplicitLinks', () => { it('skips self-references and unknown tickets', async () => { const b = bundle({ description: 'master ref T20260430.0084 (self), real ref T20260428.0053, ghost T20260101.9999', }); // First call: meta lookup. Only T20260428.0053 exists. mockedQuery.mockResolvedValueOnce({ rowCount: 1, rows: [ { ticket_number: 'T20260428.0053', title: 'Allison Leone', status_label: 'Open', last_activity_date: '2026-04-30T10:00:00Z', }, ], }); const r = await discoverExplicitLinks(b); expect(r.explicit).toHaveLength(1); expect(r.explicit[0].ticket_number).toBe('T20260428.0053'); }); it('caps explicit refs at MAX_EXPLICIT_LINKS', async () => { const refs = Array.from({ length: 30 }, (_, i) => `T2026010${i}.0001`).join(', '); const b = bundle({ description: `Many refs: ${refs}` }); // Return meta for all 15 it queries. mockedQuery.mockImplementationOnce(async (_sql: string, params: unknown[]) => { const numbers = params[0] as string[]; expect(numbers.length).toBeLessThanOrEqual(MAX_EXPLICIT_LINKS); return { rowCount: numbers.length, rows: numbers.map((n) => ({ ticket_number: n, title: 't', status_label: 'Open', last_activity_date: '2026-04-30T10:00:00Z', })), }; }); const r = await discoverExplicitLinks(b); expect(r.explicit.length).toBeLessThanOrEqual(MAX_EXPLICIT_LINKS); }); it('resolves problem_ticket_id and dedupes against text mention', async () => { const b = bundle({ description: 'See T20260427.0142 for context', problem_ticket_id: 555, }); // Call 1: resolve problem_ticket_id → ticket_number. mockedQuery.mockResolvedValueOnce({ rowCount: 1, rows: [{ ticket_number: 'T20260427.0142' }], }); // Call 2: meta lookup. mockedQuery.mockResolvedValueOnce({ rowCount: 1, rows: [ { ticket_number: 'T20260427.0142', title: 'George Droder', status_label: 'Open', last_activity_date: '2026-04-29T10:00:00Z', }, ], }); const r = await discoverExplicitLinks(b); // Same ticket from two sources should appear once at the higher confidence. expect(r.explicit).toHaveLength(1); expect(r.explicit[0].confidence).toBe('high'); expect(r.explicit[0].source).toBe('problem_ticket_id'); }); it('sorts high confidence first, then by activity date desc', async () => { const b = bundle({ description: `Master. RELATED TICKETS: T20260428.0053 — high Body mention: T20260427.0142 — medium`, }); mockedQuery.mockResolvedValueOnce({ rowCount: 2, rows: [ { ticket_number: 'T20260427.0142', title: 'a', status_label: 'Open', last_activity_date: '2026-04-30T10:00:00Z', }, { ticket_number: 'T20260428.0053', title: 'b', status_label: 'Open', last_activity_date: '2026-04-29T10:00:00Z', }, ], }); const r = await discoverExplicitLinks(b); expect(r.explicit.map((x) => x.ticket_number)).toEqual([ 'T20260428.0053', 'T20260427.0142', ]); expect(r.isProblemTicket).toBe(true); expect(r.problemTicketSignals).toContain('description:related_tickets_section'); }); it('returns empty when there are no refs and no signals', async () => { const b = bundle({ description: 'No ticket refs in here.', title: 'Plain ticket' }); mockedQuery.mockResolvedValueOnce({ rowCount: 0, rows: [] }); const r = await discoverExplicitLinks(b); expect(r.explicit).toEqual([]); expect(r.isProblemTicket).toBe(false); }); it('parses refs out of retained notes too, ignoring workflow noise', async () => { const b = bundle({ description: null, title: 'Plain', }); // Inject a workflow-noise note (filtered) and a real note (kept). b.notes = [ { id: 1, title: 'Workflow Rule "Foo" fired.', description: 'Mentions T20260101.0001 but should be ignored', note_type: 13, publish: 1, creator_resource_id: 4, creator_name: 'Autotask Administrator', creator_email: null, creator_type: 1, create_date_time: '2026-04-30T12:00:00Z', }, { id: 2, title: 'Tech note', description: 'See T20260427.0142 for the related issue', note_type: 1, publish: 1, creator_resource_id: 50, creator_name: 'Tech', creator_email: 'tech@wulfconsulting.com', creator_type: 1, create_date_time: '2026-04-30T13:00:00Z', }, ]; mockedQuery.mockResolvedValueOnce({ rowCount: 1, rows: [ { ticket_number: 'T20260427.0142', title: 'real', status_label: 'Open', last_activity_date: '2026-04-30T10:00:00Z', }, ], }); const r = await discoverExplicitLinks(b); expect(r.explicit.map((x) => x.ticket_number)).toEqual(['T20260427.0142']); expect(r.explicit[0].source).toBe('note_mention'); }); });