Files
homelab/chat/lib/session-transfer.test.mjs
T

82 lines
5.3 KiB
JavaScript

import {describe, expect, test} from "bun:test";
import {
currentPath,
detectSessionTransferFormat,
parseSessionTransfer,
serializeSessionTransfer,
serializeXitengArchive
} from "./session-transfer.ts";
const lines = (values) => `${values.map((value) => JSON.stringify(value)).join("\n")}\n`;
describe("session transfer formats", () => {
test("imports Codex rollout records without duplicate event messages", () => {
const text = lines([
{type: "session_meta", timestamp: "2026-08-13T00:00:00Z", payload: {id: "11111111-1111-4111-8111-111111111111", timestamp: "2026-08-13T00:00:00Z", cwd: "/tmp"}},
{type: "turn_context", timestamp: "2026-08-13T00:00:00Z", payload: {model: "gpt-5.6-sol"}},
{type: "response_item", timestamp: "2026-08-13T00:00:01Z", payload: {type: "message", role: "user", content: [{type: "input_text", text: "hello"}]}},
{type: "event_msg", timestamp: "2026-08-13T00:00:01Z", payload: {type: "user_message", message: "hello"}},
{type: "response_item", timestamp: "2026-08-13T00:00:02Z", payload: {type: "message", role: "assistant", content: [{type: "output_text", text: "hi"}]}},
{type: "event_msg", timestamp: "2026-08-13T00:00:02Z", payload: {type: "agent_message", message: "hi"}}
]);
const parsed = parseSessionTransfer(text, "rollout.jsonl");
expect(parsed.format).toBe("codex");
expect(parsed.nodes.map((node) => node.role)).toEqual(["user", "assistant"]);
expect(parsed.sessions[0].model).toBe("gpt-5.6-sol");
});
test("preserves Claude parent branches", () => {
const text = lines([
{type: "user", sessionId: "s1", uuid: "u1", parentUuid: null, timestamp: "2026-08-13T00:00:00Z", message: {role: "user", content: "question"}},
{type: "assistant", sessionId: "s1", uuid: "a1", parentUuid: "u1", timestamp: "2026-08-13T00:00:01Z", message: {role: "assistant", model: "claude-sonnet-4-5", content: [{type: "text", text: "first"}]}},
{type: "assistant", sessionId: "s1", uuid: "a2", parentUuid: "u1", timestamp: "2026-08-13T00:00:02Z", message: {role: "assistant", model: "claude-sonnet-4-5", content: [{type: "text", text: "second"}]}},
{type: "custom-title", sessionId: "s1", customTitle: "branched"}
]);
const parsed = parseSessionTransfer(text, "claude.jsonl");
expect(parsed.nodes.map((node) => node.parentSourceId)).toEqual([null, "u1", "u1"]);
expect(parsed.sessions[0].headSourceId).toBe("a2");
expect(parsed.sessions[0].name).toBe("branched");
});
test("imports current OMP title slot and tree", () => {
const text = lines([
{type: "title", v: 1, title: "OMP task", updatedAt: "2026-08-13T00:00:00Z", pad: ""},
{type: "session", version: 3, id: "omp-1", timestamp: "2026-08-13T00:00:00Z", cwd: "/tmp"},
{type: "message", id: "u1", parentId: null, timestamp: "2026-08-13T00:00:01Z", message: {role: "user", content: [{type: "text", text: "hello"}]}},
{type: "message", id: "a1", parentId: "u1", timestamp: "2026-08-13T00:00:02Z", message: {role: "assistant", content: [{type: "text", text: "hi"}]}}
]);
const parsed = parseSessionTransfer(text, "omp.jsonl");
expect(parsed.sessions[0].name).toBe("OMP task");
expect(currentPath(parsed.nodes, "a1").map((node) => node.sourceId)).toEqual(["u1", "a1"]);
});
test("exports parseable Codex, Claude and OMP JSONL", () => {
const document = {
format: "xiteng",
sessions: [{sourceId: "22222222-2222-4222-8222-222222222222", name: "round trip", headSourceId: "a1", providerId: "openai", model: "gpt-5.6", generationSettings: {reasoning: "auto", showReasoningSummary: false, temperature: null, maxOutputTokens: null}, createdAt: "2026-08-13T00:00:00Z", updatedAt: "2026-08-13T00:00:02Z"}],
nodes: [
{sourceId: "u1", parentSourceId: null, role: "user", parts: [{type: "text", text: "hello"}], createdAt: "2026-08-13T00:00:01Z", completedAt: "2026-08-13T00:00:01Z"},
{sourceId: "a1", parentSourceId: "u1", role: "assistant", parts: [{type: "reasoning", text: "brief"}, {type: "text", text: "hi"}], createdAt: "2026-08-13T00:00:02Z", completedAt: "2026-08-13T00:00:02Z"}
]
};
for (const format of ["codex", "claude", "omp"]) {
const exported = serializeSessionTransfer(document, format);
expect(detectSessionTransferFormat(exported)).toBe(format);
const imported = parseSessionTransfer(exported);
expect(imported.nodes.some((node) => node.parts.some((part) => part.type === "text" && part.text === "hi"))).toBe(true);
}
});
test("native archive keeps graph objects and working items", () => {
const text = serializeXitengArchive([
{id: "c1", name: "native", headMessageId: "m1", providerId: "p", model: "m", messageCount: 1, createdAt: "2026-08-13T00:00:00Z", updatedAt: "2026-08-13T00:00:01Z", generationSettings: {reasoning: "auto", showReasoningSummary: false, temperature: null, maxOutputTokens: null}, messages: []}
], [
{id: "m1", parentMessageId: null, role: "user", parts: [{type: "text", text: "draft"}], origin: {type: "user"}, completion: {status: "complete"}, createdAt: "2026-08-13T00:00:00Z", completedAt: "2026-08-13T00:00:00Z"}
], []);
const parsed = parseSessionTransfer(text, "backup.xiteng-chat.json");
expect(parsed.format).toBe("xiteng");
expect(parsed.sessions).toHaveLength(1);
expect(parsed.nodes).toHaveLength(1);
});
});