152 lines
5.6 KiB
TypeScript
152 lines
5.6 KiB
TypeScript
/**
|
|
* 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> = {}): 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");
|
|
});
|
|
});
|