import { describe, it, expect } from 'vitest'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import { preprocessTicket, tagTicketNote, tagTimeEntry, isWorkflowNoise, isEmailNotification, classifyActorType, computeContentHash, type RawTicketBundle, } from './preprocessor'; // Load the regression fixture once for the file. The fixture is the canonical // shape the data-access layer produces (header + ticket notes + time entries // joined with creator name/email). const FIXTURE_DIR = resolve(__dirname, 'fixtures'); const inputFixture = JSON.parse( readFileSync(`${FIXTURE_DIR}/T20260424.0045.input.json`, 'utf8') ) as RawTicketBundle; const expectedFixture = JSON.parse( readFileSync(`${FIXTURE_DIR}/T20260424.0045.expected.json`, 'utf8') ) as { preprocessor: { filtered_workflow_noise_ids: number[]; filtered_email_notification_ids_in_input: number[]; retained_note_ids: number[]; retained_time_entry_ids: number[]; }; }; describe('isWorkflowNoise', () => { it('matches by Autotask Administrator resource id', () => { expect( isWorkflowNoise({ id: 1, title: null, description: 'x', note_type: null, publish: null, creator_resource_id: 4, creator_name: 'Autotask Administrator', creator_email: null, creator_type: 1, create_date_time: '2026-04-24T00:00:00Z', }) ).toBe(true); }); it('matches by title prefix', () => { expect( isWorkflowNoise({ id: 1, title: 'Workflow Rule "Foo" fired.', description: 'x', note_type: 13, publish: 1, creator_resource_id: 99, creator_name: null, creator_email: null, creator_type: null, create_date_time: '2026-04-24T00:00:00Z', }) ).toBe(true); }); it('does not match a real ticket note', () => { expect( isWorkflowNoise({ id: 1, title: 'Wulf Support Ticket Update -', description: 'real content', note_type: 1, publish: 1, creator_resource_id: 29683311, creator_name: 'Lorentz Hinrichsen', creator_email: 'lorentz@wulfconsulting.com', creator_type: 1, create_date_time: '2026-04-24T00:00:00Z', }) ).toBe(false); }); }); describe('isEmailNotification', () => { it('matches "Service Desk Notification" titles', () => { expect( isEmailNotification({ id: 1, title: 'Service Desk Notification', description: 'a@b.com, c@d.com', note_type: 2, publish: 4, creator_resource_id: 99, creator_name: null, creator_email: null, creator_type: null, create_date_time: '2026-04-24T00:00:00Z', }) ).toBe(true); }); it('does not match other titles', () => { expect( isEmailNotification({ id: 1, title: 'Wulf Support Ticket Update -', description: 'x', note_type: 1, publish: 1, creator_resource_id: 99, creator_name: null, creator_email: null, creator_type: null, create_date_time: '2026-04-24T00:00:00Z', }) ).toBe(false); }); }); describe('classifyActorType', () => { it.each([ ['lorentz@wulfconsulting.com', null, 'wulf_tech'], ['cory.houck@wulfconsulting.com', null, 'wulf_tech'], ['tlyster@seubert.com', null, 'client_contact'], ['rmurphy@vertafore.com', null, 'vendor'], [null, null, 'system'], [undefined, null, 'system'], [null, 4, 'automation'], // Autotask Administrator id ['anyone@anywhere.com', 4, 'automation'], // creator id beats domain ['mixed@WulfConsulting.COM', null, 'wulf_tech'], // case-insensitive ] as const)('email=%s creatorId=%s -> %s', (email, creatorId, expected) => { expect(classifyActorType(email, creatorId)).toBe(expected); }); }); describe('tagTicketNote', () => { const baseNote = { id: 1, title: 'Wulf Support Ticket Update -', description: 'hello', note_type: 1, creator_type: 1, create_date_time: '2026-04-24T14:34:46.583Z', }; it('tags publish=1 as customer_facing and puts content in summary_notes', () => { const evt = tagTicketNote({ ...baseNote, publish: 1, creator_resource_id: 29683311, creator_name: 'Lorentz Hinrichsen', creator_email: 'lorentz@wulfconsulting.com', }); expect(evt).toMatchObject({ source: 'ticket_note', visibility: 'customer_facing', actor: 'Lorentz Hinrichsen', actor_type: 'wulf_tech', summary_notes: 'hello', }); expect(evt?.internal_notes).toBeUndefined(); }); it('tags publish=2 as internal_only and puts content in internal_notes', () => { const evt = tagTicketNote({ ...baseNote, publish: 2, creator_resource_id: 29683311, creator_name: 'Lorentz Hinrichsen', creator_email: 'lorentz@wulfconsulting.com', }); expect(evt).toMatchObject({ visibility: 'internal_only', internal_notes: 'hello', }); expect(evt?.summary_notes).toBeUndefined(); }); }); describe('tagTimeEntry', () => { const baseEntry = { id: 1, resource_id: 30861463, resource_name: 'Cory Houck', resource_email: 'cory.houck@wulfconsulting.com', hours_worked: 0.5, entry_date: '2026-04-24T00:00:00Z', start_date_time: null, end_date_time: '2026-04-24T15:00:00Z', type: 2, }; it('returns mixed visibility when both notes and internal_notes are present', () => { const evt = tagTimeEntry({ ...baseEntry, notes: 'public summary', internal_notes: 'internal-only follow-up', }); expect(evt).toMatchObject({ source: 'time_entry', visibility: 'mixed', summary_notes: 'public summary', internal_notes: 'internal-only follow-up', hours: 0.5, }); }); it('returns customer_facing when only notes are present', () => { const evt = tagTimeEntry({ ...baseEntry, notes: 'public', internal_notes: null }); expect(evt?.visibility).toBe('customer_facing'); expect(evt?.summary_notes).toBe('public'); expect(evt?.internal_notes).toBeUndefined(); }); it('returns internal_only when only internal_notes are present', () => { const evt = tagTimeEntry({ ...baseEntry, notes: null, internal_notes: 'internal' }); expect(evt?.visibility).toBe('internal_only'); expect(evt?.internal_notes).toBe('internal'); expect(evt?.summary_notes).toBeUndefined(); }); it('drops entries with no narrative content', () => { expect(tagTimeEntry({ ...baseEntry, notes: null, internal_notes: null })).toBeNull(); expect(tagTimeEntry({ ...baseEntry, notes: ' ', internal_notes: '' })).toBeNull(); }); }); describe('computeContentHash', () => { const evt = { timestamp: '2026-04-24T12:53:50.163Z', actor: 'A', actor_type: 'wulf_tech' as const, source: 'ticket_create' as const, visibility: 'customer_facing' as const, summary_notes: 'x', }; it('is deterministic for the same input', () => { expect(computeContentHash([evt], 7, 8, 99)).toBe( computeContentHash([evt], 7, 8, 99) ); }); it('changes when ticket status changes', () => { expect(computeContentHash([evt], 7, 8, 99)).not.toBe( computeContentHash([evt], 5, 8, 99) ); }); it('changes when priority changes', () => { expect(computeContentHash([evt], 7, 8, 99)).not.toBe( computeContentHash([evt], 7, 1, 99) ); }); it('is independent of object key insertion order', () => { const e1 = { ...evt }; const e2 = { visibility: evt.visibility, summary_notes: evt.summary_notes, timestamp: evt.timestamp, actor_type: evt.actor_type, actor: evt.actor, source: evt.source, }; expect(computeContentHash([e1], 7, 8, 99)).toBe( computeContentHash([e2], 7, 8, 99) ); }); }); // ============================================================================= // Regression: end-to-end against the T20260424.0045 fixture. // ============================================================================= describe('preprocessTicket — T20260424.0045 fixture', () => { const result = preprocessTicket(inputFixture); it('filters the four workflow-rule firings', () => { const filteredIds = expectedFixture.preprocessor.filtered_workflow_noise_ids; for (const id of filteredIds) { expect(result.events.some((e) => e.source === 'ticket_note' && e.actor !== 'Autotask Administrator' || false)).toBe(true); // Confirm none of the filtered IDs survived as events. const survived = inputFixture.notes .filter((n) => filteredIds.includes(n.id)) .some((n) => result.events.some((e) => e.timestamp === n.create_date_time)); expect(survived).toBe(false); } }); it('filters all "Service Desk Notification" rows', () => { const filteredIds = expectedFixture.preprocessor.filtered_email_notification_ids_in_input; expect(filteredIds.length).toBeGreaterThan(0); const survived = inputFixture.notes .filter((n) => filteredIds.includes(n.id)) .some((n) => result.events.some((e) => e.timestamp === n.create_date_time && e.source === 'ticket_note')); expect(survived).toBe(false); }); it('reports filtered_noise = workflow + email-notification count', () => { const expected = expectedFixture.preprocessor.filtered_workflow_noise_ids.length + expectedFixture.preprocessor.filtered_email_notification_ids_in_input.length; expect(result.counts.filtered_noise).toBe(expected); }); it("retains Lorentz's \"I'll take it from here\" note as a tagged event", () => { const lorentzNote = inputFixture.notes.find((n) => n.id === 33738796)!; expect(lorentzNote).toBeDefined(); const matching = result.events.find( (e) => e.source === 'ticket_note' && e.timestamp === lorentzNote.create_date_time ); expect(matching).toBeDefined(); expect(matching).toMatchObject({ actor: 'Lorentz Hinrichsen', actor_type: 'wulf_tech', visibility: 'customer_facing', }); expect(matching?.summary_notes).toContain("take it from here"); }); it('emits 7 events total: 1 ticket_create + 1 retained note + 5 time entries', () => { expect(result.events.length).toBe(7); const counts = result.events.reduce>((acc, e) => { acc[e.source] = (acc[e.source] ?? 0) + 1; return acc; }, {}); expect(counts).toEqual({ ticket_create: 1, ticket_note: 1, time_entry: 5, }); }); it('produces a chronologically sorted timeline', () => { for (let i = 1; i < result.events.length; i++) { expect( result.events[i].timestamp >= result.events[i - 1].timestamp ).toBe(true); } }); it('tags the ticket_create as client_contact based on contact email', () => { const created = result.events.find((e) => e.source === 'ticket_create'); expect(created).toMatchObject({ actor: 'Tyler Lyster', actor_type: 'client_contact', visibility: 'customer_facing', }); }); it('tags time entries 465933, 465961, 466183 as mixed visibility', () => { // These are the entries with both summary and internal notes per the fixture. const expectedMixed = new Set([465933, 465961, 466183]); for (const id of expectedMixed) { const entry = inputFixture.time_entries.find((e) => e.id === id)!; const evt = result.events.find( (e) => e.source === 'time_entry' && e.hours === entry.hours_worked && e.summary_notes && e.internal_notes ); expect(evt, `time_entry ${id} should produce a mixed event`).toBeDefined(); expect(evt?.visibility).toBe('mixed'); } }); it('reports counts that add up', () => { const { customer_facing, internal_only, mixed, total_events } = result.counts; expect(customer_facing + internal_only + mixed).toBe(total_events); expect(total_events).toBe(7); // 1 ticket_create + 1 ticket_note (publish=1) + 2 time_entries with only notes = 4 customer_facing expect(customer_facing).toBe(4); expect(internal_only).toBe(0); expect(mixed).toBe(3); }); it('produces a stable content_hash across re-runs', () => { const a = preprocessTicket(inputFixture).content_hash; const b = preprocessTicket(inputFixture).content_hash; expect(a).toBe(b); expect(a).toMatch(/^[0-9a-f]{64}$/); }); it('changes content_hash if a meaningful field changes', () => { const original = preprocessTicket(inputFixture).content_hash; const modified = preprocessTicket({ ...inputFixture, ticket: { ...inputFixture.ticket, status: 5 }, }).content_hash; expect(modified).not.toBe(original); }); it('populates the LLM-bound header correctly', () => { expect(result.header).toMatchObject({ ticket_number: 'T20260424.0045', autotask_ticket_id: 680282, status_label: 'Waiting Customer', priority_label: 'Minor Service', queue: 'Level 2 Support', account_name: 'Seubert and Associates', contact_name: 'Tyler Lyster', contact_email: 'tlyster@seubert.com', resolved_at: null, }); }); });