2026-04-15 20:01:12 +08:00
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
vi.mock("@/lib/convex/enabled", () => ({
|
|
|
|
|
isConvexEnabled: vi.fn(() => true),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-04-23 07:38:34 +08:00
|
|
|
vi.mock("@/lib/auth/authContext", () => ({
|
|
|
|
|
HttpError: class HttpError extends Error {
|
|
|
|
|
status: number;
|
|
|
|
|
|
|
|
|
|
constructor(status: number, message: string) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.status = status;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
requireAuthContext: vi.fn(async () => ({
|
|
|
|
|
userId: "user_1",
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/lib/api-utils", () => ({
|
|
|
|
|
apiErrorResponse: vi.fn((message: string, status = 500, details?: unknown) => ({
|
|
|
|
|
message,
|
|
|
|
|
status,
|
|
|
|
|
details,
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-04-15 20:01:12 +08:00
|
|
|
vi.mock("@/lib/documents/page-command-adapter", () => ({
|
|
|
|
|
executeDocumentCreateChildBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
|
|
|
|
executeDocumentEmbedBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
|
|
|
|
executeDocumentTemplateBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
|
|
|
|
executeDocumentEmptyTrashBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
|
|
|
|
executeDocumentPurgeBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-04-23 07:38:34 +08:00
|
|
|
vi.mock("@/lib/documents/page-write-command-adapter", () => ({
|
|
|
|
|
executePageWriteBridgeCommand: vi.fn(async () => ({
|
|
|
|
|
requestId: "req_1",
|
|
|
|
|
traceId: "trace_1",
|
|
|
|
|
commandId: "cmd_1",
|
|
|
|
|
commandName: "page.command",
|
|
|
|
|
revision: 1,
|
|
|
|
|
conflictDetectionKey: "doc_1:1",
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/lib/documents/page-aggregate-loader", () => ({
|
|
|
|
|
loadPageAggregate: vi.fn(async () => ({
|
|
|
|
|
page: {
|
|
|
|
|
identity: { documentId: "doc_1", workspaceId: "ws_1" },
|
|
|
|
|
head: {
|
|
|
|
|
title: "页面标题",
|
|
|
|
|
updatedAt: null,
|
|
|
|
|
permissions: {
|
|
|
|
|
readOnly: false,
|
|
|
|
|
disableDownload: false,
|
|
|
|
|
disableCopy: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
layout: { pageOptions: { wideLayout: false } },
|
|
|
|
|
body: { content: null, revision: 0, conflictDetectionKey: "doc_1:0" },
|
|
|
|
|
tree: { pageSubtree: null },
|
|
|
|
|
stats: null,
|
|
|
|
|
},
|
|
|
|
|
bridge: {
|
|
|
|
|
requestId: "req_1",
|
|
|
|
|
traceId: "trace_1",
|
|
|
|
|
queryName: "documents.page.get",
|
|
|
|
|
},
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-04-15 20:01:12 +08:00
|
|
|
import { POST as postCreateChild } from "@/app/api/documents/create-child/route";
|
2026-04-26 04:29:23 +08:00
|
|
|
import { POST as postDelete } from "@/app/api/documents/delete/route";
|
2026-04-15 20:01:12 +08:00
|
|
|
import { POST as postEmbed } from "@/app/api/documents/embed/route";
|
|
|
|
|
import { POST as postTemplate } from "@/app/api/documents/template/route";
|
|
|
|
|
import { POST as postEmptyTrash } from "@/app/api/documents/empty-trash/route";
|
|
|
|
|
import { POST as postPurge } from "@/app/api/documents/purge/route";
|
2026-04-26 04:29:23 +08:00
|
|
|
import { POST as postRestore } from "@/app/api/documents/restore/route";
|
2026-04-23 07:38:34 +08:00
|
|
|
import { POST as postTitle } from "@/app/api/documents/title/route";
|
|
|
|
|
import { POST as postOptions } from "@/app/api/documents/options/route";
|
|
|
|
|
import { POST as postSave } from "@/app/api/documents/save/route";
|
2026-04-26 04:29:23 +08:00
|
|
|
import { POST as postCopyTree } from "@/app/api/documents/copy-tree/route";
|
2026-04-24 06:10:18 +08:00
|
|
|
import { POST as postCreate } from "@/app/api/documents/create/route";
|
|
|
|
|
import { POST as postMove } from "@/app/api/documents/move/route";
|
2026-04-23 07:38:34 +08:00
|
|
|
import { GET as getPage } from "@/app/api/documents/page/route";
|
2026-04-15 20:01:12 +08:00
|
|
|
import {
|
|
|
|
|
executeDocumentCreateChildBridgeCommand,
|
|
|
|
|
executeDocumentTemplateBridgeCommand,
|
|
|
|
|
executeDocumentEmptyTrashBridgeCommand,
|
|
|
|
|
} from "@/lib/documents/page-command-adapter";
|
2026-04-23 07:38:34 +08:00
|
|
|
import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter";
|
|
|
|
|
import { loadPageAggregate } from "@/lib/documents/page-aggregate-loader";
|
2026-04-15 20:01:12 +08:00
|
|
|
|
|
|
|
|
describe("documents route adapters", () => {
|
2026-04-24 06:10:18 +08:00
|
|
|
it("create route 作为 compat 壳委托 tree commands 主路径", async () => {
|
|
|
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
|
|
|
new Response(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
requestId: "req_tree_create_1",
|
|
|
|
|
traceId: "trace_tree_create_1",
|
|
|
|
|
result: {
|
|
|
|
|
action: "create",
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
documentId: "doc_new",
|
|
|
|
|
parentId: null,
|
|
|
|
|
title: "无标题",
|
|
|
|
|
sortOrder: 0,
|
|
|
|
|
updatedAt: "2026-04-23T00:00:00Z",
|
|
|
|
|
execution: {
|
|
|
|
|
access_scope: "private",
|
|
|
|
|
is_template: false,
|
|
|
|
|
created_at: "2026-04-23T00:00:00Z",
|
|
|
|
|
updated_at: "2026-04-23T00:00:00Z",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await postCreate(new Request("http://localhost/api/documents/create", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ parentId: null }),
|
|
|
|
|
}));
|
|
|
|
|
const payload = await response.json() as {
|
|
|
|
|
id: string;
|
|
|
|
|
workspace_id: string;
|
|
|
|
|
meta: { commandName: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
|
|
|
"http://localhost/api/tree/commands",
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: expect.any(Headers),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
action: "create",
|
|
|
|
|
parentId: null,
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(payload.id).toBe("doc_new");
|
|
|
|
|
expect(payload.workspace_id).toBe("ws_1");
|
|
|
|
|
expect(payload.meta.commandName).toBe("tree.node.create");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("move route 作为 compat 壳委托 tree commands 主路径", async () => {
|
|
|
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
|
|
|
new Response(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
requestId: "req_tree_move_1",
|
|
|
|
|
traceId: "trace_tree_move_1",
|
|
|
|
|
result: {
|
|
|
|
|
action: "move",
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
documentId: "doc_1",
|
|
|
|
|
parentId: "parent_1",
|
|
|
|
|
sortOrder: 2,
|
|
|
|
|
updatedAt: "2026-04-23T00:00:00Z",
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await postMove(new Request("http://localhost/api/documents/move", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ documentId: "doc_1", parentId: "parent_1", position: 2.7, workspaceId: "ws_1" }),
|
|
|
|
|
}));
|
|
|
|
|
const payload = await response.json() as {
|
|
|
|
|
ok: boolean;
|
|
|
|
|
meta: { commandName: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
|
|
|
"http://localhost/api/tree/commands",
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: expect.any(Headers),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
action: "move",
|
|
|
|
|
documentId: "doc_1",
|
|
|
|
|
parentId: "parent_1",
|
|
|
|
|
sortOrder: 2,
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(payload.ok).toBe(true);
|
|
|
|
|
expect(payload.meta.commandName).toBe("tree.subtree.move");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("title route 在树重命名兼容请求下委托 tree commands 主路径", async () => {
|
|
|
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
|
|
|
new Response(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
requestId: "req_tree_rename_1",
|
|
|
|
|
traceId: "trace_tree_rename_1",
|
|
|
|
|
result: {
|
|
|
|
|
action: "rename",
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
documentId: "doc_1",
|
|
|
|
|
title: "新标题",
|
|
|
|
|
updatedAt: "2026-04-23T00:00:00Z",
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await postTitle(new Request("http://localhost/api/documents/title", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", title: "新标题" }),
|
|
|
|
|
}));
|
|
|
|
|
const payload = await response.json() as {
|
|
|
|
|
ok: boolean;
|
|
|
|
|
meta: { commandName: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
|
|
|
"http://localhost/api/tree/commands",
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: expect.any(Headers),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
action: "rename",
|
|
|
|
|
documentId: "doc_1",
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
title: "新标题",
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(payload.ok).toBe(true);
|
|
|
|
|
expect(payload.meta.commandName).toBe("tree.node.rename");
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-15 20:01:12 +08:00
|
|
|
it("creates child route delegates to unified adapter", async () => {
|
|
|
|
|
await postCreateChild(new Request("http://localhost/api/documents/create-child", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ parentId: null }),
|
|
|
|
|
}));
|
|
|
|
|
expect(executeDocumentCreateChildBridgeCommand).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-26 04:29:23 +08:00
|
|
|
it("delete route 作为 compat 壳委托 tree commands 主路径", async () => {
|
|
|
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
|
|
|
new Response(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
requestId: "req_tree_archive_1",
|
|
|
|
|
traceId: "trace_tree_archive_1",
|
|
|
|
|
}),
|
|
|
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await postDelete(new Request("http://localhost/api/documents/delete", {
|
2026-04-15 20:01:12 +08:00
|
|
|
method: "POST",
|
2026-04-26 04:29:23 +08:00
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1" }),
|
|
|
|
|
}));
|
|
|
|
|
const payload = await response.json() as {
|
|
|
|
|
success: boolean;
|
|
|
|
|
meta: { commandName: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
|
|
|
"http://localhost/api/tree/commands",
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: expect.any(Headers),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
action: "archive",
|
|
|
|
|
documentId: "doc_1",
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(payload.success).toBe(true);
|
|
|
|
|
expect(payload.meta.commandName).toBe("tree.node.archive");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("restore route 作为 compat 壳委托 tree commands 主路径", async () => {
|
|
|
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
|
|
|
new Response(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
requestId: "req_tree_restore_1",
|
|
|
|
|
traceId: "trace_tree_restore_1",
|
|
|
|
|
}),
|
|
|
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await postRestore(new Request("http://localhost/api/documents/restore", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1" }),
|
|
|
|
|
}));
|
|
|
|
|
const payload = await response.json() as {
|
|
|
|
|
success: boolean;
|
|
|
|
|
meta: { commandName: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
|
|
|
"http://localhost/api/tree/commands",
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: expect.any(Headers),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
action: "restore",
|
|
|
|
|
documentId: "doc_1",
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(payload.success).toBe(true);
|
|
|
|
|
expect(payload.meta.commandName).toBe("tree.node.restore");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("embed route 作为 compat 壳委托 tree commands 主路径", async () => {
|
|
|
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
|
|
|
new Response(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
requestId: "req_tree_embed_1",
|
|
|
|
|
traceId: "trace_tree_embed_1",
|
|
|
|
|
}),
|
|
|
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await postEmbed(new Request("http://localhost/api/documents/embed", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "content-type": "application/json" },
|
2026-04-15 20:01:12 +08:00
|
|
|
body: JSON.stringify({ sourceId: "doc_1", targetId: "doc_2" }),
|
|
|
|
|
}));
|
2026-04-26 04:29:23 +08:00
|
|
|
const payload = await response.json() as {
|
|
|
|
|
ok: boolean;
|
|
|
|
|
meta: { commandName: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
|
|
|
"http://localhost/api/tree/commands",
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: expect.any(Headers),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
action: "embed",
|
|
|
|
|
sourceId: "doc_1",
|
|
|
|
|
targetId: "doc_2",
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(payload.ok).toBe(true);
|
|
|
|
|
expect(payload.meta.commandName).toBe("tree.node.embed");
|
2026-04-15 20:01:12 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("template route delegates to unified adapter", async () => {
|
|
|
|
|
await postTemplate(new Request("http://localhost/api/documents/template", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ documentId: "doc_1", isTemplate: true }),
|
|
|
|
|
}));
|
|
|
|
|
expect(executeDocumentTemplateBridgeCommand).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("empty trash route delegates to unified adapter", async () => {
|
|
|
|
|
await postEmptyTrash(new Request("http://localhost/api/documents/empty-trash", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ workspaceId: "ws_1" }),
|
|
|
|
|
}));
|
|
|
|
|
expect(executeDocumentEmptyTrashBridgeCommand).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-26 04:29:23 +08:00
|
|
|
it("purge route 作为 compat 壳委托 tree commands 主路径", async () => {
|
|
|
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
|
|
|
new Response(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
requestId: "req_tree_purge_1",
|
|
|
|
|
traceId: "trace_tree_purge_1",
|
|
|
|
|
result: {
|
|
|
|
|
purged: true,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await postPurge(new Request("http://localhost/api/documents/purge", {
|
2026-04-15 20:01:12 +08:00
|
|
|
method: "POST",
|
2026-04-26 04:29:23 +08:00
|
|
|
headers: { "content-type": "application/json" },
|
2026-04-15 20:01:12 +08:00
|
|
|
body: JSON.stringify({ documentId: "doc_1" }),
|
|
|
|
|
}));
|
2026-04-26 04:29:23 +08:00
|
|
|
const payload = await response.json() as {
|
|
|
|
|
success: boolean;
|
|
|
|
|
purged: boolean;
|
|
|
|
|
meta: { commandName: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
|
|
|
"http://localhost/api/tree/commands",
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: expect.any(Headers),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
action: "purge",
|
|
|
|
|
documentId: "doc_1",
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(payload.success).toBe(true);
|
|
|
|
|
expect(payload.purged).toBe(true);
|
|
|
|
|
expect(payload.meta.commandName).toBe("tree.node.purge");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("copy-tree route 作为 compat 壳委托 tree commands 主路径", async () => {
|
|
|
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
|
|
|
new Response(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
requestId: "req_tree_copy_1",
|
|
|
|
|
traceId: "trace_tree_copy_1",
|
|
|
|
|
result: {
|
|
|
|
|
items: [{ oldId: "doc_1", newId: "doc_2" }],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await postCopyTree(new Request("http://localhost/api/documents/copy-tree", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ targetParentId: null, items: [{ documentId: "doc_1", recursive: true }] }),
|
|
|
|
|
}));
|
|
|
|
|
const payload = await response.json() as {
|
|
|
|
|
items: Array<{ oldId: string; newId: string }>;
|
|
|
|
|
meta: { commandName: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
|
|
|
"http://localhost/api/tree/commands",
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: expect.any(Headers),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
action: "copy",
|
|
|
|
|
targetParentId: null,
|
|
|
|
|
items: [{ documentId: "doc_1", recursive: true }],
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(payload.items).toEqual([{ oldId: "doc_1", newId: "doc_2" }]);
|
|
|
|
|
expect(payload.meta.commandName).toBe("tree.subtree.copy");
|
2026-04-15 20:01:12 +08:00
|
|
|
});
|
2026-04-23 07:38:34 +08:00
|
|
|
|
|
|
|
|
it("page route delegates to unified aggregate loader", async () => {
|
|
|
|
|
const response = await getPage(
|
|
|
|
|
new Request("http://localhost/api/documents/page?documentId=doc_1&workspaceId=ws_1"),
|
|
|
|
|
);
|
|
|
|
|
const payload = await response.json() as {
|
|
|
|
|
page: { identity: { documentId: string } };
|
|
|
|
|
meta: { queryName: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(loadPageAggregate).toHaveBeenCalled();
|
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
|
expect(payload.page.identity.documentId).toBe("doc_1");
|
|
|
|
|
expect(payload.meta.queryName).toBe("documents.page.get");
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-24 06:10:18 +08:00
|
|
|
it("title route 在 page head 请求下仍委托 unified page write adapter", async () => {
|
2026-04-23 07:38:34 +08:00
|
|
|
await postTitle(new Request("http://localhost/api/documents/title", {
|
|
|
|
|
method: "POST",
|
2026-04-24 06:10:18 +08:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
documentId: "doc_1",
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
title: "新标题",
|
|
|
|
|
commandName: "page.head.updateTitle",
|
|
|
|
|
}),
|
2026-04-23 07:38:34 +08:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
expect(executePageWriteBridgeCommand).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("options route delegates to unified page write adapter", async () => {
|
|
|
|
|
await postOptions(new Request("http://localhost/api/documents/options", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", options: { wideLayout: true } }),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
expect(executePageWriteBridgeCommand).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("save route delegates to unified page write adapter", async () => {
|
|
|
|
|
await postSave(new Request("http://localhost/api/documents/save", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", content: [] }),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
expect(executePageWriteBridgeCommand).toHaveBeenCalled();
|
|
|
|
|
});
|
2026-04-15 20:01:12 +08:00
|
|
|
});
|