重构会话标题编辑和删除确认逻辑;重构历史会话时间记录

This commit is contained in:
2026-05-19 17:54:09 +08:00
parent 2fbfba118f
commit 4f54da64d0
6 changed files with 329 additions and 136 deletions
+142
View File
@@ -0,0 +1,142 @@
import type { ChatSessionRecord } from "./GlobalChatbox.types";
import {
createEmptyChatSession,
loadChatSessionById,
saveActiveChatState,
updateChatSessionTitle,
} from "./chatStorage";
type StoreName = "sessions" | "meta";
const stores: Record<StoreName, Map<string, any>> = {
sessions: new Map(),
meta: new Map(),
};
const mockDb = {
get: jest.fn(async (storeName: StoreName, key: string) => stores[storeName].get(key)),
getAll: jest.fn(async (storeName: StoreName) => Array.from(stores[storeName].values())),
put: jest.fn(async (storeName: StoreName, value: { id?: string; key?: string }) => {
const key = storeName === "sessions" ? value.id : value.key;
if (!key) {
throw new Error(`Missing key for store ${storeName}`);
}
stores[storeName].set(key, value);
return key;
}),
delete: jest.fn(async (storeName: StoreName, key: string) => {
stores[storeName].delete(key);
}),
};
jest.mock("idb", () => ({
openDB: jest.fn(async () => mockDb),
}));
describe("chatStorage timestamp semantics", () => {
let now = new Date("2026-05-19T09:00:00+08:00").getTime();
let dateNowSpy: jest.SpyInstance<number, []>;
beforeEach(() => {
stores.sessions.clear();
stores.meta.clear();
mockDb.get.mockClear();
mockDb.getAll.mockClear();
mockDb.put.mockClear();
mockDb.delete.mockClear();
window.localStorage.clear();
now = new Date("2026-05-19T09:00:00+08:00").getTime();
dateNowSpy = jest.spyOn(Date, "now").mockImplementation(() => now);
});
afterEach(() => {
dateNowSpy.mockRestore();
});
it("keeps anchor and content timestamps when reopening an old session", async () => {
const record: ChatSessionRecord = {
id: "old-session",
title: "很久之前的会话",
isTitleManuallyEdited: false,
createdAt: new Date("2026-04-01T10:00:00+08:00").getTime(),
updatedAt: new Date("2026-04-01T10:30:00+08:00").getTime(),
sessionId: "remote-1",
messages: [
{
id: "message-1",
role: "user",
content: "老问题",
branchRootId: "message-1",
},
],
branchGroups: [],
};
stores.sessions.set(record.id, record);
const loadedState = await loadChatSessionById(record.id);
now = new Date("2026-05-19T09:30:00+08:00").getTime();
await saveActiveChatState(loadedState);
expect(stores.sessions.get(record.id)).toMatchObject({
createdAt: record.createdAt,
updatedAt: record.updatedAt,
});
});
it("does not change timestamps when renaming a session", async () => {
const record: ChatSessionRecord = {
id: "rename-session",
title: "旧标题",
isTitleManuallyEdited: false,
createdAt: new Date("2026-04-10T08:00:00+08:00").getTime(),
updatedAt: new Date("2026-04-10T08:05:00+08:00").getTime(),
sessionId: "remote-2",
messages: [
{
id: "message-2",
role: "user",
content: "保留时间",
branchRootId: "message-2",
},
],
branchGroups: [],
};
stores.sessions.set(record.id, record);
now = new Date("2026-05-19T11:00:00+08:00").getTime();
await updateChatSessionTitle(record.id, "新标题", {
isTitleManuallyEdited: true,
});
expect(stores.sessions.get(record.id)).toMatchObject({
title: "新标题",
isTitleManuallyEdited: true,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
});
});
it("anchors createdAt to the first real message time for a new empty session", async () => {
const emptyState = await createEmptyChatSession();
const storageSessionId = emptyState.storageSessionId;
now = new Date("2026-05-19T09:05:00+08:00").getTime();
await saveActiveChatState({
...emptyState,
messages: [
{
id: "message-3",
role: "user",
content: "第一条消息",
branchRootId: "message-3",
},
],
sessionId: "remote-3",
});
expect(stores.sessions.get(storageSessionId!)).toMatchObject({
createdAt: now,
updatedAt: now,
});
});
});