import { describe, expect, it, vi } from "vitest"; vi.mock("@/lib/convex/enabled", () => ({ isConvexEnabled: vi.fn(() => true), })); 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, })), })); 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 }))), })); 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", }, })), })); import { POST as postCreateChild } from "@/app/api/documents/create-child/route"; import { POST as postDelete } from "@/app/api/documents/delete/route"; 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"; import { POST as postRestore } from "@/app/api/documents/restore/route"; 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"; import { POST as postCopyTree } from "@/app/api/documents/copy-tree/route"; import { POST as postCreate } from "@/app/api/documents/create/route"; import { POST as postMove } from "@/app/api/documents/move/route"; import { GET as getPage } from "@/app/api/documents/page/route"; import { executeDocumentCreateChildBridgeCommand, executeDocumentTemplateBridgeCommand, executeDocumentEmptyTrashBridgeCommand, } from "@/lib/documents/page-command-adapter"; import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter"; import { loadPageAggregate } from "@/lib/documents/page-aggregate-loader"; describe("documents route adapters", () => { 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: { "authorization": "Bearer test-token", "content-type": "application/json", "cookie": "convex-auth=test-cookie", }, 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: "新标题", }), }), ); const renameCall = fetchMock.mock.calls.find(([, init]) => { if (!init || typeof init.body !== "string") { return false; } return init.body.includes('"action":"rename"'); }); const forwardedHeaders = renameCall?.[1]?.headers as Headers; expect(forwardedHeaders.get("authorization")).toBe("Bearer test-token"); expect(forwardedHeaders.get("cookie")).toBe("convex-auth=test-cookie"); expect(payload.ok).toBe(true); expect(payload.meta.commandName).toBe("tree.node.rename"); }); 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(); }); 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", { 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: "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" }, body: JSON.stringify({ sourceId: "doc_1", targetId: "doc_2" }), })); 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"); }); 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(); }); 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", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ documentId: "doc_1" }), })); 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"); }); 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"); }); it("title route 在 page head 请求下仍委托 unified page write adapter", async () => { await postTitle(new Request("http://localhost/api/documents/title", { method: "POST", body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", title: "新标题", commandName: "page.head.updateTitle", }), })); 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(); }); });