|
|
|
@@ -20,11 +20,11 @@ describe("owned session workspaces", () => {
|
|
|
|
|
afterEach(async () => { await rm(base, { recursive: true, force: true }); });
|
|
|
|
|
|
|
|
|
|
it("keeps messages and searchable transcripts private to their user and scope", async () => {
|
|
|
|
|
const ui = new SessionUiStateStore(join(base, "old-ui"), records);
|
|
|
|
|
const transcript = new SessionTranscriptStore(join(base, "old-transcripts"), records);
|
|
|
|
|
const ui = new SessionUiStateStore(records);
|
|
|
|
|
const transcript = new SessionTranscriptStore(records);
|
|
|
|
|
await ui.write(owner, { sessionId: owner.sessionId, messages: [{ text: "private answer" }] });
|
|
|
|
|
await transcript.appendTurn(owner, { userMessage: "private question", assistantMessage: "private answer", toolCallCount: 0 });
|
|
|
|
|
expect((await new SessionUiStateStore(join(base, "old-ui"), records).read(owner))?.messages).toHaveLength(1);
|
|
|
|
|
expect((await new SessionUiStateStore(records).read(owner))?.messages).toHaveLength(1);
|
|
|
|
|
expect(await ui.read({ ...owner, actorKey: "actor-b" })).toBeNull();
|
|
|
|
|
expect(await ui.read({ ...owner, projectKey: "project-b" })).toBeNull();
|
|
|
|
|
expect(await transcript.search(owner, "private")).toHaveLength(2);
|
|
|
|
@@ -36,7 +36,7 @@ describe("owned session workspaces", () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("has a separate personal namespace for each user and separates it from projects", async () => {
|
|
|
|
|
const ui = new SessionUiStateStore(join(base, "old-ui"), records);
|
|
|
|
|
const ui = new SessionUiStateStore(records);
|
|
|
|
|
const personal = { ...owner, projectKey: "personal-actor-a" };
|
|
|
|
|
await ui.write(personal, { sessionId: owner.sessionId, messages: [{ text: "personal answer" }] });
|
|
|
|
|
expect(records.path(personal, "messages.json")).toContain("/actor-a/personal/");
|
|
|
|
@@ -60,13 +60,13 @@ describe("owned session workspaces", () => {
|
|
|
|
|
expect(await records.migrateLegacyRecords(options)).toEqual({ uiStates: 1, transcripts: 1, learningStates: 0, retainedUnownedUiStates: 1 });
|
|
|
|
|
expect(await readFile(records.path(owner, "messages.json"), "utf8")).toBe(messages);
|
|
|
|
|
expect(await readFile(records.path(owner, "transcript.json"), "utf8")).toBe(turns);
|
|
|
|
|
await new SessionUiStateStore(uiStateDir, records).write(owner, { sessionId: owner.sessionId, messages: [{ text: "new answer" }] });
|
|
|
|
|
await new SessionUiStateStore(records).write(owner, { sessionId: owner.sessionId, messages: [{ text: "new answer" }] });
|
|
|
|
|
expect((await records.migrateLegacyRecords(options)).uiStates).toBe(0);
|
|
|
|
|
const archive = join(sessionWorkspaceDirectory(records.backupRoot, owner), "chat");
|
|
|
|
|
const backup = (await readdir(archive)).find((name) => name.startsWith("messages-"))!;
|
|
|
|
|
expect(await readFile(join(archive, backup), "utf8")).toBe(messages);
|
|
|
|
|
expect(await readdir(uiStateDir)).toEqual(["orphan.json"]);
|
|
|
|
|
expect((await new SessionUiStateStore(uiStateDir, records).read(owner))?.messages).toEqual([{ text: "new answer" }]);
|
|
|
|
|
expect((await new SessionUiStateStore(records).read(owner))?.messages).toEqual([{ text: "new answer" }]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects traversal, linked records and mismatched ownership", async () => {
|
|
|
|
@@ -75,7 +75,7 @@ describe("owned session workspaces", () => {
|
|
|
|
|
const foreign = join(base, "foreign.json");
|
|
|
|
|
await writeFile(foreign, "private");
|
|
|
|
|
await symlink(foreign, records.path(owner, "messages.json"));
|
|
|
|
|
await expect(new SessionUiStateStore(join(base, "old-ui"), records).read(owner)).rejects.toThrow("symbolic link");
|
|
|
|
|
await expect(new SessionUiStateStore(records).read(owner)).rejects.toThrow("symbolic link");
|
|
|
|
|
await writeFile(join(sessionWorkspaceDirectory(records.root, owner), "chat", "owner.json"), JSON.stringify({ ...owner, actorKey: "actor-b" }));
|
|
|
|
|
await expect(records.ensure(owner)).rejects.toThrow("ownership mismatch");
|
|
|
|
|
});
|
|
|
|
@@ -114,8 +114,8 @@ describe("owned session workspaces", () => {
|
|
|
|
|
for (const path of [metadataDir, uiStateDir, transcriptDir, learningStateDir]) await mkdir(path);
|
|
|
|
|
const metadata = new SessionMetadataStore(metadataDir, records);
|
|
|
|
|
const { record } = await metadata.ensure(owner);
|
|
|
|
|
const ui = new SessionUiStateStore(uiStateDir, records);
|
|
|
|
|
const transcript = new SessionTranscriptStore(transcriptDir, records);
|
|
|
|
|
const ui = new SessionUiStateStore(records);
|
|
|
|
|
const transcript = new SessionTranscriptStore(records);
|
|
|
|
|
const learning = new SessionLearningStateStore(learningStateDir, records);
|
|
|
|
|
const legacyUi = JSON.stringify({ sessionId: owner.sessionId, messages: [{ text: "private content" }] });
|
|
|
|
|
await writeFile(join(uiStateDir, "chat.json"), legacyUi);
|
|
|
|
@@ -153,7 +153,7 @@ describe("owned session workspaces", () => {
|
|
|
|
|
const metadataDir = join(base, "metadata");
|
|
|
|
|
const metadata = new SessionMetadataStore(metadataDir, records);
|
|
|
|
|
const { record } = await metadata.ensure(owner);
|
|
|
|
|
const transcript = new SessionTranscriptStore(join(base, "legacy"), records);
|
|
|
|
|
const transcript = new SessionTranscriptStore(records);
|
|
|
|
|
await transcript.appendTurn(owner, { userMessage: "private question", assistantMessage: "private answer", toolCallCount: 0 });
|
|
|
|
|
const markDeleted = records.markDeleted.bind(records);
|
|
|
|
|
records.markDeleted = async () => { throw new Error("disk write interrupted"); };
|
|
|
|
@@ -163,7 +163,7 @@ describe("owned session workspaces", () => {
|
|
|
|
|
await expect(records.writeRecord(owner, "messages.json", { sessionId: owner.sessionId, messages: [] })).rejects.toThrow("session not found");
|
|
|
|
|
records = new SessionWorkspaceStore(records.root, records.backupRoot);
|
|
|
|
|
const restarted = new SessionMetadataStore(metadataDir, records);
|
|
|
|
|
expect(await new SessionTranscriptStore(join(base, "legacy"), records).search(owner, "private")).toEqual([]);
|
|
|
|
|
expect(await new SessionTranscriptStore(records).search(owner, "private")).toEqual([]);
|
|
|
|
|
await expect(restarted.ensure(owner)).rejects.toThrow("session not found");
|
|
|
|
|
await markDeleted(owner);
|
|
|
|
|
await restarted.remove(record);
|
|
|
|
|