114 lines
4.8 KiB
TypeScript
114 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { ToolCallRecord } from "./tool-call-state";
|
|
import type { TaskRecord } from "./task-state";
|
|
import { ConversationStore } from "./conversation-store";
|
|
|
|
class MemoryStorage implements Storage {
|
|
private readonly values = new Map<string, string>();
|
|
|
|
public get length(): number { return this.values.size; }
|
|
public clear(): void { this.values.clear(); }
|
|
public getItem(key: string): string | null { return this.values.get(key) ?? null; }
|
|
public key(index: number): string | null { return [...this.values.keys()][index] ?? null; }
|
|
public removeItem(key: string): void { this.values.delete(key); }
|
|
public setItem(key: string, value: string): void { this.values.set(key, value); }
|
|
}
|
|
|
|
const conversationID = "user_test:2:agent_channel";
|
|
const createdAt = "2026-08-03T00:00:00.000Z";
|
|
|
|
describe("ConversationStore", () => {
|
|
it("keeps the inclusive sync cursor at zero after a send acknowledgement", () => {
|
|
const store = new ConversationStore(new MemoryStorage());
|
|
store.open(conversationID);
|
|
store.appendLocalText("local_001", "hello", createdAt);
|
|
store.markSubmitted("local_001", 41, createdAt);
|
|
|
|
expect(store.snapshot()).toMatchObject({ cursor: 0, outbox: [] });
|
|
expect(store.snapshot().items).toEqual([expect.objectContaining({
|
|
local_id: "local_001",
|
|
message_seq: 41,
|
|
item: expect.objectContaining({ kind: "user-message", delivery: expect.objectContaining({ status: "submitted" }) }),
|
|
})]);
|
|
});
|
|
|
|
it("deduplicates the inclusive sync echo of a locally submitted user message", () => {
|
|
const store = new ConversationStore(new MemoryStorage());
|
|
store.open(conversationID);
|
|
store.appendLocalText("local_001", "hello", createdAt);
|
|
store.markSubmitted("local_001", 41, createdAt);
|
|
|
|
const restored = store.recordSyncedUserText(41, "hello", createdAt);
|
|
|
|
expect(restored).toBeUndefined();
|
|
expect(store.snapshot()).toMatchObject({ cursor: 41 });
|
|
expect(store.snapshot().items).toHaveLength(1);
|
|
});
|
|
|
|
it("persists a local echo and restores it for the same conversation", () => {
|
|
const storage = new MemoryStorage();
|
|
const first = new ConversationStore(storage);
|
|
first.open(conversationID);
|
|
first.appendLocalText("local_002", "persist me", createdAt);
|
|
|
|
const reopened = new ConversationStore(storage);
|
|
const snapshot = reopened.open(conversationID);
|
|
|
|
expect(snapshot.cursor).toBe(0);
|
|
expect(snapshot.outbox).toEqual([expect.objectContaining({ local_id: "local_002", payload: "persist me" })]);
|
|
expect(snapshot.items).toEqual([expect.objectContaining({
|
|
local_id: "local_002",
|
|
item: expect.objectContaining({ kind: "user-message", text: "persist me" }),
|
|
})]);
|
|
});
|
|
|
|
it("accepts one remote message per IM sequence and advances the cursor only from sync data", () => {
|
|
const store = new ConversationStore(new MemoryStorage());
|
|
store.open(conversationID);
|
|
const item = { kind: "markdown" as const, id: "evt_remote_001", markdown: "remote" };
|
|
|
|
expect(store.recordIncoming(item, 42, createdAt)).toBe(true);
|
|
expect(store.recordIncoming(item, 42, createdAt)).toBe(false);
|
|
expect(store.snapshot()).toMatchObject({ cursor: 42 });
|
|
expect(store.snapshot().items).toHaveLength(1);
|
|
});
|
|
|
|
it("persists Tool Call state and an unsubmitted form draft per conversation", () => {
|
|
const storage = new MemoryStorage();
|
|
const state: ToolCallRecord = {
|
|
call_id: "call_form_001",
|
|
conversation_id: conversationID,
|
|
request: {
|
|
call_id: "call_form_001",
|
|
tool: "input",
|
|
title: "Deploy",
|
|
prompt: "Provide a version.",
|
|
data: { form: {} },
|
|
},
|
|
status: "pending",
|
|
created_at: createdAt,
|
|
updated_at: createdAt,
|
|
};
|
|
const first = new ConversationStore(storage);
|
|
first.open(conversationID);
|
|
first.replaceToolCalls([state]);
|
|
first.saveToolDraft("call_form_001", { version: "1.2.3" });
|
|
|
|
const reopened = new ConversationStore(storage).open(conversationID);
|
|
expect(reopened.tool_calls).toEqual([expect.objectContaining({ call_id: "call_form_001", status: "pending" })]);
|
|
expect(reopened.tool_drafts).toEqual({ call_form_001: { version: "1.2.3" } });
|
|
});
|
|
|
|
it("persists the latest aggregated task record per conversation", () => {
|
|
const storage = new MemoryStorage();
|
|
const task: TaskRecord = {
|
|
operation_id: "task_store_001", conversation_id: conversationID, title: "Render preview", percent: 70, status: "running", cancellable: true, updated_at: createdAt,
|
|
};
|
|
const first = new ConversationStore(storage);
|
|
first.open(conversationID);
|
|
first.replaceTasks([task]);
|
|
|
|
expect(new ConversationStore(storage).open(conversationID).tasks).toEqual([expect.objectContaining({ operation_id: "task_store_001", percent: 70 })]);
|
|
});
|
|
});
|