import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import { analyzerWorker } from './worker'; import * as dataAccess from './data-access'; import * as persistence from './persistence'; import * as pipelineModule from './pipeline'; import type { RawTicketBundle } from './preprocessor'; const FIXTURE = JSON.parse( readFileSync( resolve(__dirname, 'fixtures', 'T20260424.0045.input.json'), 'utf8' ) ) as RawTicketBundle; let loadSpy: ReturnType; let runPipelineSpy: ReturnType; let insertSpy: ReturnType; let completeSpy: ReturnType; let failSpy: ReturnType; let updateStatusSpy: ReturnType; beforeEach(() => { loadSpy = vi.spyOn(dataAccess, 'loadTicketBundle'); runPipelineSpy = vi.spyOn(pipelineModule, 'runPipeline'); insertSpy = vi .spyOn(persistence, 'insertAnalysis') .mockResolvedValue({ id: 'an_uuid', analysis_version: 1 }); completeSpy = vi.spyOn(persistence, 'completeJob').mockResolvedValue(); failSpy = vi.spyOn(persistence, 'failJob').mockResolvedValue(); updateStatusSpy = vi.spyOn(persistence, 'updateJobStatus').mockResolvedValue(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('analyzerWorker.runJob', () => { it('writes the analysis row and marks the job complete on a happy-path run', async () => { loadSpy.mockResolvedValueOnce(FIXTURE); runPipelineSpy.mockResolvedValueOnce({ outcome: 'complete', analysis: { summary: 's', timeline: [], what_was_done: [], what_should_have_been_done: [], gaps: [], next_step: 'next', next_step_rationale: 'why', post_resolution_analysis: null, confidence_score: 0.8, needs_human_review: false, human_review_reasons: [], ambiguities_for_opus: [], itglue_docs_referenced: [], }, pre: { header: { ticket_number: 'T20260424.0045', autotask_ticket_id: 680282, title: 't', 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', created_at: '2026-04-24T12:53:50.163Z', resolved_at: null, }, events: [], counts: { total_events: 0, customer_facing: 0, internal_only: 0, mixed: 0, filtered_noise: 8, }, content_hash: 'a'.repeat(64), }, meta: { haiku_used: true, sonnet_used: true, opus_used: false, total_input_tokens: 6_000, total_output_tokens: 800, total_cache_creation_tokens: 0, total_cache_read_tokens: 0, estimated_cost_usd: 0.05, cost_circuit_breaker_tripped: false, }, filtered_noise_count: 8, itglue_search_used: false, itglue_org_resolved: false, model_traces: {}, }); const result = await analyzerWorker.runJob( 'job_1', 'T20260424.0045', 'user_abc' ); expect(result.outcome).toBe('complete'); expect(result.analysis_id).toBe('an_uuid'); expect(insertSpy).toHaveBeenCalledTimes(1); expect(insertSpy.mock.calls[0][0]).toMatchObject({ ticket_number: 'T20260424.0045', autotask_ticket_id: 680282, triggered_by_user_id: 'user_abc', status: 'complete', haiku_used: true, sonnet_used: true, opus_used: false, filtered_noise_count: 8, }); expect(completeSpy).toHaveBeenCalledWith('job_1', 'an_uuid'); expect(failSpy).not.toHaveBeenCalled(); }); it('completes the job pointing at the existing analysis on idempotent short-circuit', async () => { loadSpy.mockResolvedValueOnce(FIXTURE); runPipelineSpy.mockResolvedValueOnce({ outcome: 'idempotent_short_circuit', existing_analysis_id: 'existing_id', existing_analysis_version: 4, pre: { header: { ticket_number: 'T20260424.0045', autotask_ticket_id: 680282, title: 't', status_label: 'x', priority_label: null, queue: null, account_name: null, contact_name: null, contact_email: null, created_at: '2026-04-24T12:53:50.163Z', resolved_at: null, }, events: [], counts: { total_events: 0, customer_facing: 0, internal_only: 0, mixed: 0, filtered_noise: 0, }, content_hash: 'b'.repeat(64), }, }); const result = await analyzerWorker.runJob('job_1', 'T20260424.0045', null); expect(result.outcome).toBe('idempotent_short_circuit'); expect(result.analysis_id).toBe('existing_id'); expect(insertSpy).not.toHaveBeenCalled(); expect(completeSpy).toHaveBeenCalledWith('job_1', 'existing_id'); }); it('marks the job failed with a clear message when the ticket is missing', async () => { loadSpy.mockRejectedValueOnce( new dataAccess.TicketNotFoundError('T20260424.0045') ); const result = await analyzerWorker.runJob( 'job_1', 'T20260424.0045', null ); expect(result.outcome).toBe('failed'); expect(result.analysis_id).toBeNull(); expect(failSpy).toHaveBeenCalledTimes(1); expect(failSpy.mock.calls[0][1]).toMatch(/not found in local mirror/); expect(insertSpy).not.toHaveBeenCalled(); expect(completeSpy).not.toHaveBeenCalled(); }); it('fails the job and persists the error message when the pipeline throws', async () => { loadSpy.mockResolvedValueOnce(FIXTURE); runPipelineSpy.mockRejectedValueOnce(new Error('LLM stage on claude-sonnet-4-6 failed twice')); const result = await analyzerWorker.runJob('job_1', 'T20260424.0045', null); expect(result.outcome).toBe('failed'); expect(failSpy).toHaveBeenCalledTimes(1); expect(failSpy.mock.calls[0][1]).toMatch(/failed twice/); }); it('drives updateJobStatus through the pipeline progress callbacks', async () => { loadSpy.mockResolvedValueOnce(FIXTURE); runPipelineSpy.mockImplementationOnce((async ( _input: unknown, _deps: unknown, callbacks: { onStage?: (stage: string) => Promise | void } | undefined ) => { await callbacks?.onStage?.('fetching'); await callbacks?.onStage?.('triaging'); await callbacks?.onStage?.('analyzing'); return { outcome: 'complete', analysis: { summary: 's', timeline: [], what_was_done: [], what_should_have_been_done: [], gaps: [], next_step: 'n', next_step_rationale: 'r', post_resolution_analysis: null, confidence_score: 0.8, needs_human_review: false, human_review_reasons: [], ambiguities_for_opus: [], itglue_docs_referenced: [], }, pre: { header: { ticket_number: 'T20260424.0045', autotask_ticket_id: 680282, title: 't', status_label: null, priority_label: null, queue: null, account_name: null, contact_name: null, contact_email: null, created_at: '2026-04-24T12:53:50.163Z', resolved_at: null, }, events: [], counts: { total_events: 0, customer_facing: 0, internal_only: 0, mixed: 0, filtered_noise: 0, }, content_hash: 'c'.repeat(64), }, meta: { haiku_used: true, sonnet_used: true, opus_used: false, total_input_tokens: 0, total_output_tokens: 0, total_cache_creation_tokens: 0, total_cache_read_tokens: 0, estimated_cost_usd: 0, cost_circuit_breaker_tripped: false, }, filtered_noise_count: 0, itglue_search_used: false, itglue_org_resolved: false, model_traces: {}, }; }) as never); await analyzerWorker.runJob('job_1', 'T20260424.0045', null); const stagesPassedToUpdate = (updateStatusSpy.mock.calls as Array<[string, string]>).map( (c) => c[1] ); expect(stagesPassedToUpdate).toEqual(['fetching', 'triaging', 'analyzing']); }); });