import { describe, expect, test, vi, beforeEach, afterEach } from "vitest"; // Mock the langfuse modules at the top level const mockStartObservation = vi.fn(); const mockPropagateAttributes = vi.fn(); const mockSpanEnd = vi.fn(); const mockGenerationEnd = vi.fn(); const mockGenerationUpdate = vi.fn(); const mockGuardSpanEnd = vi.fn(); const mockEventEnd = vi.fn(); const mockGeneration: any = { update: (...args: unknown[]) => { mockGenerationUpdate(...args); return mockGeneration; }, end: mockGenerationEnd, }; const mockGuardSpan: any = { end: mockGuardSpanEnd, }; const mockEventObs: any = { end: mockEventEnd, }; const mockUpdateTrace = vi.fn(); const mockRootSpan = { startObservation: vi.fn(), updateTrace: mockUpdateTrace, end: mockSpanEnd, }; // Default: route by observation name function setupDefaultStartObservation() { mockRootSpan.startObservation.mockImplementation((name: string) => { if (name === "guard-pipeline") return mockGuardSpan; if (name === "provider-attempt") return mockEventObs; return mockGeneration; // "llm-call" }); } vi.mock("@langfuse/tracing", () => ({ startObservation: (...args: unknown[]) => { mockStartObservation(...args); return mockRootSpan; }, propagateAttributes: async (attrs: unknown, fn: () => Promise) => { mockPropagateAttributes(attrs); await fn(); }, })); vi.mock("@/lib/logger", () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); let langfuseEnabled = true; vi.mock("@/lib/langfuse/index", () => ({ isLangfuseEnabled: () => langfuseEnabled, })); function createMockSession(overrides: Record = {}) { const startTime = (overrides.startTime as number) ?? Date.now() - 500; return { startTime, method: "POST", headers: new Headers({ "content-type": "application/json", "x-api-key": "test-mock-key-not-real", "user-agent": "claude-code/1.0", }), request: { message: { model: "claude-sonnet-4-20250514", messages: [{ role: "user", content: "Hello" }], stream: true, max_tokens: 4096, tools: [{ name: "tool1" }], }, model: "claude-sonnet-4-20250514", }, originalFormat: "claude", userAgent: "claude-code/1.0", sessionId: "sess_abc12345_def67890", provider: { id: 1, name: "anthropic-main", providerType: "claude", }, messageContext: { id: 42, user: { id: 7, name: "testuser" }, key: { name: "default-key" }, }, ttfbMs: 200, forwardStartTime: startTime + 5, forwardedRequestBody: null, getEndpoint: () => "/v1/messages", getRequestSequence: () => 3, getMessagesLength: () => 1, getCurrentModel: () => "claude-sonnet-4-20250514", getOriginalModel: () => "claude-sonnet-4-20250514", isModelRedirected: () => false, getProviderChain: () => [ { id: 1, name: "anthropic-main", providerType: "claude", reason: "initial_selection", timestamp: startTime + 2, }, ], getSpecialSettings: () => null, getCacheTtlResolved: () => null, getContext1mApplied: () => false, ...overrides, } as any; } describe("traceProxyRequest", () => { beforeEach(() => { vi.clearAllMocks(); langfuseEnabled = true; setupDefaultStartObservation(); }); test("should not trace when Langfuse is disabled", async () => { langfuseEnabled = false; const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); expect(mockStartObservation).not.toHaveBeenCalled(); }); test("should trace when Langfuse is enabled with actual bodies", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const responseBody = { content: "Hi there" }; await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers({ "content-type": "application/json" }), durationMs: 500, statusCode: 200, isStreaming: false, responseText: JSON.stringify(responseBody), }); // Root span should have actual request body as input (not summary) const rootCall = mockStartObservation.mock.calls[0]; expect(rootCall[0]).toBe("proxy-request"); // Input should be the actual request message (since forwardedRequestBody is null) expect(rootCall[1].input).toEqual( expect.objectContaining({ model: "claude-sonnet-4-20250514", messages: expect.any(Array), }) ); // Output should be actual response body expect(rootCall[1].output).toEqual(responseBody); // Should have level expect(rootCall[1].level).toBe("DEFAULT"); // Should have metadata with former summaries expect(rootCall[1].metadata).toEqual( expect.objectContaining({ endpoint: "/v1/messages", method: "POST", statusCode: 200, durationMs: 500, }) ); // Should have child observations const callNames = mockRootSpan.startObservation.mock.calls.map((c: unknown[]) => c[0]); expect(callNames).toContain("guard-pipeline"); expect(callNames).toContain("llm-call"); expect(mockSpanEnd).toHaveBeenCalledWith(expect.any(Date)); expect(mockGenerationEnd).toHaveBeenCalledWith(expect.any(Date)); }); test("should use actual request messages as generation input", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const session = createMockSession(); await traceProxyRequest({ session, responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, responseText: '{"content": "response"}', }); // Find the llm-call invocation const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall).toBeDefined(); expect(llmCall[1].input).toEqual(session.request.message); }); test("should use actual response body as generation output", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const responseBody = { content: [{ type: "text", text: "Hello!" }] }; await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, responseText: JSON.stringify(responseBody), }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[1].output).toEqual(responseBody); }); test("should pass raw headers without redaction", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers({ "x-api-key": "secret-mock" }), durationMs: 500, statusCode: 200, isStreaming: false, }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); const metadata = llmCall[1].metadata; expect(metadata.requestHeaders["x-api-key"]).toBe("test-mock-key-not-real"); expect(metadata.requestHeaders["content-type"]).toBe("application/json"); expect(metadata.responseHeaders["x-api-key"]).toBe("secret-mock"); }); test("should include provider name and model in tags", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); expect(mockPropagateAttributes).toHaveBeenCalledWith( expect.objectContaining({ userId: "testuser", sessionId: "sess_abc12345_def67890", tags: expect.arrayContaining([ "claude", "anthropic-main", "claude-sonnet-4-20250514", "2xx", ]), }) ); }); test("should include usage details when provided", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, usageMetrics: { input_tokens: 100, output_tokens: 50, cache_read_input_tokens: 20, }, costUsd: "0.0015", }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[1].usageDetails).toEqual({ input: 100, output: 50, cache_read_input_tokens: 20, }); expect(llmCall[1].costDetails).toEqual({ total: 0.0015, }); }); test("should include providerChain, specialSettings, and model in metadata", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const providerChain = [ { id: 1, name: "anthropic-main", providerType: "claude", reason: "initial_selection", timestamp: Date.now(), }, ]; await traceProxyRequest({ session: createMockSession({ getSpecialSettings: () => ({ maxThinking: 8192 }), getProviderChain: () => providerChain, }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); const metadata = llmCall[1].metadata; expect(metadata.providerChain).toEqual(providerChain); expect(metadata.specialSettings).toEqual({ maxThinking: 8192 }); expect(metadata.model).toBe("claude-sonnet-4-20250514"); expect(metadata.originalModel).toBe("claude-sonnet-4-20250514"); expect(metadata.providerName).toBe("anthropic-main"); expect(metadata.requestSummary).toEqual( expect.objectContaining({ model: "claude-sonnet-4-20250514", messageCount: 1, }) ); }); test("should handle model redirect metadata", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession({ isModelRedirected: () => true, getOriginalModel: () => "claude-sonnet-4-20250514", getCurrentModel: () => "glm-4", request: { message: { model: "glm-4", messages: [] }, model: "glm-4", }, }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[1].metadata.modelRedirected).toBe(true); expect(llmCall[1].metadata.originalModel).toBe("claude-sonnet-4-20250514"); }); test("should set completionStartTime from ttfbMs", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const startTime = Date.now() - 500; await traceProxyRequest({ session: createMockSession({ startTime, ttfbMs: 200 }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); expect(mockGenerationUpdate).toHaveBeenCalledWith({ completionStartTime: new Date(startTime + 200), }); }); test("should pass correct startTime and endTime to observations", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const startTime = 1700000000000; const durationMs = 5000; await traceProxyRequest({ session: createMockSession({ startTime, forwardStartTime: startTime + 5 }), responseHeaders: new Headers(), durationMs, statusCode: 200, isStreaming: false, }); const expectedStart = new Date(startTime); const expectedEnd = new Date(startTime + durationMs); const expectedForwardStart = new Date(startTime + 5); // Root span gets startTime in options (3rd arg) expect(mockStartObservation).toHaveBeenCalledWith("proxy-request", expect.any(Object), { startTime: expectedStart, }); // Generation gets forwardStartTime in options (3rd arg) const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[2]).toEqual({ asType: "generation", startTime: expectedForwardStart, }); // Both end() calls receive the computed endTime expect(mockGenerationEnd).toHaveBeenCalledWith(expectedEnd); expect(mockSpanEnd).toHaveBeenCalledWith(expectedEnd); }); test("should handle errors gracefully without throwing", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); // Make startObservation throw mockStartObservation.mockImplementationOnce(() => { throw new Error("SDK error"); }); await expect( traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }) ).resolves.toBeUndefined(); }); test("should include correct tags for error responses", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 502, isStreaming: false, errorMessage: "upstream error", }); expect(mockPropagateAttributes).toHaveBeenCalledWith( expect.objectContaining({ tags: expect.arrayContaining(["5xx"]), }) ); }); test("should pass large input/output without truncation", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); // Generate a large response text const largeContent = "x".repeat(200_000); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, responseText: largeContent, }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); const output = llmCall[1].output as string; // Should be the full content, no truncation expect(output).toBe(largeContent); expect(output).not.toContain("...[truncated]"); }); test("should show streaming output with sseEventCount when no responseText", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: true, sseEventCount: 42, }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[1].output).toEqual({ streaming: true, sseEventCount: 42, }); }); test("should include costUsd in root span metadata", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, costUsd: "0.05", }); const rootCall = mockStartObservation.mock.calls[0]; expect(rootCall[1].metadata).toEqual( expect.objectContaining({ costUsd: "0.05", }) ); }); test("should set trace-level input/output via updateTrace with actual bodies", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const responseBody = { result: "ok" }; await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, responseText: JSON.stringify(responseBody), costUsd: "0.05", }); expect(mockUpdateTrace).toHaveBeenCalledWith({ input: expect.objectContaining({ model: "claude-sonnet-4-20250514", messages: expect.any(Array), }), output: responseBody, }); }); // --- New tests for multi-span hierarchy --- test("should create guard-pipeline span with correct timing", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const startTime = 1700000000000; const forwardStartTime = startTime + 8; // 8ms guard pipeline await traceProxyRequest({ session: createMockSession({ startTime, forwardStartTime }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const guardCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "guard-pipeline" ); expect(guardCall).toBeDefined(); expect(guardCall[1]).toEqual({ output: { durationMs: 8, passed: true }, }); expect(guardCall[2]).toEqual({ startTime: new Date(startTime) }); // Guard span should end at forwardStartTime expect(mockGuardSpanEnd).toHaveBeenCalledWith(new Date(forwardStartTime)); }); test("should skip guard-pipeline span when forwardStartTime is null", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession({ forwardStartTime: null }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const guardCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "guard-pipeline" ); expect(guardCall).toBeUndefined(); expect(mockGuardSpanEnd).not.toHaveBeenCalled(); }); test("should create provider-attempt events for failed chain items", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const startTime = 1700000000000; const failTimestamp = startTime + 100; await traceProxyRequest({ session: createMockSession({ startTime, getProviderChain: () => [ { id: 1, name: "provider-a", providerType: "claude", reason: "retry_failed", errorMessage: "502 Bad Gateway", statusCode: 502, attemptNumber: 1, timestamp: failTimestamp, }, { id: 2, name: "provider-b", providerType: "claude", reason: "system_error", errorMessage: "ECONNREFUSED", timestamp: failTimestamp + 50, }, { id: 3, name: "provider-c", providerType: "claude", reason: "request_success", timestamp: failTimestamp + 200, }, ], }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const eventCalls = mockRootSpan.startObservation.mock.calls.filter( (c: unknown[]) => c[0] === "provider-attempt" ); // 2 failed items (retry_failed + system_error), success is skipped expect(eventCalls).toHaveLength(2); // First event: retry_failed -> WARNING level expect(eventCalls[0][1]).toEqual( expect.objectContaining({ level: "WARNING", input: expect.objectContaining({ providerId: 1, providerName: "provider-a", attempt: 1, }), output: expect.objectContaining({ reason: "retry_failed", errorMessage: "502 Bad Gateway", statusCode: 502, }), }) ); expect(eventCalls[0][2]).toEqual({ asType: "event", startTime: new Date(failTimestamp), }); // Second event: system_error -> ERROR level expect(eventCalls[1][1].level).toBe("ERROR"); expect(eventCalls[1][1].output.reason).toBe("system_error"); }); test("should set generation startTime to forwardStartTime", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const startTime = 1700000000000; const forwardStartTime = startTime + 10; await traceProxyRequest({ session: createMockSession({ startTime, forwardStartTime }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[2]).toEqual({ asType: "generation", startTime: new Date(forwardStartTime), }); }); test("should fall back to requestStartTime when forwardStartTime is null", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const startTime = 1700000000000; await traceProxyRequest({ session: createMockSession({ startTime, forwardStartTime: null }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[2]).toEqual({ asType: "generation", startTime: new Date(startTime), }); }); test("should include timingBreakdown in root span metadata and generation metadata", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const startTime = 1700000000000; const forwardStartTime = startTime + 5; await traceProxyRequest({ session: createMockSession({ startTime, forwardStartTime, ttfbMs: 105, getProviderChain: () => [ { id: 1, name: "p1", reason: "retry_failed", timestamp: startTime + 50 }, { id: 2, name: "p2", reason: "request_success", timestamp: startTime + 100 }, ], }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const expectedTimingBreakdown = { guardPipelineMs: 5, upstreamTotalMs: 495, ttfbFromForwardMs: 100, // ttfbMs(105) - guardPipelineMs(5) tokenGenerationMs: 395, // durationMs(500) - ttfbMs(105) failedAttempts: 1, // only retry_failed is non-success providersAttempted: 2, // 2 unique provider ids }; // Root span metadata should have timingBreakdown const rootCall = mockStartObservation.mock.calls[0]; expect(rootCall[1].metadata.timingBreakdown).toEqual(expectedTimingBreakdown); // Generation metadata should also have timingBreakdown const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[1].metadata.timingBreakdown).toEqual(expectedTimingBreakdown); }); test("should not create provider-attempt events when all providers succeeded", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession({ getProviderChain: () => [ { id: 1, name: "p1", reason: "initial_selection", timestamp: Date.now() }, { id: 1, name: "p1", reason: "request_success", timestamp: Date.now() }, ], }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const eventCalls = mockRootSpan.startObservation.mock.calls.filter( (c: unknown[]) => c[0] === "provider-attempt" ); expect(eventCalls).toHaveLength(0); }); // --- New tests for input/output, level, and cost breakdown --- test("should use forwardedRequestBody as trace input when available", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const forwardedBody = JSON.stringify({ model: "claude-sonnet-4-20250514", messages: [{ role: "user", content: "Preprocessed Hello" }], stream: true, }); await traceProxyRequest({ session: createMockSession({ forwardedRequestBody: forwardedBody, }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, responseText: '{"ok": true}', }); // Root span input should be the forwarded body (parsed JSON) const rootCall = mockStartObservation.mock.calls[0]; expect(rootCall[1].input).toEqual(JSON.parse(forwardedBody)); // updateTrace should also use forwarded body expect(mockUpdateTrace).toHaveBeenCalledWith({ input: JSON.parse(forwardedBody), output: { ok: true }, }); }); test("should set root span level to DEFAULT for successful request", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const rootCall = mockStartObservation.mock.calls[0]; expect(rootCall[1].level).toBe("DEFAULT"); }); test("should set root span level to WARNING when retries occurred", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const startTime = Date.now() - 500; await traceProxyRequest({ session: createMockSession({ startTime, getProviderChain: () => [ { id: 1, name: "p1", reason: "retry_failed", timestamp: startTime + 50 }, { id: 2, name: "p2", reason: "request_success", timestamp: startTime + 200 }, ], }), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, }); const rootCall = mockStartObservation.mock.calls[0]; expect(rootCall[1].level).toBe("WARNING"); }); test("should set root span level to ERROR for non-200 status", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 502, isStreaming: false, }); const rootCall = mockStartObservation.mock.calls[0]; expect(rootCall[1].level).toBe("ERROR"); }); test("should set root span level to ERROR for 499 client abort", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 499, isStreaming: false, }); const rootCall = mockStartObservation.mock.calls[0]; expect(rootCall[1].level).toBe("ERROR"); }); test("should include cost breakdown in costDetails when provided", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); const costBreakdown = { input: 0.001, output: 0.002, cache_creation: 0.0005, cache_read: 0.0001, total: 0.0036, }; await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, costUsd: "0.0036", costBreakdown, }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[1].costDetails).toEqual(costBreakdown); }); test("should fallback to total-only costDetails when no breakdown", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, costUsd: "0.05", }); const llmCall = mockRootSpan.startObservation.mock.calls.find( (c: unknown[]) => c[0] === "llm-call" ); expect(llmCall[1].costDetails).toEqual({ total: 0.05 }); }); test("should include former summaries in root span metadata", async () => { const { traceProxyRequest } = await import("@/lib/langfuse/trace-proxy-request"); await traceProxyRequest({ session: createMockSession(), responseHeaders: new Headers(), durationMs: 500, statusCode: 200, isStreaming: false, costUsd: "0.05", }); const rootCall = mockStartObservation.mock.calls[0]; const metadata = rootCall[1].metadata; // Former input summary fields expect(metadata.endpoint).toBe("/v1/messages"); expect(metadata.method).toBe("POST"); expect(metadata.model).toBe("claude-sonnet-4-20250514"); expect(metadata.clientFormat).toBe("claude"); expect(metadata.providerName).toBe("anthropic-main"); // Former output summary fields expect(metadata.statusCode).toBe(200); expect(metadata.durationMs).toBe(500); expect(metadata.costUsd).toBe("0.05"); expect(metadata.timingBreakdown).toBeDefined(); }); }); describe("isLangfuseEnabled", () => { const originalPublicKey = process.env.LANGFUSE_PUBLIC_KEY; const originalSecretKey = process.env.LANGFUSE_SECRET_KEY; afterEach(() => { // Restore env if (originalPublicKey !== undefined) { process.env.LANGFUSE_PUBLIC_KEY = originalPublicKey; } else { delete process.env.LANGFUSE_PUBLIC_KEY; } if (originalSecretKey !== undefined) { process.env.LANGFUSE_SECRET_KEY = originalSecretKey; } else { delete process.env.LANGFUSE_SECRET_KEY; } }); test("should return false when env vars are not set", () => { delete process.env.LANGFUSE_PUBLIC_KEY; delete process.env.LANGFUSE_SECRET_KEY; // Direct function test (not using the mock) const isEnabled = !!(process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY); expect(isEnabled).toBe(false); }); test("should return true when both keys are set", () => { process.env.LANGFUSE_PUBLIC_KEY = "pk-lf-test-mock"; process.env.LANGFUSE_SECRET_KEY = "test-mock-not-real"; const isEnabled = !!(process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY); expect(isEnabled).toBe(true); }); });