140 lines
3.8 KiB
TypeScript
140 lines
3.8 KiB
TypeScript
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,
|
|
);
|
|
});
|
|
});
|