Skip to content

Commit 57fe60a

Browse files
committed
test(validation): add integration test for full validation pipeline
Tests extractJson + ValidatedLlmInvoker + schema primitives together: messy LLM output handling, JSONL batch validation, retry with feedback, exhausted retries error with history, all real-world output patterns.
1 parent 486e01d commit 57fe60a

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Full pipeline integration test for the LLM output validation layer.
3+
* Exercises extractJson + ValidatedLlmInvoker + schema primitives together.
4+
*/
5+
6+
import { describe, it, expect, vi } from 'vitest';
7+
import { z } from 'zod';
8+
import { createValidatedInvoker } from '../ValidatedLlmInvoker.js';
9+
import { extractJson } from '../extractJson.js';
10+
import { LlmOutputValidationError } from '../errors.js';
11+
import { ReflectionTraceOutput, ObservationNoteOutput } from '../schema-primitives.js';
12+
13+
describe('Validation Layer — integration', () => {
14+
it('full pipeline: messy LLM output → extractJson → Zod validation → typed result', async () => {
15+
const invoker = vi.fn().mockResolvedValue(
16+
'<thinking>Let me analyze this.</thinking>\n' +
17+
'```json\n{"type":"semantic","scope":"user","content":"User is an engineer","confidence":0.95}\n```'
18+
);
19+
20+
const validated = createValidatedInvoker(invoker, ReflectionTraceOutput);
21+
const result = await validated('system', 'user');
22+
23+
expect(result.type).toBe('semantic');
24+
expect(result.content).toBe('User is an engineer');
25+
expect(result.confidence).toBe(0.95);
26+
expect(result.entities).toEqual([]);
27+
expect(result.sourceType).toBe('reflection');
28+
});
29+
30+
it('JSONL batch: multiple observation notes validated as array', async () => {
31+
const invoker = vi.fn().mockResolvedValue(
32+
'{"type":"factual","content":"User is an engineer","importance":0.9,"entities":["user"]}\n' +
33+
'{"type":"commitment","content":"Check back Friday","importance":0.8,"entities":[]}\n' +
34+
'{"type":"emotional","content":"Feeling stressed","importance":0.7,"entities":["user"]}'
35+
);
36+
37+
const BatchSchema = z.array(ObservationNoteOutput);
38+
const validated = createValidatedInvoker(invoker, BatchSchema);
39+
const results = await validated('system', 'user');
40+
41+
expect(results).toHaveLength(3);
42+
expect(results[0].type).toBe('factual');
43+
expect(results[1].type).toBe('commitment');
44+
expect(results[2].type).toBe('emotional');
45+
});
46+
47+
it('retry succeeds after initial malformed output', async () => {
48+
const invoker = vi.fn()
49+
.mockResolvedValueOnce('I apologize, here are the results... {broken json')
50+
.mockResolvedValueOnce('{"type":"factual","content":"A fact","importance":0.5,"entities":[]}');
51+
52+
const validated = createValidatedInvoker(invoker, ObservationNoteOutput, { maxRetries: 1 });
53+
const result = await validated('system', 'user');
54+
55+
expect(result.type).toBe('factual');
56+
expect(result.content).toBe('A fact');
57+
});
58+
59+
it('exhausted retries produce descriptive error with history', async () => {
60+
const invoker = vi.fn().mockResolvedValue('The answer is 42.');
61+
const validated = createValidatedInvoker(invoker, ObservationNoteOutput, { maxRetries: 2 });
62+
63+
try {
64+
await validated('system', 'user');
65+
expect.unreachable();
66+
} catch (err) {
67+
expect(err).toBeInstanceOf(LlmOutputValidationError);
68+
const ve = err as LlmOutputValidationError;
69+
expect(ve.retryCount).toBe(2);
70+
expect(ve.retryHistory).toHaveLength(3);
71+
expect(ve.rawOutput).toBe('The answer is 42.');
72+
}
73+
});
74+
75+
it('extractJson handles all real-world LLM output patterns', () => {
76+
expect(extractJson('{"a":1}')).toBe('{"a":1}');
77+
expect(extractJson('```json\n{"a":1}\n```')).toBe('{"a":1}');
78+
expect(extractJson('<thinking>hmm</thinking>{"a":1}')).toBe('{"a":1}');
79+
expect(extractJson('Result: {"a":1} done')).toBe('{"a":1}');
80+
81+
const jsonl = extractJson('{"a":1}\n{"b":2}');
82+
expect(JSON.parse(jsonl!)).toEqual([{ a: 1 }, { b: 2 }]);
83+
84+
expect(extractJson('just text')).toBeNull();
85+
});
86+
});

0 commit comments

Comments
 (0)