import { describe, it, expect, beforeEach, vi } from "vitest"; import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; vi.mock("../src/logger.js", () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, })); import { registerReplayFunctions } from "../src/functions/replay.js"; import { KV } from "../src/state/schema.js"; function mockKV() { const store = new Map>(); const setCalls: Array<{ scope: string; key: string | undefined; value: any }> = []; return { get: async (scope: string, key: string): Promise => (store.get(scope)?.get(key) as T) ?? null, set: async (scope: string, key: string, value: T): Promise => { setCalls.push({ scope, key, value }); if (!store.has(scope)) store.set(scope, new Map()); // Mirror the engine: a state::set with key=undefined fails. We // surface this via setCalls so the test can assert key !== undefined. if (key === undefined) { throw new Error("missing field `key`"); } store.get(scope)!.set(key, value); return value; }, delete: async (scope: string, key: string) => { store.get(scope)?.delete(key); }, list: async (scope: string): Promise => Array.from(store.get(scope)?.values() ?? []) as T[], getSetCalls: () => setCalls, }; } function mockSdk(kv: ReturnType) { const fns = new Map(); return { registerFunction: (id: string, handler: Function) => fns.set(id, handler), registerTrigger: () => {}, trigger: async ( idOrInput: string | { function_id: string; payload?: unknown }, data?: unknown, ) => { const id = typeof idOrInput === "string" ? idOrInput : idOrInput.function_id; const payload = typeof idOrInput === "string" ? data : (idOrInput as any).payload; const fn = fns.get(id); if (!fn) return { success: true }; return fn(payload); }, _kv: kv, } as any; } describe("import-jsonl re-key on parsed.sessionId (#775)", () => { let tmpRoot: string; beforeEach(() => { tmpRoot = mkdtempSync(join(tmpdir(), "replay-import-key-")); }); function writeFixture(sessionId: string, ts = "2026-04-17T10:00:00.000Z") { const dir = join(tmpRoot, "proj"); rmSync(dir, { recursive: true, force: true }); require("node:fs").mkdirSync(dir, { recursive: true }); const lines = [ JSON.stringify({ type: "user", uuid: "u1", sessionId, timestamp: ts, cwd: tmpRoot, message: { role: "user", content: [{ type: "text", text: "hello" }], }, }), JSON.stringify({ type: "assistant", uuid: "a1", sessionId, timestamp: ts, message: { role: "assistant", content: [{ type: "text", text: "world" }], }, }), ]; writeFileSync(join(dir, `${sessionId}.jsonl`), lines.join("\n") + "\n"); } it("re-imports a session whose stored row is missing the `id` field without aborting the batch", async () => { writeFixture("sess-no-id"); const kv = mockKV(); const sdk = mockSdk(kv); registerReplayFunctions(sdk, kv as never); // Seed an existing session row that is MISSING `id` — the // pre-fix code would re-key on `existing.id` (undefined) and // throw `missing field \`key\``, aborting the whole import. await kv.set(KV.sessions, "sess-no-id", { project: "proj", cwd: tmpRoot, startedAt: "2026-04-17T09:00:00Z", endedAt: "2026-04-17T09:30:00Z", status: "completed", observationCount: 2, tags: [], }); const result = (await sdk.trigger("mem::replay::import-jsonl", { path: tmpRoot, })) as { success: boolean; imported?: number; error?: string }; expect(result.success).toBe(true); expect(result.imported).toBe(1); const undefinedKeyWrites = kv .getSetCalls() .filter((c) => c.scope === KV.sessions && c.key === undefined); expect(undefinedKeyWrites.length).toBe(0); const sessionWrites = kv .getSetCalls() .filter((c) => c.scope === KV.sessions && c.key === "sess-no-id"); expect(sessionWrites.length).toBeGreaterThan(0); // The handler also backfills the missing id field so future reads // are well-formed. expect((sessionWrites.at(-1)!.value as any).id).toBe("sess-no-id"); }); it("fresh import (no existing row) still writes session keyed by parsed.sessionId", async () => { writeFixture("sess-fresh"); const kv = mockKV(); const sdk = mockSdk(kv); registerReplayFunctions(sdk, kv as never); const result = (await sdk.trigger("mem::replay::import-jsonl", { path: tmpRoot, })) as { success: boolean; imported?: number }; expect(result.success).toBe(true); expect(result.imported).toBe(1); const sessionWrites = kv .getSetCalls() .filter((c) => c.scope === KV.sessions && c.key === "sess-fresh"); expect(sessionWrites.length).toBe(1); }); });