mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 13:42:09 +03:00
* feat(responses): virtualize previous_response_id continuation regardless of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (#10121). * feat(dashboard): agentic conversation tracking with live transcript view Every agentic chat request now gets a conversation id (X-ConversationId response header). 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 that happen to share identical opening content. Continuation detection excludes the system message from the identity anchor, since real coding-agent CLIs commonly regenerate it every request with live context (timestamp, cwd, git status) — without this, that volatility alone broke every continuation check against real traffic. - `/dashboard/logs`: new toggleable Conversation column. - `/dashboard/logs/timeline`: requests sharing a conversation id 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 — Markdown rendering, per-turn timestamps, turn-relative view, click-any-turn navigation, live auto-refresh building the transcript in real time from the in-flight SSE chunk buffer while a request is still streaming, auto-scroll-to-bottom as the live turn grows. - New `/dashboard/conversations` page listing conversations with 2+ turns, no-forking model (an edited/duplicated mid-history turn mints its own independent conversation instead of merging), pagination, duplicate- anchor fix. - Configurable auto-refresh intervals on both the timeline and conversations list pages. - Responses API tool-call gap fix: turnsFromOpenAiMessages only handled role-based Chat Completions messages, so bare {type:"function_call"} / {type:"function_call_output"} / {type:"reasoning"} items (real Responses API traffic) silently vanished from the Conversation Context panel. - truncateForLog now counts input[] (Responses API), not just messages[] (Chat Completions), so a truncated /v1/responses request still shows a placeholder instead of nothing. - RequestTimeline.tsx now reads the same debugEnabled/emailsVisible settings RequestLoggerV2.tsx already used, instead of hardcoding both false — the timeline view never showed SSE/stream-chunk events or respected email-masking, regardless of the actual setting. Migrations 147/148 (agentic_conversations, conversation_turn_nodes) — 135 and 136 are now taken upstream; 143-145 are documented KNOWN_GAPS, so this uses the next free slot past upstream's current highest. Test plan: - npm run typecheck:core — clean - npm run lint — clean - node --import tsx/esm scripts/check/check-migration-numbering.mjs — OK, 0 collisions - 109 unit tests across the conversation-tracking, migration-renumber, and dashboard-wiring surface — 0 failures * refactor(dashboard): reuse call-log artifacts for conversation transcript content conversation_turn_nodes no longer stores turn text/tool-call content (text_preview/block_kind/tool_name) -- it's identity-only now (id/parent/ content_hash), matching agentic_conversations' existing lightweight-index shape. Every node's originating request is already fully captured by the call-log pipeline artifact its last_correlation_id points at, so the /dashboard/conversations tree view resolves each node's actual display content on demand from there (open-sse/services/conversationTurnContent.ts), re-running the same extractCanonicalTurns/hashTurnContent the write path used and matching by content_hash, instead of duplicating conversation content into a second store under a separate retention/gating policy. This also drops the old 8000-char text_preview truncation entirely -- resolved content is always full and untruncated. The frontend contract is unchanged (tree API still returns {textPreview, blockKind, toolName} per node), so the dashboard UI itself (page.tsx, RequestLoggerDetail/RequestTimeline, sidebar, i18n) needed no changes. Renumbered the cherry-picked 147/148 migrations to 153/154 -- 147 now collides with 147_api_keys_model_access_mode.sql, which landed on release/v3.8.50 after this work was originally built. Also includes a standalone, unrelated fix carried along from this rebase: close isProviderModelHidden's missing function-body brace in modelSelectModalHelpers.ts (separately landed as #10206). Stacked on feat/responses-previous-response-id-virtualization (#3), which is itself stacked on feat/openai-responses-store-toggle (#10121). * fix(dashboard): resync conversation list on open so the live-text poll starts immediately openConversation() seeded activeConversation (and therefore activeCallLogId, which gates the live-partial-text poll effect) from whatever row snapshot the list's own fixed-interval poll last produced. A conversation opened right after a reply started streaming -- after that tick, before the next -- had activeCallLogId still null, so the live-text poll never started; only a subsequent background list-poll resync (already existed) picked it up, which is why closing and reopening the same conversation "just worked". loadConversations() is now a shared callback so openConversation can force one immediately on open instead of waiting on pollSeconds. Live-verified against omniroute-dev: opening a conversation mid-stream now shows live reasoning on the first open. * style: prettier formatting for conversationTurnContent.test.ts * fix(db): close migration numbering gap left by decoupling from #3/#10262 153/154 (originally 154/155) were chosen back when this branch stacked on top of the previous_response_id migration (153_call_logs_response_id.sql). Decoupling removed that migration from this branch's history, leaving an unused 153 slot that check-migration-numbering.test.ts correctly flags as a gap. * refactor(dashboard): split RequestTimeline/RequestLoggerDetail under the 1000-line file-size cap Both files exceeded check-file-size's new-file cap after this PR's own additions (RequestTimeline 1048, RequestLoggerDetail 1163). Extracted pure non-component logic (types, constants, allocateLanes and its helpers) out of RequestTimeline.tsx into RequestTimeline.utils.ts, and the two self-contained presentational sub-components (PayloadSection, ConversationContextSection + its private helper) out of RequestLoggerDetail.tsx into RequestLoggerDetail.sections.tsx. No behavior change; existing external imports (default exports, allocateLanes, TimelineLog, CONVERSATION_LANE_REUSE_STORAGE_KEY) still resolve from the original file paths. * fix(db): renumber agentic-conversation migrations to clear 153 collision + sync migration-count docs The refresh-merge of release/v3.8.50 exposed that the feature's three migrations collided at slot 153 with the base's radar_local_model_state (153) and its own call_logs_response_id. Migration runner enforces unique numeric prefixes -> every DB init threw, red-ing Vitest, all Unit shards and the DB-backed quality gates. Renumber the feature's pair to 155_agentic_conversations / 156_conversation_turn_nodes and move call_logs_response_id to 154 (keeps 153_radar base-owned, preserves agentic-before-turn_nodes ordering). Update SQL headers and the 154/156 references in feature code + tests. Migration count is now 151 (was 148 stale in README/AGENTS/llm.txt) — sync the doc counts to clear the docs-accuracy gate. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(ui): drop unused CONVERSATION_LANE_REUSE_STORAGE_KEY re-export from RequestTimeline Knip 6.32 (baseline 415) flags the public re-export of CONVERSATION_LANE_REUSE_STORAGE_KEY from RequestTimeline.tsx as dead: no external consumer imports it through that re-export (it is imported and used directly from RequestTimeline.utils.ts inside the component). Removed the unused re-export; the internal import stays. DEAD_TOTAL 416 -> 415, back to the frozen baseline. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(agentic-conversations): guard resolveConversationId, drop dead whole-chain export - Wrap resolveConversationId() in try/catch in chat.ts, matching the defensive pattern used by every other best-effort side call nearby, so a DB hiccup in conversation tracking can't turn a working chat request into a hard failure. - Remove getConversationTurnTree: knip's project scope excludes tests/**, so an export used only by tests can never register as used there. Swap its 8 test call sites to the paginated getConversationTurnPage (already the dashboard's canonical query) with a generous limit, collapsing to one query path instead of keeping a second whole-chain export alive solely for test convenience. - Regenerate i18n llm.txt mirrors from root (pre-existing drift on this branch, unrelated to the above, caught by the docs-sync pre-commit gate). Addresses PR review feedback. * fix(i18n): close requestLogger conversation-column gap, fix domain-modules count drift - fr.json, vi.json were missing requestLogger.columns.conversation (added in the conversation-tracking feature), failing i18n-vi-completeness.test.ts. - docs/i18n/*/llm.txt mirrors still said 117 domain-specific files after an earlier rebase fixed the migration count but missed this companion number, failing check-docs-sync.mjs across all 42 locales. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(docs): restore PROXY_LOG_INCLUDE_IPS env/doc entries (env-doc-sync red) .env.example and docs/reference/ENVIRONMENT.md were both missing the PROXY_LOG_INCLUDE_IPS entry that src/lib/proxyLogger.ts already reads (confirmed present at this branch's merge-base too, so this predates the conversation-tracking work and is unrelated to it) -- the entry was added on release/v3.8.50 after this branch's last sync and this branch never picked it up. That gap red-lines tests/unit/check-env-doc-sync.test.ts and tests/unit/issue-7793-env-doc-sync-repro.test.ts (Unit Tests fast-path 2/4 in CI). Restore both entries verbatim from the current release/v3.8.50 tip -- no feature-code change. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: hartmark <hartmark@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
300 lines
13 KiB
TypeScript
300 lines
13 KiB
TypeScript
/**
|
|
* Unit tests for src/lib/db/agenticConversations.ts CRUD.
|
|
*/
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { mkdtempSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-agentic-conv-db-"));
|
|
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "agentic-conversations-test-secret";
|
|
|
|
// Dynamic imports (not static) are required here: a static `import` of a module
|
|
// that reads process.env.DATA_DIR at its own top level (src/lib/db/core.ts's
|
|
// `export const DATA_DIR = ...`) is evaluated before this file's own top-level
|
|
// code runs — ESM instantiates the whole dependency graph, dependencies first,
|
|
// regardless of source-line order — so the override above would silently miss
|
|
// and the module would resolve the real host DATA_DIR instead of the temp dir.
|
|
const {
|
|
createAgenticConversation,
|
|
findAgenticConversationsByFingerprint,
|
|
updateAgenticConversation,
|
|
touchOrCreateExternalConversation,
|
|
listMultiTurnConversations,
|
|
getConversationTurnIndex,
|
|
insertConversationTurnNodes,
|
|
getConversationTurnPage,
|
|
resolveCallLogIdsByCorrelationIds,
|
|
} = await import("../../src/lib/db/agenticConversations.ts");
|
|
const { getDbInstance } = await import("../../src/lib/db/core.ts");
|
|
|
|
test("createAgenticConversation + findAgenticConversationsByFingerprint round-trip", () => {
|
|
const row = createAgenticConversation({
|
|
apiKeyId: "key-a",
|
|
fingerprintHash: "fp-round-trip",
|
|
});
|
|
|
|
assert.match(row.id, /^conv_/);
|
|
assert.equal(row.turnCount, 1);
|
|
|
|
const found = findAgenticConversationsByFingerprint("fp-round-trip");
|
|
assert.equal(found.length, 1);
|
|
assert.equal(found[0].id, row.id);
|
|
assert.equal(found[0].apiKeyId, "key-a");
|
|
});
|
|
|
|
test("findAgenticConversationsByFingerprint returns multiple rows for a shared fingerprint", () => {
|
|
createAgenticConversation({ apiKeyId: "key-b", fingerprintHash: "fp-shared" });
|
|
createAgenticConversation({ apiKeyId: "key-b", fingerprintHash: "fp-shared" });
|
|
|
|
const found = findAgenticConversationsByFingerprint("fp-shared");
|
|
assert.equal(found.length, 2);
|
|
});
|
|
|
|
test("updateAgenticConversation updates turn count", () => {
|
|
const row = createAgenticConversation({ apiKeyId: "key-c", fingerprintHash: "fp-update" });
|
|
|
|
updateAgenticConversation(row.id, { turnCount: 3 });
|
|
|
|
const found = findAgenticConversationsByFingerprint("fp-update");
|
|
assert.equal(found[0].turnCount, 3);
|
|
});
|
|
|
|
test("insertConversationTurnNodes + getConversationTurnIndex round-trip", () => {
|
|
const row = createAgenticConversation({ apiKeyId: "key-nodes", fingerprintHash: "fp-nodes" });
|
|
|
|
insertConversationTurnNodes(row.id, "corr-1", [
|
|
{ id: "node-a", parentId: null, role: "user", contentHash: "hash-a" },
|
|
{ id: "node-b", parentId: "node-a", role: "assistant", contentHash: "hash-b" },
|
|
]);
|
|
|
|
const index = getConversationTurnIndex(row.id);
|
|
assert.equal(index.nodeIds.size, 2);
|
|
assert.ok(index.nodeIds.has("node-a"));
|
|
assert.ok(index.nodeIds.has("node-b"));
|
|
assert.deepEqual(index.byContentHash.get("hash-a"), ["node-a"]);
|
|
assert.deepEqual(index.byContentHash.get("hash-b"), ["node-b"]);
|
|
|
|
// A different conversation's nodes must never leak into this index.
|
|
const other = createAgenticConversation({
|
|
apiKeyId: "key-nodes-2",
|
|
fingerprintHash: "fp-nodes-2",
|
|
});
|
|
insertConversationTurnNodes(other.id, "corr-2", [
|
|
{ id: "node-c", parentId: null, role: "user", contentHash: "hash-c" },
|
|
]);
|
|
const reReadIndex = getConversationTurnIndex(row.id);
|
|
assert.equal(reReadIndex.nodeIds.size, 2);
|
|
assert.equal(reReadIndex.byContentHash.has("hash-c"), false);
|
|
});
|
|
|
|
test("getConversationTurnIndex groups multiple node ids under the same content hash (duplicate turn text at different tree positions)", () => {
|
|
const row = createAgenticConversation({ apiKeyId: "key-dup-content", fingerprintHash: "fp-dup" });
|
|
|
|
insertConversationTurnNodes(row.id, "corr-1", [
|
|
{ id: "node-1", parentId: null, role: "user", contentHash: "hash-ok" },
|
|
{ id: "node-2", parentId: "node-1", role: "assistant", contentHash: "hash-reply" },
|
|
// Same content ("ok") recurs later in the same tree, at a different node.
|
|
{ id: "node-3", parentId: "node-2", role: "user", contentHash: "hash-ok" },
|
|
]);
|
|
|
|
const index = getConversationTurnIndex(row.id);
|
|
const matches = index.byContentHash.get("hash-ok");
|
|
assert.equal(matches?.length, 2);
|
|
assert.deepEqual([...matches!].sort(), ["node-1", "node-3"]);
|
|
});
|
|
|
|
test("insertConversationTurnNodes is idempotent for already-existing node ids (INSERT OR IGNORE)", () => {
|
|
const row = createAgenticConversation({ apiKeyId: "key-idem", fingerprintHash: "fp-idem" });
|
|
|
|
insertConversationTurnNodes(row.id, "corr-1", [
|
|
{ id: "node-dup", parentId: null, role: "user", contentHash: "hash-dup" },
|
|
]);
|
|
// Re-insert the same id — must not throw, must not duplicate.
|
|
insertConversationTurnNodes(row.id, "corr-2", [
|
|
{ id: "node-dup", parentId: null, role: "user", contentHash: "hash-dup" },
|
|
]);
|
|
|
|
const tree = getConversationTurnPage(row.id, { limit: 500 }).nodes;
|
|
assert.equal(tree.length, 1);
|
|
});
|
|
|
|
test("getConversationTurnPage returns the full chain with parent/child structure and content hash", () => {
|
|
const row = createAgenticConversation({ apiKeyId: "key-tree", fingerprintHash: "fp-tree" });
|
|
|
|
insertConversationTurnNodes(row.id, "corr-tree", [
|
|
{ id: "root-turn", parentId: null, role: "user", contentHash: "hash-hello" },
|
|
{ id: "child-turn", parentId: "root-turn", role: "assistant", contentHash: "hash-hi" },
|
|
]);
|
|
// A sibling branch off the same parent.
|
|
insertConversationTurnNodes(row.id, "corr-tree-2", [
|
|
{ id: "sibling-turn", parentId: "root-turn", role: "assistant", contentHash: "hash-hey" },
|
|
]);
|
|
|
|
const tree = getConversationTurnPage(row.id, { limit: 500 }).nodes;
|
|
assert.equal(tree.length, 3);
|
|
|
|
const root = tree.find((n) => n.id === "root-turn");
|
|
const children = tree.filter((n) => n.parentId === "root-turn");
|
|
assert.equal(root?.parentId, null);
|
|
assert.equal(root?.contentHash, "hash-hello");
|
|
assert.equal(children.length, 2);
|
|
assert.deepEqual(children.map((c) => c.id).sort(), ["child-turn", "sibling-turn"]);
|
|
});
|
|
|
|
test("getConversationTurnPage: initial load returns only the last `limit` turns, oldest-first, with hasMore", () => {
|
|
const row = createAgenticConversation({ apiKeyId: "key-page", fingerprintHash: "fp-page" });
|
|
const nodes = Array.from({ length: 25 }, (_, i) => ({
|
|
id: `n${i}`,
|
|
parentId: i === 0 ? null : `n${i - 1}`,
|
|
role: i % 2 === 0 ? "user" : "assistant",
|
|
contentHash: `hash-${i}`,
|
|
}));
|
|
insertConversationTurnNodes(row.id, "corr-page", nodes);
|
|
|
|
const page = getConversationTurnPage(row.id, { limit: 20 });
|
|
assert.equal(page.nodes.length, 20);
|
|
assert.equal(page.hasMore, true);
|
|
// Oldest-first within the page, and it's the LAST 20 (n5..n24).
|
|
assert.equal(page.nodes[0].id, "n5");
|
|
assert.equal(page.nodes[19].id, "n24");
|
|
});
|
|
|
|
test("getConversationTurnPage: beforeSeq loads the previous page (older turns), with correct hasMore", () => {
|
|
const row = createAgenticConversation({ apiKeyId: "key-page-2", fingerprintHash: "fp-page-2" });
|
|
const nodes = Array.from({ length: 25 }, (_, i) => ({
|
|
id: `m${i}`,
|
|
parentId: i === 0 ? null : `m${i - 1}`,
|
|
role: "user",
|
|
contentHash: `hash-m${i}`,
|
|
}));
|
|
insertConversationTurnNodes(row.id, "corr-page-2", nodes);
|
|
|
|
const firstPage = getConversationTurnPage(row.id, { limit: 20 });
|
|
const oldestSeqInFirstPage = firstPage.nodes[0].seq;
|
|
|
|
const olderPage = getConversationTurnPage(row.id, { limit: 20, beforeSeq: oldestSeqInFirstPage });
|
|
assert.equal(olderPage.nodes.length, 5, "only 5 turns (0-4) exist before the first page");
|
|
assert.equal(olderPage.hasMore, false);
|
|
assert.equal(olderPage.nodes[0].id, "m0");
|
|
assert.equal(olderPage.nodes[4].id, "m4");
|
|
});
|
|
|
|
test("getConversationTurnPage: afterSeq returns only turns newer than the cursor (for polling), uncapped", () => {
|
|
const row = createAgenticConversation({ apiKeyId: "key-page-3", fingerprintHash: "fp-page-3" });
|
|
insertConversationTurnNodes(row.id, "corr-page-3", [
|
|
{ id: "p0", parentId: null, role: "user", contentHash: "h0" },
|
|
{ id: "p1", parentId: "p0", role: "assistant", contentHash: "h1" },
|
|
]);
|
|
const firstPage = getConversationTurnPage(row.id, { limit: 20 });
|
|
const newestSeq = firstPage.nodes[firstPage.nodes.length - 1].seq;
|
|
|
|
// Nothing new yet.
|
|
assert.equal(getConversationTurnPage(row.id, { afterSeq: newestSeq }).nodes.length, 0);
|
|
|
|
// A new turn arrives (e.g. a later request continuing this conversation).
|
|
insertConversationTurnNodes(row.id, "corr-page-3b", [
|
|
{ id: "p2", parentId: "p1", role: "user", contentHash: "h2" },
|
|
]);
|
|
const polled = getConversationTurnPage(row.id, { afterSeq: newestSeq });
|
|
assert.equal(polled.nodes.length, 1);
|
|
assert.equal(polled.nodes[0].id, "p2");
|
|
assert.equal(polled.hasMore, false);
|
|
});
|
|
|
|
test("touchOrCreateExternalConversation creates then increments turn_count on repeat calls", () => {
|
|
const id = "ext-conv-test-id";
|
|
touchOrCreateExternalConversation(id, { apiKeyId: "key-d" });
|
|
|
|
const db = getDbInstance();
|
|
const afterCreate = db
|
|
.prepare("SELECT turn_count FROM agentic_conversations WHERE id = ?")
|
|
.get(id) as { turn_count: number };
|
|
assert.equal(afterCreate.turn_count, 1);
|
|
|
|
touchOrCreateExternalConversation(id, { apiKeyId: "key-d" });
|
|
const afterTouch = db
|
|
.prepare("SELECT turn_count FROM agentic_conversations WHERE id = ?")
|
|
.get(id) as { turn_count: number };
|
|
assert.equal(afterTouch.turn_count, 2);
|
|
});
|
|
|
|
test("listMultiTurnConversations only returns conversations with >= 2 actual turn nodes, joined to their latest call_logs row", () => {
|
|
const db = getDbInstance();
|
|
|
|
createAgenticConversation({
|
|
id: "conv-single-turn",
|
|
apiKeyId: null,
|
|
fingerprintHash: "fp-single",
|
|
});
|
|
insertConversationTurnNodes("conv-single-turn", "corr-single", [
|
|
{ id: "single-node-1", parentId: null, role: "user", contentHash: "hash-single-1" },
|
|
]);
|
|
|
|
const multi = createAgenticConversation({
|
|
id: "conv-multi-turn",
|
|
apiKeyId: null,
|
|
fingerprintHash: "fp-multi",
|
|
});
|
|
// turn_count deliberately left at its default of 1 here: it tracks
|
|
// requests-touched, not node count, and a freshly-minted conversation can
|
|
// already carry many turn nodes from a single insert (see the doc comment
|
|
// on listMultiTurnConversations) — the filter must key off actual node
|
|
// count, not turn_count, for this conversation to be listed at all.
|
|
insertConversationTurnNodes(multi.id, "corr-multi", [
|
|
{ id: "multi-node-1", parentId: null, role: "user", contentHash: "hash-multi-1" },
|
|
{
|
|
id: "multi-node-2",
|
|
parentId: "multi-node-1",
|
|
role: "assistant",
|
|
contentHash: "hash-multi-2",
|
|
},
|
|
]);
|
|
|
|
db.prepare(
|
|
`INSERT INTO call_logs (id, timestamp, method, path, status, model, provider, session_tag)
|
|
VALUES (?, ?, 'POST', '/v1/chat/completions', 200, 'big-pickle', 'opencode-zen', ?)`
|
|
).run("multi-turn-1", "2026-03-01T00:00:00.000Z", "conv-multi-turn");
|
|
db.prepare(
|
|
`INSERT INTO call_logs (id, timestamp, method, path, status, model, provider, session_tag)
|
|
VALUES (?, ?, 'POST', '/v1/chat/completions', 200, 'gemma-4', 'gemini', ?)`
|
|
).run("multi-turn-2", "2026-03-01T00:01:00.000Z", "conv-multi-turn");
|
|
|
|
const { rows, total } = listMultiTurnConversations();
|
|
const ids = rows.map((r) => r.id);
|
|
assert.ok(ids.includes("conv-multi-turn"));
|
|
assert.ok(!ids.includes("conv-single-turn"));
|
|
assert.ok(total >= 1);
|
|
|
|
const found = rows.find((r) => r.id === "conv-multi-turn");
|
|
assert.equal(found?.lastCallLogId, "multi-turn-2");
|
|
assert.equal(found?.lastModel, "gemma-4");
|
|
assert.equal(found?.lastProvider, "gemini");
|
|
});
|
|
|
|
test("resolveCallLogIdsByCorrelationIds bulk-resolves correlation_id to call_logs.id", () => {
|
|
const db = getDbInstance();
|
|
|
|
db.prepare(
|
|
`INSERT INTO call_logs (id, timestamp, method, path, status, model, correlation_id)
|
|
VALUES (?, ?, 'POST', '/v1/chat/completions', 200, 'big-pickle', ?)`
|
|
).run("call-corr-1", "2026-04-01T00:00:00.000Z", "corr-a");
|
|
db.prepare(
|
|
`INSERT INTO call_logs (id, timestamp, method, path, status, model, correlation_id)
|
|
VALUES (?, ?, 'POST', '/v1/chat/completions', 200, 'big-pickle', ?)`
|
|
).run("call-corr-2", "2026-04-01T00:01:00.000Z", "corr-b");
|
|
|
|
const resolved = resolveCallLogIdsByCorrelationIds(["corr-a", "corr-b", "corr-missing"]);
|
|
assert.equal(resolved.get("corr-a"), "call-corr-1");
|
|
assert.equal(resolved.get("corr-b"), "call-corr-2");
|
|
assert.equal(resolved.has("corr-missing"), false);
|
|
});
|
|
|
|
test("resolveCallLogIdsByCorrelationIds returns an empty map for an empty/all-falsy input", () => {
|
|
assert.equal(resolveCallLogIdsByCorrelationIds([]).size, 0);
|
|
assert.equal(resolveCallLogIdsByCorrelationIds(["", ""]).size, 0);
|
|
});
|