2026-04-14 13:22:29 +08:00
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
|
import {
|
|
|
|
|
compareDocumentCanonicalOrder,
|
|
|
|
|
getCanonicalDocumentByBusinessId,
|
|
|
|
|
getCanonicalParentDocumentId,
|
2026-04-26 04:29:23 +08:00
|
|
|
pickCanonicalDocumentRecordsByBusinessId,
|
2026-04-14 13:22:29 +08:00
|
|
|
pickCanonicalDocumentRecord,
|
|
|
|
|
} from "../../../convex/_utils/documentRecord";
|
|
|
|
|
|
|
|
|
|
describe("pickCanonicalDocumentRecord", () => {
|
|
|
|
|
it("同一 business id 出现重复记录时优先选择最新未删除记录", () => {
|
|
|
|
|
const selected = pickCanonicalDocumentRecord([
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_old",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:01.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_deleted",
|
|
|
|
|
deleted_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
created_at: "2026-04-14T00:00:02.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_new",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:02.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:04.000Z",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(selected?._id).toBe("doc_new");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("全部已删除时仍稳定选择最新记录,避免 first() 命中漂移", () => {
|
|
|
|
|
const selected = pickCanonicalDocumentRecord([
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_deleted_old",
|
|
|
|
|
deleted_at: "2026-04-14T00:00:01.000Z",
|
|
|
|
|
created_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:01.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_deleted_new",
|
|
|
|
|
deleted_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
created_at: "2026-04-14T00:00:02.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(selected?._id).toBe("doc_deleted_new");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("时间戳相同时用 _id 稳定打破平手,避免 collect 后结果不稳定", () => {
|
|
|
|
|
const selected = pickCanonicalDocumentRecord([
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_b",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_a",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(selected?._id).toBe("doc_a");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("compareDocumentCanonicalOrder", () => {
|
|
|
|
|
it("未删除记录始终排在已删除记录前面,供页面权限/内容读链复用", () => {
|
|
|
|
|
const records = [
|
|
|
|
|
{
|
|
|
|
|
_id: "deleted",
|
|
|
|
|
deleted_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
created_at: "2026-04-14T00:00:02.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "alive",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:01.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:01.000Z",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const sorted = [...records].sort(compareDocumentCanonicalOrder);
|
|
|
|
|
expect(sorted.map((record) => record._id)).toEqual(["alive", "deleted"]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("canonical document helper", () => {
|
|
|
|
|
const createCtx = (records: Array<Record<string, unknown>>) => ({
|
|
|
|
|
db: {
|
|
|
|
|
query: () => ({
|
|
|
|
|
withIndex: () => ({
|
|
|
|
|
collect: async () => records,
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("按 business id 查询时返回 canonical 记录,避免高风险读链命中旧记录", async () => {
|
|
|
|
|
const ctx = createCtx([
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_old",
|
|
|
|
|
id: "doc_1",
|
|
|
|
|
parent_id: "parent_old",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:01.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_new",
|
|
|
|
|
id: "doc_1",
|
|
|
|
|
parent_id: "parent_new",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:02.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, "doc_1");
|
|
|
|
|
expect(doc?._id).toBe("doc_new");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("父链辅助函数返回 canonical 父页面 id,供共享/评论祖先扫描复用", async () => {
|
|
|
|
|
const ctx = createCtx([
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_old",
|
|
|
|
|
id: "doc_1",
|
|
|
|
|
parent_id: "parent_old",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:01.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_new",
|
|
|
|
|
id: "doc_1",
|
|
|
|
|
parent_id: "parent_new",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:02.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const parentId = await getCanonicalParentDocumentId(ctx, "doc_1");
|
|
|
|
|
expect(parentId).toBe("parent_new");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("父链辅助函数命中已删除旧记录时仍回退到最新未删除父级,供收藏祖先权限复用", async () => {
|
|
|
|
|
const ctx = createCtx([
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_deleted",
|
|
|
|
|
id: "doc_2",
|
|
|
|
|
parent_id: "parent_deleted",
|
|
|
|
|
deleted_at: "2026-04-14T00:00:04.000Z",
|
|
|
|
|
created_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:04.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_alive",
|
|
|
|
|
id: "doc_2",
|
|
|
|
|
parent_id: "parent_alive",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:01.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const parentId = await getCanonicalParentDocumentId(ctx, "doc_2");
|
|
|
|
|
expect(parentId).toBe("parent_alive");
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-26 04:29:23 +08:00
|
|
|
|
|
|
|
|
describe("pickCanonicalDocumentRecordsByBusinessId", () => {
|
|
|
|
|
it("同一 workspace 扫描结果里 business id 重复时应先折叠成 canonical 记录,供树命令写链复用", () => {
|
|
|
|
|
const records = pickCanonicalDocumentRecordsByBusinessId([
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_old",
|
|
|
|
|
id: "doc_1",
|
|
|
|
|
parent_id: "parent_old",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:00.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:01.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_new",
|
|
|
|
|
id: "doc_1",
|
|
|
|
|
parent_id: "parent_new",
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:02.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:03.000Z",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
_id: "doc_2",
|
|
|
|
|
id: "doc_2",
|
|
|
|
|
parent_id: null,
|
|
|
|
|
deleted_at: null,
|
|
|
|
|
created_at: "2026-04-14T00:00:04.000Z",
|
|
|
|
|
updated_at: "2026-04-14T00:00:05.000Z",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(records).toHaveLength(2);
|
|
|
|
|
expect(records.map((record) => record._id)).toEqual(["doc_new", "doc_2"]);
|
|
|
|
|
});
|
|
|
|
|
});
|