wulf-pulse/lib/services/analyzer/pipeline.test.ts

420 lines
13 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { runPipeline, _PIPELINE_INTERNALS } from './pipeline';
import * as persistence from './persistence';
import type { RawTicketBundle } from './preprocessor';
import type Anthropic from '@anthropic-ai/sdk';
import type { ITGlueSearchResult } from './itglue-search';
const FIXTURE = JSON.parse(
readFileSync(
resolve(__dirname, 'fixtures', 'T20260424.0045.input.json'),
'utf8'
)
) as RawTicketBundle;
let findExistingSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
findExistingSpy = vi
.spyOn(persistence, 'findExistingAnalysisByContentHash')
.mockResolvedValue(null);
});
afterEach(() => {
findExistingSpy.mockRestore();
vi.restoreAllMocks();
});
interface Reply {
text: string;
usage?: Partial<Anthropic.Usage>;
}
/**
* Sequential mock: each stage is a separate `messages.create` call. Queue the
* replies in order: Haiku Sonnet (Opus, optional).
*/
function makeFakeAnthropic(queue: Reply[]): {
fake: Anthropic;
bodies: any[];
} {
const bodies: any[] = [];
let i = 0;
const create = vi.fn(async (body: any) => {
bodies.push(body);
const next = queue[i++];
if (!next) throw new Error('No more queued LLM replies');
return {
id: `msg_${i}`,
type: 'message',
role: 'assistant',
content: [{ type: 'text', text: next.text }],
model: body.model,
stop_reason: 'end_turn',
stop_sequence: null,
usage: {
input_tokens: next.usage?.input_tokens ?? 5_000,
output_tokens: next.usage?.output_tokens ?? 500,
cache_creation_input_tokens: next.usage?.cache_creation_input_tokens ?? 0,
cache_read_input_tokens: next.usage?.cache_read_input_tokens ?? 0,
},
} as Anthropic.Message;
});
return {
fake: { messages: { create } } as unknown as Anthropic,
bodies,
};
}
const validTriage = (overrides: Record<string, unknown> = {}) =>
JSON.stringify({
ticket_type: 'service_request',
category: 'Vendor Integration',
entities: {
client_name: 'Seubert and Associates',
site_name: null,
devices: [],
users: ['Lorentz Hinrichsen'],
applications: ['AMS360'],
vendors: ['Vertafore'],
},
is_resolved: false,
status_matches_reality: false,
complexity_tier: 'medium',
complexity_reasons: ['vendor case opened after requestor pivoted'],
itglue_lookup_needed: false,
itglue_search_hints: [],
...overrides,
});
const validSonnet = (overrides: Record<string, unknown> = {}) =>
JSON.stringify({
summary: 'Lorentz pivoted; tech opened a Vertafore case anyway.',
timeline: [
{
timestamp: '2026-04-24T14:34:46.583Z',
actor: 'Lorentz Hinrichsen',
actor_type: 'wulf_tech',
source: 'ticket_note',
visibility: 'customer_facing',
action: 'Said no further outreach to Vertafore was needed.',
},
],
what_was_done: ['Opened a case with Vertafore on 04/24'],
what_should_have_been_done: ['Confirmed with requestor before opening case'],
gaps: [
{
description: 'Work continued after requestor said to stop.',
severity: 'high',
evidence_timestamps: ['2026-04-27T00:00:00.000Z'],
},
],
next_step: 'Confirm with requestor whether Vertafore endpoint info is still useful.',
next_step_rationale: 'They effectively closed the loop on 04/24.',
post_resolution_analysis: null,
confidence_score: 0.7,
needs_human_review: false,
human_review_reasons: [],
ambiguities_for_opus: [],
itglue_docs_referenced: [],
...overrides,
});
const validOpus = (overrides: Record<string, unknown> = {}) =>
JSON.stringify({
opus_notes: 'Reviewed Sonnet analysis; agree on the high-severity gap.',
updates: {},
...overrides,
});
describe('runPipeline', () => {
it('short-circuits when an existing analysis with the same content hash exists', async () => {
findExistingSpy.mockResolvedValueOnce({
id: 'existing-uuid',
analysis_version: 3,
});
const { fake } = makeFakeAnthropic([]);
const result = await runPipeline(
{ bundle: FIXTURE },
{ anthropic: fake }
);
expect(result.outcome).toBe('idempotent_short_circuit');
if (result.outcome === 'idempotent_short_circuit') {
expect(result.existing_analysis_id).toBe('existing-uuid');
expect(result.existing_analysis_version).toBe(3);
}
});
it('does NOT short-circuit when force=true', async () => {
findExistingSpy.mockResolvedValueOnce({
id: 'existing-uuid',
analysis_version: 3,
});
const { fake } = makeFakeAnthropic([
{ text: validTriage({ status_matches_reality: true }) },
{ text: validSonnet() },
]);
const result = await runPipeline(
{ bundle: FIXTURE, force: true },
{ anthropic: fake }
);
expect(result.outcome).toBe('complete');
});
it('runs Stage 1 → Stage 3 happy path with no Opus when nothing triggers it', async () => {
const { fake, bodies } = makeFakeAnthropic([
{ text: validTriage({ status_matches_reality: true, complexity_tier: 'low' }) },
{
text: validSonnet({
confidence_score: 0.85,
ambiguities_for_opus: [],
}),
},
]);
const result = await runPipeline(
{ bundle: FIXTURE },
{ anthropic: fake }
);
expect(result.outcome).toBe('complete');
if (result.outcome !== 'complete') return;
expect(bodies).toHaveLength(2);
expect(bodies[0].model).toBe('claude-haiku-4-5');
expect(bodies[1].model).toBe('claude-sonnet-4-6');
expect(result.meta.haiku_used).toBe(true);
expect(result.meta.sonnet_used).toBe(true);
expect(result.meta.opus_used).toBe(false);
expect(result.analysis.confidence_score).toBe(0.85);
});
it('runs Opus when triage.status_matches_reality=false', async () => {
const { fake, bodies } = makeFakeAnthropic([
{ text: validTriage({ status_matches_reality: false }) },
{ text: validSonnet({ confidence_score: 0.9 }) },
{ text: validOpus() },
]);
const result = await runPipeline(
{ bundle: FIXTURE },
{ anthropic: fake }
);
expect(result.outcome).toBe('complete');
if (result.outcome !== 'complete') return;
expect(bodies.map((b) => b.model)).toEqual([
'claude-haiku-4-5',
'claude-sonnet-4-6',
'claude-opus-4-7',
]);
expect(result.meta.opus_used).toBe(true);
});
it('applies Opus updates over the Sonnet result', async () => {
const { fake } = makeFakeAnthropic([
{ text: validTriage({ complexity_tier: 'high' }) },
{
text: validSonnet({
next_step: 'sonnet step',
confidence_score: 0.4,
}),
},
{
text: validOpus({
updates: {
next_step: 'opus step',
confidence_score: 0.85,
needs_human_review: true,
human_review_reasons: ['opus flagged for review'],
},
}),
},
]);
const result = await runPipeline({ bundle: FIXTURE }, { anthropic: fake });
expect(result.outcome).toBe('complete');
if (result.outcome !== 'complete') return;
expect(result.analysis.next_step).toBe('opus step');
expect(result.analysis.confidence_score).toBe(0.85);
expect(result.analysis.needs_human_review).toBe(true);
expect(result.analysis.human_review_reasons).toEqual(['opus flagged for review']);
});
it('respects forceSkipOpus even when triggers fire', async () => {
const { fake, bodies } = makeFakeAnthropic([
{ text: validTriage({ complexity_tier: 'high' }) },
{ text: validSonnet({ ambiguities_for_opus: ['why?'] }) },
]);
const result = await runPipeline(
{ bundle: FIXTURE, forceSkipOpus: true },
{ anthropic: fake }
);
expect(result.outcome).toBe('complete');
if (result.outcome !== 'complete') return;
expect(result.meta.opus_used).toBe(false);
expect(bodies.map((b) => b.model)).toEqual([
'claude-haiku-4-5',
'claude-sonnet-4-6',
]);
});
it('trips the cost circuit breaker before Opus when running cost ≥ ceiling', async () => {
const ceiling = _PIPELINE_INTERNALS.COST_CEILING_USD;
// Force the running cost above the ceiling by inflating Sonnet input/output.
// Sonnet pricing: $3/1M input, $15/1M output. Spend 700M tokens on output
// — well over $2 — to deterministically trip the breaker.
const { fake, bodies } = makeFakeAnthropic([
{ text: validTriage({ complexity_tier: 'high' }) },
{
text: validSonnet({ ambiguities_for_opus: ['why?'] }),
usage: { input_tokens: 1, output_tokens: 700_000_000 },
},
]);
const result = await runPipeline({ bundle: FIXTURE }, { anthropic: fake });
expect(result.outcome).toBe('complete');
if (result.outcome !== 'complete') return;
expect(result.meta.opus_used).toBe(false);
expect(result.meta.cost_circuit_breaker_tripped).toBe(true);
expect(result.analysis.needs_human_review).toBe(true);
expect(result.analysis.human_review_reasons.some((r) => r.includes('cost ceiling'))).toBe(true);
expect(bodies.map((b) => b.model)).toEqual([
'claude-haiku-4-5',
'claude-sonnet-4-6',
]);
expect(result.meta.estimated_cost_usd).toBeGreaterThanOrEqual(ceiling);
});
it('runs IT Glue search when triage requests it AND succeeds', async () => {
const { fake } = makeFakeAnthropic([
{
text: validTriage({
itglue_lookup_needed: true,
itglue_search_hints: ['AMS360', 'VSSO'],
status_matches_reality: true,
}),
},
{ text: validSonnet({ confidence_score: 0.85 }) },
]);
const fakeItglue = vi.fn().mockResolvedValue({
org_id: '42',
org_name: 'Seubert and Associates',
docs: [
{
id: 'doc1',
name: 'AMS360 Server',
doc_type: 'configuration',
organization_id: '42',
organization_name: 'Seubert and Associates',
snippet: 'sanitized snippet',
url: null,
updated_at: '2026-04-01T00:00:00.000Z',
},
],
alias_used: false,
} satisfies ITGlueSearchResult);
const result = await runPipeline(
{ bundle: FIXTURE },
{ anthropic: fake, itglueSearch: fakeItglue }
);
expect(fakeItglue).toHaveBeenCalledTimes(1);
expect(fakeItglue).toHaveBeenCalledWith({
org_name: 'Seubert and Associates',
hints: ['AMS360', 'VSSO'],
});
expect(result.outcome).toBe('complete');
if (result.outcome !== 'complete') return;
expect(result.itglue_search_used).toBe(true);
expect(result.itglue_org_resolved).toBe(true);
expect(result.model_traces.itglue?.doc_count).toBe(1);
});
it('continues without IT Glue context when search throws', async () => {
const { fake } = makeFakeAnthropic([
{
text: validTriage({
itglue_lookup_needed: true,
itglue_search_hints: ['AMS360'],
status_matches_reality: true,
}),
},
{ text: validSonnet({ confidence_score: 0.85 }) },
]);
const fakeItglue = vi.fn().mockRejectedValue(new Error('IT Glue 502'));
const result = await runPipeline(
{ bundle: FIXTURE },
{ anthropic: fake, itglueSearch: fakeItglue }
);
expect(result.outcome).toBe('complete');
});
it('reports filtered_noise_count from preprocessor', async () => {
const { fake } = makeFakeAnthropic([
{ text: validTriage({ status_matches_reality: true, complexity_tier: 'low' }) },
{ text: validSonnet({ confidence_score: 0.85 }) },
]);
const result = await runPipeline({ bundle: FIXTURE }, { anthropic: fake });
expect(result.outcome).toBe('complete');
if (result.outcome !== 'complete') return;
// T20260424.0045 has 4 workflow-rule firings + 4 service-desk-notification rows = 8 noise.
expect(result.filtered_noise_count).toBe(8);
});
it('drives onStage callbacks in order', async () => {
const stages: string[] = [];
const { fake } = makeFakeAnthropic([
{ text: validTriage({ status_matches_reality: true, complexity_tier: 'low' }) },
{ text: validSonnet({ confidence_score: 0.85 }) },
]);
await runPipeline(
{ bundle: FIXTURE },
{ anthropic: fake },
{ onStage: (stage) => void stages.push(stage) }
);
expect(stages).toEqual(['fetching', 'triaging', 'analyzing']);
});
it('drives onStage callbacks including itglue and deep_review when applicable', async () => {
const stages: string[] = [];
const { fake } = makeFakeAnthropic([
{
text: validTriage({
itglue_lookup_needed: true,
itglue_search_hints: ['x'],
complexity_tier: 'high',
status_matches_reality: false,
}),
},
{ text: validSonnet({ ambiguities_for_opus: ['?'] }) },
{ text: validOpus() },
]);
const fakeItglue = vi.fn().mockResolvedValue({
org_id: '1',
org_name: 'X',
docs: [],
alias_used: false,
} satisfies ITGlueSearchResult);
await runPipeline(
{ bundle: FIXTURE },
{ anthropic: fake, itglueSearch: fakeItglue },
{ onStage: (stage) => void stages.push(stage) }
);
expect(stages).toEqual([
'fetching',
'triaging',
'itglue',
'analyzing',
'deep_review',
]);
});
});