/** * Tests for ConversationTab separators — CONTEXT HISTORY / MODEL RESPONSE * Validates the rendering logic: both sections appear when both request/response have turns; * only CONTEXT HISTORY appears when response is empty. */ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { normalizeConversation } from "../../../src/mitm/inspector/conversationNormalizer.ts"; import type { InterceptedRequest } from "../../../src/mitm/inspector/types.ts"; function makeRequest(overrides: Partial = {}): InterceptedRequest { return { id: "test-id", source: "agent-bridge", timestamp: new Date().toISOString(), method: "POST", host: "api.openai.com", path: "/v1/chat/completions", requestHeaders: { "content-type": "application/json" }, requestBody: null, requestSize: 0, responseHeaders: {}, responseBody: null, responseSize: 0, status: 200, detectedKind: "llm", ...overrides, }; } describe("ConversationTab separators rendering logic", () => { it("shows CONTEXT HISTORY section when request has turns", () => { const reqBody = JSON.stringify({ model: "gpt-4o", messages: [ { role: "system", content: "You are helpful." }, { role: "user", content: "Hello!" }, ], }); const req = makeRequest({ requestBody: reqBody, responseBody: null }); const result = normalizeConversation(req); if (result !== null) { assert.ok(result.request.length > 0, "request section should have turns"); // Context History separator should be rendered (guarded by request.length > 0) const shouldRenderContextHistory = result.request.length > 0; assert.equal(shouldRenderContextHistory, true); } }); it("shows MODEL RESPONSE section when response has turns", () => { const reqBody = JSON.stringify({ model: "gpt-4o", messages: [{ role: "user", content: "Hi" }], }); const resBody = JSON.stringify({ choices: [ { message: { role: "assistant", content: "Hello! How can I help?" }, finish_reason: "stop", }, ], usage: { prompt_tokens: 5, completion_tokens: 8 }, }); const req = makeRequest({ requestBody: reqBody, responseBody: resBody }); const result = normalizeConversation(req); if (result !== null) { // Model Response separator should be rendered (guarded by response.length > 0) const shouldRenderModelResponse = result.response.length > 0; assert.equal(shouldRenderModelResponse, true); } }); it("does NOT render MODEL RESPONSE when response is empty", () => { const reqBody = JSON.stringify({ model: "gpt-4o", messages: [{ role: "user", content: "Hi" }], }); // No response body const req = makeRequest({ requestBody: reqBody, responseBody: null }); const result = normalizeConversation(req); if (result !== null) { // Response section should be empty, so MODEL RESPONSE separator should NOT render const shouldRenderModelResponse = result.response.length > 0; assert.equal(shouldRenderModelResponse, false); // But context history should still render const shouldRenderContextHistory = result.request.length > 0; assert.equal(shouldRenderContextHistory, true); } }); it("renders both separators when both request and response have turns", () => { const reqBody = JSON.stringify({ model: "gpt-4o", messages: [{ role: "user", content: "Hi" }], }); const resBody = JSON.stringify({ choices: [ { message: { role: "assistant", content: "Hello!" }, finish_reason: "stop", }, ], }); const req = makeRequest({ requestBody: reqBody, responseBody: resBody }); const result = normalizeConversation(req); if (result !== null) { const contextHistoryVisible = result.request.length > 0; const modelResponseVisible = result.response.length > 0; assert.equal(contextHistoryVisible, true, "CONTEXT HISTORY should be visible"); assert.equal(modelResponseVisible, true, "MODEL RESPONSE should be visible"); } }); it("request and response turns are keyed separately (req-N vs res-N)", () => { // Keys used: `req-${i}` for request turns, `res-${i}` for response turns const reqKeys = ["req-0", "req-1", "req-2"]; const resKeys = ["res-0", "res-1"]; // Verify no overlap const allKeys = [...reqKeys, ...resKeys]; const uniqueKeys = new Set(allKeys); assert.equal(uniqueKeys.size, allKeys.length, "all keys should be unique"); }); it("allTurns still accounts for correct total across both sections", () => { const request = [ { role: "user" as const, content: "Hi", contentType: "text" as const }, ]; const response = [ { role: "assistant" as const, content: "Hello!", contentType: "text" as const }, ]; // Before: allTurns = [...request, ...response] // After: both rendered in separate sections const totalTurns = request.length + response.length; assert.equal(totalTurns, 2); }); it("conversationNotAvailable key resolves when body is null (normalizeConversation returns null)", () => { // When requestBody is null and responseBody is null, normalizeConversation returns null. // The ConversationTab renders t("conversationNotAvailable") in that case. const req = makeRequest({ requestBody: null, responseBody: null }); const result = normalizeConversation(req); // Must return null so the component falls through to the conversationNotAvailable branch. assert.equal(result, null, "normalizeConversation must return null for non-LLM / null body"); }); });