import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { SessionMetadataStore } from "../../src/sessions/metadataStore.js"; describe("SessionMetadataStore", () => { let tempDir: string; let store: SessionMetadataStore; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "tjwater-session-")); store = new SessionMetadataStore(tempDir); await store.initialize(); }); afterEach(async () => { await rm(tempDir, { force: true, recursive: true }); }); it("persists the provided opencode session id", async () => { const { record, created } = await store.ensure({ actorKey: "actor-1", projectId: "project-1", projectKey: "project-key-1", sessionId: "opencode-session-1", userId: "user-1", }); expect(created).toBe(true); expect(record.sessionId).toBe("opencode-session-1"); expect(record.ownerUserId).toBe("user-1"); expect(record.status).toBe("active"); }); it("touches metadata and preserves scoped ownership", async () => { const { record } = await store.ensure({ actorKey: "actor-2", projectId: "project-2", projectKey: "project-key-2", sessionId: "existing-session", userId: "user-2", }); const touched = await store.touch(record, { title: "新标题", }); expect(touched.title).toBe("新标题"); expect(touched.updatedAt >= record.updatedAt).toBe(true); const fetched = await store.get( { actorKey: "actor-2", projectId: "project-2", projectKey: "project-key-2", userId: "user-2", }, "existing-session", ); expect(fetched?.title).toBe("新标题"); }); it("does not expose a session across users or projects", async () => { await store.ensure({ actorKey: "actor-a", projectId: "project-a", projectKey: "project-key-a", sessionId: "private-session", userId: "user-a", }); expect( await store.get( { actorKey: "actor-b", projectId: "project-a", projectKey: "project-key-a", userId: "user-b", }, "private-session", ), ).toBeNull(); expect( await store.get( { actorKey: "actor-a", projectId: "project-b", projectKey: "project-key-b", userId: "user-a", }, "private-session", ), ).toBeNull(); }); it("rejects idempotent creation when the session belongs to another scope", async () => { await store.ensure({ actorKey: "actor-a", projectId: "project-a", projectKey: "project-key-a", sessionId: "claimed-session", userId: "user-a", }); await expect( store.ensure({ actorKey: "actor-b", projectId: "project-b", projectKey: "project-key-b", sessionId: "claimed-session", userId: "user-b", }), ).rejects.toThrow("session metadata ownership mismatch"); }); it("keeps long session ids with the same slug prefix distinct", async () => { const prefix = "s".repeat(64); const firstSessionId = `${prefix}-first`; const secondSessionId = `${prefix}-second`; const context = { actorKey: "actor-long", projectId: "project-long", projectKey: "project-key-long", userId: "user-long", }; await store.ensure({ ...context, sessionId: firstSessionId }); await store.ensure({ ...context, sessionId: secondSessionId }); expect((await store.get(context, firstSessionId))?.sessionId).toBe( firstSessionId, ); expect((await store.get(context, secondSessionId))?.sessionId).toBe( secondSessionId, ); }); });