|
| 1 | +import { test as baseTest } from './annotation-queue.fixture'; |
| 2 | + |
| 3 | +export interface ExplainTraceRef { |
| 4 | + id: string; |
| 5 | + name: string; |
| 6 | + errorType: string; |
| 7 | + /** Seeded trace duration in seconds; null means left unset (renders as Duration "NA"). */ |
| 8 | + durationSeconds: number | null; |
| 9 | + /** Seeded LLM-span cost; null means no cost-bearing span (renders as Cost "-"). */ |
| 10 | + cost: number | null; |
| 11 | + /** |
| 12 | + * Source of a case-insensitive RegExp expected to appear in Ollie's error |
| 13 | + * explanation for this trace. Ollie's phrasing is non-deterministic (see |
| 14 | + * ollie-explain.spec.ts), so this is anchored to concrete details of the |
| 15 | + * seeded error (exception type / message) rather than generic |
| 16 | + * error-adjacent vocabulary ("error", "fail", ...) — those turned out to be |
| 17 | + * skippable depending on phrasing (e.g. a rate-limit explanation that never |
| 18 | + * says "fail" or "exceed"), while the concrete subject (rate limit, |
| 19 | + * document store, timeout, permissions, faiss) reliably recurs because |
| 20 | + * Ollie is grounded in that seeded content. |
| 21 | + */ |
| 22 | + errorKeywordSource: string; |
| 23 | +} |
| 24 | + |
| 25 | +export interface ExplainTracesFixtures { |
| 26 | + explainTraces: ExplainTraceRef[]; |
| 27 | +} |
| 28 | + |
| 29 | +/** |
| 30 | + * Five traces shaped for the Ollie "Explain" cell feature: each has a distinct |
| 31 | + * trace-level error, and distinct duration/cost, with one trace leaving both |
| 32 | + * duration and cost unset so the explain button's N/A path (Duration "NA", |
| 33 | + * Cost "-") gets covered too — per `explainTargets.ts`, those cells stay |
| 34 | + * explainable even at N/A, unlike the error cell which vetoes on no error. |
| 35 | + */ |
| 36 | +const SEEDS: Array<{ |
| 37 | + suffix: string; |
| 38 | + errorType: string; |
| 39 | + errorMessage: string; |
| 40 | + /** RegExp source (case-insensitive) expected in Ollie's explanation of this error. */ |
| 41 | + errorKeyword: string; |
| 42 | + durationSeconds: number | null; |
| 43 | + cost: number | null; |
| 44 | +}> = [ |
| 45 | + { |
| 46 | + suffix: 'rate-limit', |
| 47 | + errorType: 'RuntimeError', |
| 48 | + errorMessage: 'Model returned status 429: rate limit exceeded', |
| 49 | + errorKeyword: 'rate.?limit', |
| 50 | + durationSeconds: 2, |
| 51 | + cost: 0.0005, |
| 52 | + }, |
| 53 | + { |
| 54 | + suffix: 'missing-context', |
| 55 | + errorType: 'ValueError', |
| 56 | + errorMessage: 'Missing required context: document store unavailable', |
| 57 | + errorKeyword: 'context|document store', |
| 58 | + durationSeconds: 15, |
| 59 | + cost: 0.02, |
| 60 | + }, |
| 61 | + { |
| 62 | + suffix: 'tool-timeout', |
| 63 | + errorType: 'TimeoutError', |
| 64 | + errorMessage: 'Tool call timed out after 30 s', |
| 65 | + errorKeyword: 'timeout', |
| 66 | + durationSeconds: 60, |
| 67 | + cost: 0.5, |
| 68 | + }, |
| 69 | + { |
| 70 | + suffix: 'auth-failure', |
| 71 | + errorType: 'PermissionError', |
| 72 | + errorMessage: 'API key does not have access to model claude-3-opus', |
| 73 | + errorKeyword: 'permission|access|api key', |
| 74 | + durationSeconds: 180, |
| 75 | + cost: 2.0, |
| 76 | + }, |
| 77 | + { |
| 78 | + suffix: 'quota-exceeded', |
| 79 | + errorType: 'ImportError', |
| 80 | + errorMessage: "Required dependency 'faiss' is not installed", |
| 81 | + errorKeyword: 'faiss|install|depend', |
| 82 | + durationSeconds: null, |
| 83 | + cost: null, |
| 84 | + }, |
| 85 | +]; |
| 86 | + |
| 87 | +export const test = baseTest.extend<ExplainTracesFixtures>({ |
| 88 | + explainTraces: async ({ sdkClient, project, testNamespace }, use, testInfo) => { |
| 89 | + const refs: ExplainTraceRef[] = []; |
| 90 | + for (const seed of SEEDS) { |
| 91 | + const name = `${testNamespace}-explain-${seed.suffix}`; |
| 92 | + const spans = |
| 93 | + seed.cost === null |
| 94 | + ? [] |
| 95 | + : [ |
| 96 | + { |
| 97 | + name: 'llm-call', |
| 98 | + type: 'llm' as const, |
| 99 | + model: 'gpt-4o', |
| 100 | + provider: 'openai', |
| 101 | + input: { prompt: 'seed prompt' }, |
| 102 | + output: { completion: 'seed completion' }, |
| 103 | + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, |
| 104 | + total_cost: seed.cost, |
| 105 | + }, |
| 106 | + ]; |
| 107 | + const created = await sdkClient.python.createNestedTrace({ |
| 108 | + project_name: project.name, |
| 109 | + name, |
| 110 | + input: { user: `trigger for ${seed.suffix}` }, |
| 111 | + tags: ['explain', seed.suffix], |
| 112 | + error_info: { |
| 113 | + exception_type: seed.errorType, |
| 114 | + message: seed.errorMessage, |
| 115 | + }, |
| 116 | + duration_seconds: seed.durationSeconds ?? undefined, |
| 117 | + spans, |
| 118 | + }); |
| 119 | + refs.push({ |
| 120 | + id: created.id, |
| 121 | + name: created.name, |
| 122 | + errorType: seed.errorType, |
| 123 | + durationSeconds: seed.durationSeconds, |
| 124 | + cost: seed.cost, |
| 125 | + errorKeywordSource: seed.errorKeyword, |
| 126 | + }); |
| 127 | + } |
| 128 | + |
| 129 | + await testInfo.attach('opik.explainTraces', { |
| 130 | + body: JSON.stringify(refs, null, 2), |
| 131 | + contentType: 'application/json', |
| 132 | + }); |
| 133 | + |
| 134 | + await use(refs); |
| 135 | + // No explicit teardown — the project fixture's deleteProject cascades. |
| 136 | + }, |
| 137 | +}); |
| 138 | + |
| 139 | +export { expect } from './annotation-queue.fixture'; |
0 commit comments