import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { SessionHistoryStore } from "../../src/history/store.js"; describe("SessionHistoryStore", () => { let tempDir: string; let store: SessionHistoryStore; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "tjwater-history-")); store = new SessionHistoryStore(tempDir); await store.initialize(); }); afterEach(async () => { await rm(tempDir, { force: true, recursive: true }); }); it("falls back to legacy runtime-session transcripts by client session id and migrates on append", async () => { await writeFile( join(tempDir, "actor-1__project-1__runtime-session-1.json"), JSON.stringify( { actorKey: "actor-1", clientSessionId: "thread-1", projectKey: "project-1", sessionId: "runtime-session-1", turns: [ { id: "turn-1", assistantMessage: "先检查泵站流量。", timestamp: "2026-05-21T00:00:00.000Z", toolCallCount: 1, userMessage: "帮我看一下当前异常。", }, ], updatedAt: "2026-05-21T00:00:00.000Z", }, null, 2, ), "utf8", ); const recentTurns = await store.getRecentTurns( { actorKey: "actor-1", clientSessionId: "thread-1", projectKey: "project-1", sessionId: "thread-1", }, 5, ); expect(recentTurns).toHaveLength(1); expect(recentTurns[0]?.userMessage).toBe("帮我看一下当前异常。"); const transcript = await store.appendTurn( { actorKey: "actor-1", clientSessionId: "thread-1", projectKey: "project-1", sessionId: "thread-1", }, { assistantMessage: "已经定位到 3 条疑似异常支路。", toolCallCount: 2, userMessage: "继续分析这些支路。", }, ); expect(transcript.sessionId).toBe("thread-1"); expect(transcript.turns).toHaveLength(2); }); it("clones only the kept prefix when forking a thread", async () => { await store.appendTurn( { actorKey: "actor-2", clientSessionId: "thread-source", projectKey: "project-2", sessionId: "thread-source", }, { assistantMessage: "第一轮回复", toolCallCount: 0, userMessage: "第一轮提问", }, ); await store.appendTurn( { actorKey: "actor-2", clientSessionId: "thread-source", projectKey: "project-2", sessionId: "thread-source", }, { assistantMessage: "第二轮回复", toolCallCount: 0, userMessage: "第二轮提问", }, ); const cloned = await store.cloneThread( { actorKey: "actor-2", clientSessionId: "thread-source", projectKey: "project-2", sessionId: "thread-source", }, { actorKey: "actor-2", clientSessionId: "thread-fork", projectKey: "project-2", sessionId: "thread-fork", }, 2, ); expect(cloned.turns).toHaveLength(1); expect(cloned.turns[0]?.userMessage).toBe("第一轮提问"); const forkRecentTurns = await store.getRecentTurns( { actorKey: "actor-2", clientSessionId: "thread-fork", projectKey: "project-2", sessionId: "thread-fork", }, 5, ); expect(forkRecentTurns).toHaveLength(1); expect(forkRecentTurns[0]?.assistantMessage).toBe("第一轮回复"); }); });