summaryrefslogtreecommitdiffhomepage
path: root/src/features/conversation-cache/logic.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/conversation-cache/logic.test.ts')
-rw-r--r--src/features/conversation-cache/logic.test.ts248
1 files changed, 124 insertions, 124 deletions
diff --git a/src/features/conversation-cache/logic.test.ts b/src/features/conversation-cache/logic.test.ts
index 858460a..880c32e 100644
--- a/src/features/conversation-cache/logic.test.ts
+++ b/src/features/conversation-cache/logic.test.ts
@@ -4,137 +4,137 @@ import { nextSinceSeq, reconcileCache, selectEvictions } from "./logic";
import type { ConversationCacheIndexEntry } from "./types";
const chunk = (seq: number, role: "user" | "assistant" = "user"): StoredChunk => ({
- seq,
- role,
- chunk: { type: "text", text: `chunk-${seq}` },
+ seq,
+ role,
+ chunk: { type: "text", text: `chunk-${seq}` },
});
describe("reconcileCache", () => {
- it("merges and dedupes by seq", () => {
- const cached = [chunk(1), chunk(2)];
- const incoming = [chunk(2), chunk(3)];
- const result = reconcileCache(cached, incoming);
- expect(result.merged).toEqual([chunk(1), chunk(2), chunk(3)]);
- });
-
- it("toAppend excludes already-cached seqs", () => {
- const cached = [chunk(1), chunk(2)];
- const incoming = [chunk(2), chunk(3)];
- const result = reconcileCache(cached, incoming);
- expect(result.toAppend).toEqual([chunk(3)]);
- });
-
- it("tolerates out-of-order incoming", () => {
- const cached = [chunk(1)];
- const incoming = [chunk(5), chunk(3), chunk(2)];
- const result = reconcileCache(cached, incoming);
- expect(result.merged).toEqual([chunk(1), chunk(2), chunk(3), chunk(5)]);
- expect(result.toAppend).toEqual([chunk(5), chunk(3), chunk(2)]);
- });
-
- it("returns empty merged and toAppend when both inputs are empty", () => {
- const result = reconcileCache([], []);
- expect(result.merged).toEqual([]);
- expect(result.toAppend).toEqual([]);
- });
-
- it("handles empty cached with incoming", () => {
- const incoming = [chunk(3), chunk(1)];
- const result = reconcileCache([], incoming);
- expect(result.merged).toEqual([chunk(1), chunk(3)]);
- expect(result.toAppend).toEqual([chunk(3), chunk(1)]);
- });
-
- it("handles cached with empty incoming", () => {
- const cached = [chunk(1), chunk(2)];
- const result = reconcileCache(cached, []);
- expect(result.merged).toEqual([chunk(1), chunk(2)]);
- expect(result.toAppend).toEqual([]);
- });
-
- it("is idempotent — re-reconciling same incoming produces same result", () => {
- const cached = [chunk(1)];
- const incoming = [chunk(2), chunk(3)];
- const first = reconcileCache(cached, incoming);
- const second = reconcileCache(first.merged, incoming);
- expect(second.merged).toEqual(first.merged);
- expect(second.toAppend).toEqual([]);
- });
+ it("merges and dedupes by seq", () => {
+ const cached = [chunk(1), chunk(2)];
+ const incoming = [chunk(2), chunk(3)];
+ const result = reconcileCache(cached, incoming);
+ expect(result.merged).toEqual([chunk(1), chunk(2), chunk(3)]);
+ });
+
+ it("toAppend excludes already-cached seqs", () => {
+ const cached = [chunk(1), chunk(2)];
+ const incoming = [chunk(2), chunk(3)];
+ const result = reconcileCache(cached, incoming);
+ expect(result.toAppend).toEqual([chunk(3)]);
+ });
+
+ it("tolerates out-of-order incoming", () => {
+ const cached = [chunk(1)];
+ const incoming = [chunk(5), chunk(3), chunk(2)];
+ const result = reconcileCache(cached, incoming);
+ expect(result.merged).toEqual([chunk(1), chunk(2), chunk(3), chunk(5)]);
+ expect(result.toAppend).toEqual([chunk(5), chunk(3), chunk(2)]);
+ });
+
+ it("returns empty merged and toAppend when both inputs are empty", () => {
+ const result = reconcileCache([], []);
+ expect(result.merged).toEqual([]);
+ expect(result.toAppend).toEqual([]);
+ });
+
+ it("handles empty cached with incoming", () => {
+ const incoming = [chunk(3), chunk(1)];
+ const result = reconcileCache([], incoming);
+ expect(result.merged).toEqual([chunk(1), chunk(3)]);
+ expect(result.toAppend).toEqual([chunk(3), chunk(1)]);
+ });
+
+ it("handles cached with empty incoming", () => {
+ const cached = [chunk(1), chunk(2)];
+ const result = reconcileCache(cached, []);
+ expect(result.merged).toEqual([chunk(1), chunk(2)]);
+ expect(result.toAppend).toEqual([]);
+ });
+
+ it("is idempotent — re-reconciling same incoming produces same result", () => {
+ const cached = [chunk(1)];
+ const incoming = [chunk(2), chunk(3)];
+ const first = reconcileCache(cached, incoming);
+ const second = reconcileCache(first.merged, incoming);
+ expect(second.merged).toEqual(first.merged);
+ expect(second.toAppend).toEqual([]);
+ });
});
describe("nextSinceSeq", () => {
- it("returns max seq", () => {
- const cached = [chunk(1), chunk(5), chunk(3)];
- expect(nextSinceSeq(cached)).toBe(5);
- });
-
- it("returns 0 when empty", () => {
- expect(nextSinceSeq([])).toBe(0);
- });
-
- it("returns single seq for single chunk", () => {
- expect(nextSinceSeq([chunk(42)])).toBe(42);
- });
+ it("returns max seq", () => {
+ const cached = [chunk(1), chunk(5), chunk(3)];
+ expect(nextSinceSeq(cached)).toBe(5);
+ });
+
+ it("returns 0 when empty", () => {
+ expect(nextSinceSeq([])).toBe(0);
+ });
+
+ it("returns single seq for single chunk", () => {
+ expect(nextSinceSeq([chunk(42)])).toBe(42);
+ });
});
describe("selectEvictions", () => {
- it("never evicts the active conversation", () => {
- const index: ConversationCacheIndexEntry[] = [
- { conversationId: "active", chunkCount: 100, maxSeq: 100, lastAccess: 1000 },
- { conversationId: "other", chunkCount: 50, maxSeq: 50, lastAccess: 1 },
- ];
- const result = selectEvictions(index, { maxChunks: 50, activeConversationId: "active" });
- expect(result).not.toContain("active");
- expect(result).toContain("other");
- });
-
- it("evicts LRU until under budget", () => {
- const index: ConversationCacheIndexEntry[] = [
- { conversationId: "a", chunkCount: 30, maxSeq: 30, lastAccess: 100 },
- { conversationId: "b", chunkCount: 30, maxSeq: 30, lastAccess: 50 },
- { conversationId: "c", chunkCount: 30, maxSeq: 30, lastAccess: 200 },
- { conversationId: "d", chunkCount: 30, maxSeq: 30, lastAccess: 10 },
- ];
- // Total = 120, max = 60, need to evict 60+ chunks
- // LRU order: d(10), b(50), a(100), c(200)
- const result = selectEvictions(index, { maxChunks: 60, activeConversationId: null });
- expect(result).toEqual(["d", "b"]);
- });
-
- it("is a no-op under budget", () => {
- const index: ConversationCacheIndexEntry[] = [
- { conversationId: "a", chunkCount: 10, maxSeq: 10, lastAccess: 100 },
- { conversationId: "b", chunkCount: 10, maxSeq: 10, lastAccess: 50 },
- ];
- const result = selectEvictions(index, { maxChunks: 100, activeConversationId: null });
- expect(result).toEqual([]);
- });
-
- it("returns empty for empty index", () => {
- const result = selectEvictions([], { maxChunks: 100, activeConversationId: null });
- expect(result).toEqual([]);
- });
-
- it("tie-breaks by smaller maxSeq when lastAccess is equal", () => {
- const index: ConversationCacheIndexEntry[] = [
- { conversationId: "a", chunkCount: 30, maxSeq: 100, lastAccess: 50 },
- { conversationId: "b", chunkCount: 30, maxSeq: 50, lastAccess: 50 },
- { conversationId: "c", chunkCount: 30, maxSeq: 200, lastAccess: 50 },
- ];
- // Total = 90, max = 60, need to evict 30+ chunks
- // All have same lastAccess, tie-break by maxSeq: b(50), a(100), c(200)
- const result = selectEvictions(index, { maxChunks: 60, activeConversationId: null });
- expect(result).toEqual(["b"]);
- });
-
- it("handles missing lastAccess (treated as 0)", () => {
- const index: ConversationCacheIndexEntry[] = [
- { conversationId: "a", chunkCount: 30, maxSeq: 30, lastAccess: 100 },
- { conversationId: "b", chunkCount: 30, maxSeq: 30 },
- ];
- // Total = 60, max = 30, need to evict 30+ chunks
- // b has no lastAccess (0), a has 100
- const result = selectEvictions(index, { maxChunks: 30, activeConversationId: null });
- expect(result).toEqual(["b"]);
- });
+ it("never evicts the active conversation", () => {
+ const index: ConversationCacheIndexEntry[] = [
+ { conversationId: "active", chunkCount: 100, maxSeq: 100, lastAccess: 1000 },
+ { conversationId: "other", chunkCount: 50, maxSeq: 50, lastAccess: 1 },
+ ];
+ const result = selectEvictions(index, { maxChunks: 50, activeConversationId: "active" });
+ expect(result).not.toContain("active");
+ expect(result).toContain("other");
+ });
+
+ it("evicts LRU until under budget", () => {
+ const index: ConversationCacheIndexEntry[] = [
+ { conversationId: "a", chunkCount: 30, maxSeq: 30, lastAccess: 100 },
+ { conversationId: "b", chunkCount: 30, maxSeq: 30, lastAccess: 50 },
+ { conversationId: "c", chunkCount: 30, maxSeq: 30, lastAccess: 200 },
+ { conversationId: "d", chunkCount: 30, maxSeq: 30, lastAccess: 10 },
+ ];
+ // Total = 120, max = 60, need to evict 60+ chunks
+ // LRU order: d(10), b(50), a(100), c(200)
+ const result = selectEvictions(index, { maxChunks: 60, activeConversationId: null });
+ expect(result).toEqual(["d", "b"]);
+ });
+
+ it("is a no-op under budget", () => {
+ const index: ConversationCacheIndexEntry[] = [
+ { conversationId: "a", chunkCount: 10, maxSeq: 10, lastAccess: 100 },
+ { conversationId: "b", chunkCount: 10, maxSeq: 10, lastAccess: 50 },
+ ];
+ const result = selectEvictions(index, { maxChunks: 100, activeConversationId: null });
+ expect(result).toEqual([]);
+ });
+
+ it("returns empty for empty index", () => {
+ const result = selectEvictions([], { maxChunks: 100, activeConversationId: null });
+ expect(result).toEqual([]);
+ });
+
+ it("tie-breaks by smaller maxSeq when lastAccess is equal", () => {
+ const index: ConversationCacheIndexEntry[] = [
+ { conversationId: "a", chunkCount: 30, maxSeq: 100, lastAccess: 50 },
+ { conversationId: "b", chunkCount: 30, maxSeq: 50, lastAccess: 50 },
+ { conversationId: "c", chunkCount: 30, maxSeq: 200, lastAccess: 50 },
+ ];
+ // Total = 90, max = 60, need to evict 30+ chunks
+ // All have same lastAccess, tie-break by maxSeq: b(50), a(100), c(200)
+ const result = selectEvictions(index, { maxChunks: 60, activeConversationId: null });
+ expect(result).toEqual(["b"]);
+ });
+
+ it("handles missing lastAccess (treated as 0)", () => {
+ const index: ConversationCacheIndexEntry[] = [
+ { conversationId: "a", chunkCount: 30, maxSeq: 30, lastAccess: 100 },
+ { conversationId: "b", chunkCount: 30, maxSeq: 30 },
+ ];
+ // Total = 60, max = 30, need to evict 30+ chunks
+ // b has no lastAccess (0), a has 100
+ const result = selectEvictions(index, { maxChunks: 30, activeConversationId: null });
+ expect(result).toEqual(["b"]);
+ });
});