import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { MsgContext } from "../../auto-reply/templating.js"; import { clearSessionStoreCacheForTest, loadSessionStore, recordSessionMetaFromInbound, updateLastRoute, } from "../sessions.js"; const CANONICAL_KEY = "agent:main:webchat:dm:mixed-user"; const MIXED_CASE_KEY = "Agent:Main:WebChat:DM:MiXeD-User"; function createInboundContext(): MsgContext { return { Provider: "webchat", Surface: "webchat", ChatType: "direct", From: "WebChat:User-1", To: "webchat:agent", SessionKey: MIXED_CASE_KEY, OriginatingTo: "webchat:user-1", }; } describe("session store key normalization", () => { let tempDir = ""; let storePath = ""; beforeEach(async () => { tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-key-normalize-")); storePath = path.join(tempDir, "sessions.json"); await fs.writeFile(storePath, "{}", "utf-8"); }); afterEach(async () => { clearSessionStoreCacheForTest(); if (tempDir) { await fs.rm(tempDir, { recursive: true, force: true }); } }); it("records inbound metadata under a canonical lowercase key", async () => { await recordSessionMetaFromInbound({ storePath, sessionKey: MIXED_CASE_KEY, ctx: createInboundContext(), }); const store = loadSessionStore(storePath, { skipCache: true }); expect(Object.keys(store)).toEqual([CANONICAL_KEY]); expect(store[CANONICAL_KEY]?.origin?.provider).toBe("webchat"); }); it("does not create a duplicate mixed-case key when last route is updated", async () => { await recordSessionMetaFromInbound({ storePath, sessionKey: CANONICAL_KEY, ctx: createInboundContext(), }); await updateLastRoute({ storePath, sessionKey: MIXED_CASE_KEY, channel: "webchat", to: "webchat:user-1", }); const store = loadSessionStore(storePath, { skipCache: true }); expect(Object.keys(store)).toEqual([CANONICAL_KEY]); expect(store[CANONICAL_KEY]).toEqual( expect.objectContaining({ lastChannel: "webchat", lastTo: "webchat:user-1", }), ); }); it("migrates legacy mixed-case entries to the canonical key on update", async () => { await fs.writeFile( storePath, JSON.stringify( { [MIXED_CASE_KEY]: { sessionId: "legacy-session", updatedAt: 1, chatType: "direct", channel: "webchat", }, }, null, 2, ), "utf-8", ); clearSessionStoreCacheForTest(); await updateLastRoute({ storePath, sessionKey: CANONICAL_KEY, channel: "webchat", to: "webchat:user-2", }); const store = loadSessionStore(storePath, { skipCache: true }); expect(store[CANONICAL_KEY]?.sessionId).toBe("legacy-session"); expect(store[MIXED_CASE_KEY]).toBeUndefined(); }); it("preserves updatedAt when recording inbound metadata for an existing session", async () => { await fs.writeFile( storePath, JSON.stringify( { [CANONICAL_KEY]: { sessionId: "existing-session", updatedAt: 1111, chatType: "direct", channel: "webchat", origin: { provider: "webchat", chatType: "direct", from: "WebChat:User-1", to: "webchat:user-1", }, }, }, null, 2, ), "utf-8", ); clearSessionStoreCacheForTest(); await recordSessionMetaFromInbound({ storePath, sessionKey: CANONICAL_KEY, ctx: createInboundContext(), }); const store = loadSessionStore(storePath, { skipCache: true }); expect(store[CANONICAL_KEY]?.sessionId).toBe("existing-session"); expect(store[CANONICAL_KEY]?.updatedAt).toBe(1111); expect(store[CANONICAL_KEY]?.origin?.provider).toBe("webchat"); }); });