mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 10:12:11 +03:00
Every agentic chat request now gets a conversation id (X-ConversationId response header), and OmniRoute detects when a follow-up request continues the same conversation via fingerprint + bounded prefix-hash matching, with a strict-growth invariant to prevent false merges between independent single-shot requests. Dashboard changes: - /dashboard/logs: toggleable Conversation column - /dashboard/logs/timeline: same-conversation requests share a timeline lane, connected by an arrow, with a configurable lane-reuse window - Request detail panel: new "Full Conversation" transcript above the raw SSE event stream, with Markdown rendering, per-turn timestamps, turn-relative view (only turns up to the one you opened, with a jump-to-next link), click-any-turn-to-open-its-log navigation, and live auto-refresh (with an auto-follow toggle, matching the event stream's autoscroll pattern) that rebuilds the transcript in real time from the in-flight SSE chunk buffer while a request is still streaming - New /dashboard/conversations page listing only conversations with 2+ turns - Configurable auto-refresh intervals on both the timeline and conversations list pages Also fixes a pre-existing bug where the timeline view never showed SSE/ stream-chunk events or respected email-masking, because RequestTimeline.tsx hardcoded debugEnabled/emailsVisible instead of reading the same server-side/store state RequestLoggerV2.tsx already used, and makes the request detail panel and conversations list responsive on mobile.
127 lines
4.6 KiB
TypeScript
127 lines
4.6 KiB
TypeScript
/**
|
|
* Unit tests for src/mitm/inspector/multiRowConversation.ts — the delta
|
|
* algorithm that reconstructs a chronological, per-row-tagged transcript
|
|
* across every call_logs row of one agentic conversation.
|
|
*/
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { buildMultiRowConversation } from "../../src/mitm/inspector/multiRowConversation.ts";
|
|
|
|
test("buildMultiRowConversation: 3-row conversation with a tool call in the middle", () => {
|
|
const rows = [
|
|
{
|
|
id: "A",
|
|
timestamp: "2026-01-01T10:00:00.000Z",
|
|
requestBody: { messages: [{ role: "user", content: "user1" }] },
|
|
responseBody: { choices: [{ message: { role: "assistant", content: "assistant1" } }] },
|
|
},
|
|
{
|
|
id: "B",
|
|
timestamp: "2026-01-01T10:01:00.000Z",
|
|
requestBody: {
|
|
messages: [
|
|
{ role: "user", content: "user1" },
|
|
{ role: "assistant", content: "assistant1" },
|
|
{ role: "tool", content: "tool_result1" },
|
|
{ role: "user", content: "user2" },
|
|
],
|
|
},
|
|
responseBody: { choices: [{ message: { role: "assistant", content: "assistant2" } }] },
|
|
},
|
|
{
|
|
id: "C",
|
|
timestamp: "2026-01-01T10:02:00.000Z",
|
|
requestBody: {
|
|
messages: [
|
|
{ role: "user", content: "user1" },
|
|
{ role: "assistant", content: "assistant1" },
|
|
{ role: "tool", content: "tool_result1" },
|
|
{ role: "user", content: "user2" },
|
|
{ role: "assistant", content: "assistant2" },
|
|
],
|
|
},
|
|
responseBody: { choices: [{ message: { role: "assistant", content: "assistant3" } }] },
|
|
},
|
|
];
|
|
|
|
const turns = buildMultiRowConversation(rows);
|
|
|
|
// Row A contributes: user1, assistant1
|
|
// Row B contributes: tool_result1, user2, assistant2 (new request turns + its own response)
|
|
// Row C contributes: assistant3 only (no new request turns — request already
|
|
// equals the running total after row B)
|
|
assert.deepEqual(
|
|
turns.map((t) => ({ sourceCallLogId: t.sourceCallLogId, role: t.role })),
|
|
[
|
|
{ sourceCallLogId: "A", role: "user" },
|
|
{ sourceCallLogId: "A", role: "assistant" },
|
|
{ sourceCallLogId: "B", role: "tool" },
|
|
{ sourceCallLogId: "B", role: "user" },
|
|
{ sourceCallLogId: "B", role: "assistant" },
|
|
{ sourceCallLogId: "C", role: "assistant" },
|
|
]
|
|
);
|
|
|
|
// Every turn carries the ISO timestamp of its own originating row.
|
|
assert.equal(turns[0].timestamp, "2026-01-01T10:00:00.000Z");
|
|
assert.equal(turns[2].timestamp, "2026-01-01T10:01:00.000Z");
|
|
assert.equal(turns[5].timestamp, "2026-01-01T10:02:00.000Z");
|
|
});
|
|
|
|
test("buildMultiRowConversation: single-row conversation", () => {
|
|
const rows = [
|
|
{
|
|
id: "solo",
|
|
timestamp: "2026-01-01T10:00:00.000Z",
|
|
requestBody: { messages: [{ role: "user", content: "hi" }] },
|
|
responseBody: { choices: [{ message: { role: "assistant", content: "hello" } }] },
|
|
},
|
|
];
|
|
|
|
const turns = buildMultiRowConversation(rows);
|
|
assert.equal(turns.length, 2);
|
|
assert.equal(turns[0].role, "user");
|
|
assert.equal(turns[0].sourceCallLogId, "solo");
|
|
assert.equal(turns[1].role, "assistant");
|
|
assert.equal(turns[1].sourceCallLogId, "solo");
|
|
});
|
|
|
|
test("buildMultiRowConversation: a later row that is NOT a superset (malformed/adversarial) clamps instead of throwing", () => {
|
|
const rows = [
|
|
{
|
|
id: "A",
|
|
timestamp: "2026-01-01T10:00:00.000Z",
|
|
requestBody: {
|
|
messages: [
|
|
{ role: "user", content: "user1" },
|
|
{ role: "assistant", content: "assistant1" },
|
|
{ role: "user", content: "user2" },
|
|
],
|
|
},
|
|
responseBody: { choices: [{ message: { role: "assistant", content: "assistant2" } }] },
|
|
},
|
|
{
|
|
// Shorter request than row A's total (4) despite sharing the same
|
|
// conversation id somehow — should never happen given
|
|
// resolveConversationId's strict-growth guarantee, but must not throw
|
|
// or produce a negative-length slice.
|
|
id: "B",
|
|
timestamp: "2026-01-01T10:01:00.000Z",
|
|
requestBody: { messages: [{ role: "user", content: "user1" }] },
|
|
responseBody: { choices: [{ message: { role: "assistant", content: "assistant3" } }] },
|
|
},
|
|
];
|
|
|
|
assert.doesNotThrow(() => buildMultiRowConversation(rows));
|
|
const turns = buildMultiRowConversation(rows);
|
|
// Row B's own response is still included; it just contributes no new
|
|
// request turns since it's shorter than what's already been seen.
|
|
assert.ok(turns.some((t) => t.sourceCallLogId === "B"));
|
|
});
|
|
|
|
test("buildMultiRowConversation: empty rows array returns an empty transcript", () => {
|
|
assert.deepEqual(buildMultiRowConversation([]), []);
|
|
});
|