import { beforeEach, describe, expect, it, vi } from "vitest"; import { TREE_3000_ROUTE_BOUNDARY_MANIFEST } from "@/lib/tree-route-boundary"; const mockIsConvexEnabled = vi.fn(() => true); const mockGetAuthedConvexClient = vi.fn(); const mockBuildDocumentBridgeContext = vi.fn(); const mockBuildDocumentCommandEnvelope = vi.fn(); const mockResolveRustBridgeCommandPlan = vi.fn(); const mockExecuteRustBridgeMutationTransport = vi.fn(); const mockRecordRustBridgeCommandArtifacts = vi.fn(); const mockRecordBridgeCommandArtifacts = vi.fn(); const mockRecordBridgeCommandFailureArtifacts = vi.fn(); const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) => Response.json( { error: error instanceof Error ? error.message : String(error), }, { status: typeof (error as { status?: unknown })?.status === "number" ? ((error as { status: number }).status ?? 500) : 500, }, ), ); const mockEnsureDocumentScaffold = vi.fn(); const mockCopyMindmapFilesIfExists = vi.fn(); const mockLoadSidebarDataFromConvex = vi.fn(); vi.mock("@/lib/convex/enabled", () => ({ isConvexEnabled: () => mockIsConvexEnabled(), })); vi.mock("@/lib/convex/api", () => ({ api: { documents: { getMeta: "documents:getMeta", getContent: "documents:getContent", }, workspaces: { ensureDefaultWorkspace: "workspaces:ensureDefaultWorkspace", }, }, })); vi.mock("@/lib/convex/route", () => ({ getAuthedConvexClient: mockGetAuthedConvexClient, })); vi.mock("@/lib/documents/bridge", () => ({ assertDocumentId: (value: string | null | undefined) => { const normalized = typeof value === "string" ? value.trim() : ""; if (!normalized) { throw new Error("缺少 documentId"); } return normalized; }, assertTitle: (value: string | null | undefined) => { const normalized = typeof value === "string" ? value.trim() : ""; if (!normalized) { throw new Error("缺少标题"); } return normalized; }, buildDocumentBridgeContext: mockBuildDocumentBridgeContext, buildDocumentCommandEnvelope: mockBuildDocumentCommandEnvelope, documentBridgeErrorResponse: (...args: Parameters) => mockDocumentBridgeErrorResponse(...args), })); vi.mock("@/lib/documents/rust-runtime", () => ({ resolveRustBridgeCommandPlan: (...args: Parameters) => mockResolveRustBridgeCommandPlan(...args), executeRustBridgeMutationTransport: (...args: Parameters) => mockExecuteRustBridgeMutationTransport(...args), recordRustBridgeCommandArtifacts: (...args: Parameters) => mockRecordRustBridgeCommandArtifacts(...args), readRustTreeDomainEventType: (plan: { argsJson?: Record }) => { const eventPlan = plan.argsJson?.domainEventPlan as | { family?: string; eventType?: string; } | undefined; if (eventPlan?.family === "tree" && typeof eventPlan.eventType === "string") { return eventPlan.eventType; } const hint = plan.argsJson?.domainEventHint as | { family?: string; eventType?: string; } | undefined; return hint?.family === "tree" && typeof hint.eventType === "string" ? hint.eventType : null; }, materializeRustTreeDomainEventPlan: (input: { plan: { argsJson?: Record }; streamDelta?: Record | null; }) => { const eventPlan = input.plan.argsJson?.domainEventPlan as | { family?: string; schema?: string; schemaVersion?: number; eventType?: string; } | undefined; if ( eventPlan?.family !== "tree" || eventPlan.schema !== "mnote.tree.domain_event" || eventPlan.schemaVersion !== 1 || typeof eventPlan.eventType !== "string" ) { return null; } return { family: "tree", schema: "mnote.tree.domain_event", schemaVersion: 1, eventType: eventPlan.eventType, ...(input.streamDelta ? { streamDelta: input.streamDelta } : {}), }; }, materializeRustTreeStreamDelta: (input: { plan: { argsJson?: Record }; result: unknown }) => { const hint = input.plan.argsJson?.streamDeltaHint as | { family?: string; kind?: string; args?: Record; } | undefined; if (hint?.family !== "tree" || !hint.kind) return null; const args = hint.args ?? {}; const result = input.result as Record; if (hint.kind === "document_result") { return result.document ? { op: "upsert_document", document: result.document } : null; } if (hint.kind === "upsert_document_patch") { return { op: "upsert_document", document: { id: args.documentId, ...(args.patch as Record), updated_at: result.updated_at ?? null, }, }; } if (hint.kind === "move_document") { return { op: "move_document", documentId: args.documentId, parentId: result.parent_id ?? args.parentId ?? null, sortOrder: result.sort_order ?? args.sortOrder, updatedAt: result.updated_at, }; } if (hint.kind === "remove_document") { return { op: "remove_document", documentId: args.documentId }; } if (hint.kind === "noop") { return { op: "noop" }; } if (hint.kind === "copy_result") { return { op: "upsert_documents", upsertDocuments: Array.isArray(result.items) ? result.items.map((item) => item?.document).filter(Boolean) : [], }; } return null; }, })); vi.mock("@/lib/documents/bridge-log", () => ({ recordBridgeCommandArtifacts: (...args: unknown[]) => mockRecordBridgeCommandArtifacts(...args), recordBridgeCommandFailureArtifacts: (...args: unknown[]) => mockRecordBridgeCommandFailureArtifacts(...args), })); vi.mock("@/lib/documents/page-lifecycle-side-effects", () => ({ ensureDocumentScaffold: (...args: unknown[]) => mockEnsureDocumentScaffold(...args), copyMindmapFilesIfExists: (...args: unknown[]) => mockCopyMindmapFilesIfExists(...args), })); vi.mock("@/lib/server/sidebar-data", () => ({ loadSidebarDataFromConvex: (...args: unknown[]) => mockLoadSidebarDataFromConvex(...args), })); describe("/api/tree/commands route", () => { beforeEach(() => { mockIsConvexEnabled.mockReturnValue(true); mockGetAuthedConvexClient.mockReset(); mockBuildDocumentBridgeContext.mockReset(); mockBuildDocumentCommandEnvelope.mockReset(); mockResolveRustBridgeCommandPlan.mockReset(); mockExecuteRustBridgeMutationTransport.mockReset(); mockRecordRustBridgeCommandArtifacts.mockReset().mockResolvedValue(null); mockRecordBridgeCommandArtifacts.mockReset(); mockRecordBridgeCommandFailureArtifacts.mockReset(); mockEnsureDocumentScaffold.mockReset(); mockCopyMindmapFilesIfExists.mockReset(); mockLoadSidebarDataFromConvex.mockReset(); mockDocumentBridgeErrorResponse.mockClear(); }); it("create action 走 tree.node.create,并保留本地 scaffold 副作用", async () => { expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.routes).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "tree.commands", role: "next-thin-proxy", }), ]), ); expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.rustOwnedSemantics).toContain("tree.node.create"); expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.browserSubstrateDuties).toContain( "新页面 scaffold 文件创建", ); const client = { mutation: vi.fn(async () => ({ activeWorkspaceId: "ws_root" })), query: vi.fn(), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_1", traceId: "trace_tree_1", workspaceId: "ws_root", actor: { actorType: "user", actorId: "user_1", sessionId: null, }, source: { channel: "next-route", client: "wolai-frontend", }, tenantId: null, deploymentId: null, projectId: null, authToken: null, idempotencyKey: null, validateOnly: false, dryRun: false, }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockResolvedValue({ kind: "command", commandName: "tree.node.create", commandId: "cmd_tree_create_1", functionName: "documents:createWithParentReference", workspaceId: "ws_root", requestId: "req_tree_1", traceId: "trace_tree_1", actorId: "user_1", idempotencyKey: null, payloadJson: "{}", argsJson: { streamDeltaHint: { family: "tree", kind: "document_result", args: { documentField: "document" }, }, domainEventHint: { family: "tree", eventType: "tree.node.created", }, }, }); mockExecuteRustBridgeMutationTransport.mockResolvedValue({ id: "doc_new", title: "无标题", parent_id: null, sort_order: 0, workspace_id: "ws_root", access_scope: "private", is_template: false, created_at: "2026-04-23T00:00:00Z", updated_at: "2026-04-23T00:00:00Z", document: { id: "doc_new", workspace_id: "ws_root", title: "无标题", parent_id: null, sort_order: 0, is_starred: false, access_scope: "private", is_template: false, created_at: "2026-04-23T00:00:00Z", updated_at: "2026-04-23T00:00:00Z", }, }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "create", parentId: null, }), }), ); expect(response.status).toBe(200); const payload = await response.json() as { result: { action: string; documentId: string; workspaceId: string; title: string; }; }; expect(payload.result.action).toBe("create"); expect(payload.result.documentId).toBe("doc_new"); expect(payload.result.workspaceId).toBe("ws_root"); expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( expect.objectContaining({ name: "tree.node.create", payload: expect.objectContaining({ workspaceId: "ws_root", parentId: null, title: "无标题", accessScope: "private", }), }), ); expect(mockEnsureDocumentScaffold).toHaveBeenCalledWith("doc_new", "无标题"); expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( expect.objectContaining({ context: expect.objectContaining({ requestId: "req_tree_1", traceId: "trace_tree_1", }), envelope: expect.objectContaining({ name: "tree.node.create", }), plan: expect.objectContaining({ commandName: "tree.node.create", functionName: "documents:createWithParentReference", }), result: expect.objectContaining({ id: "doc_new", workspace_id: "ws_root", }), }), ); expect(mockRecordBridgeCommandArtifacts).not.toHaveBeenCalled(); }); it("move action 走 tree.subtree.move,并把 position 归一化为 sortOrder", async () => { let docMetaReadCount = 0; const client = { mutation: vi.fn(), query: vi.fn(async (name: string, args: { id: string }) => { if (name === "documents:getMeta" && args.id === "doc_1") { docMetaReadCount += 1; return { id: "doc_1", workspace_id: "ws_1", parent_id: docMetaReadCount === 1 ? null : "parent_1", sort_order: docMetaReadCount === 1 ? 4 : 1, updated_at: docMetaReadCount === 1 ? "2026-04-23T00:00:00Z" : "2026-04-23T00:01:00Z", }; } if (name === "documents:getMeta" && args.id === "parent_1") { return { id: "parent_1", workspace_id: "ws_1", parent_id: null, }; } return null; }), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_2", traceId: "trace_tree_2", workspaceId: "ws_1", actor: { actorType: "user", actorId: "user_1", sessionId: null, }, source: { channel: "next-route", client: "wolai-frontend", }, tenantId: null, deploymentId: null, projectId: null, authToken: null, idempotencyKey: null, validateOnly: false, dryRun: false, }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockResolvedValue({ kind: "command", commandName: "tree.subtree.move", commandId: "cmd_tree_move_1", functionName: "documents:move", workspaceId: "ws_1", requestId: "req_tree_2", traceId: "trace_tree_2", actorId: "user_1", idempotencyKey: null, payloadJson: "{}", argsJson: { streamDeltaHint: { family: "tree", kind: "move_document", args: { documentId: "doc_1", parentId: "parent_1", sortOrder: 2, }, }, domainEventHint: { family: "tree", eventType: "tree.subtree.moved", }, }, }); mockExecuteRustBridgeMutationTransport.mockResolvedValue({ ok: true, parent_id: "parent_1", sort_order: 1, workspace_id: "ws_1", updated_at: "2026-04-23T00:01:00Z", }); mockLoadSidebarDataFromConvex.mockResolvedValue({ sidebarInitialData: { activeWorkspaceId: "ws_1", workspaces: [], documents: [ { id: "doc_1", workspace_id: "ws_1", parent_id: null, }, { id: "parent_1", workspace_id: "ws_1", parent_id: null, }, ], rootDocuments: [], sidebarTree: [], trashedDocuments: [], mediaAssets: [], trashedMediaAssets: [], mindmapDocs: [], mindmapAssets: [], trashedMindmapAssets: [], mindmapAssetChildren: {}, tableAssets: [], trashedTableAssets: [], kernelSidebarProjection: { projectionId: "kernel_projection:sidebar_tree:workspace_root", projection: "sidebar_tree", rootNodeId: null, items: [], edges: [], }, kernelFileTreeProjection: { projectionId: "kernel_projection:file_tree:workspace_root", projection: "file_tree", rootNodeId: null, items: [], edges: [], }, }, }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "move", documentId: "doc_1", parentId: "parent_1", sortOrder: 2.9, workspaceId: "ws_1", }), }), ); expect(response.status).toBe(200); const payload = await response.json() as { result: { action: string; documentId: string; parentId: string | null; sortOrder: number | null; }; }; expect(payload.result).toMatchObject({ action: "move", documentId: "doc_1", parentId: "parent_1", sortOrder: 1, workspaceId: "ws_1", }); expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( expect.objectContaining({ name: "tree.subtree.move", preflightData: { documents: [ { id: "doc_1", workspace_id: "ws_1", parent_id: null, }, { id: "parent_1", workspace_id: "ws_1", parent_id: null, }, ], }, payload: expect.objectContaining({ documentId: "doc_1", parentId: "parent_1", sortOrder: 2, }), }), ); expect(mockEnsureDocumentScaffold).not.toHaveBeenCalled(); expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( expect.objectContaining({ envelope: expect.objectContaining({ name: "tree.subtree.move", }), plan: expect.objectContaining({ commandName: "tree.subtree.move", functionName: "documents:move", }), result: expect.objectContaining({ parent_id: "parent_1", sort_order: 1, }), }), ); }); it("move action 应优先使用 mutation 返回的 canonical 结果,而不是再次回读页面元数据", async () => { const client = { mutation: vi.fn(), query: vi.fn(async (name: string, args: { id: string }) => { if (name === "documents:getMeta" && args.id === "doc_1") { return { id: "doc_1", workspace_id: "ws_1", parent_id: "parent_old", sort_order: 4, updated_at: "2026-04-23T00:00:00Z", }; } if (name === "documents:getMeta" && args.id === "parent_1") { return { id: "parent_1", workspace_id: "ws_1", parent_id: null, }; } return null; }), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_2b", traceId: "trace_tree_2b", workspaceId: "ws_1", actor: { actorType: "user", actorId: "user_1", sessionId: null, }, source: { channel: "next-route", client: "wolai-frontend", }, tenantId: null, deploymentId: null, projectId: null, authToken: null, idempotencyKey: null, validateOnly: false, dryRun: false, }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockResolvedValue({ kind: "command", commandName: "tree.subtree.move", commandId: "cmd_tree_move_2", functionName: "documents:move", workspaceId: "ws_1", requestId: "req_tree_2b", traceId: "trace_tree_2b", actorId: "user_1", idempotencyKey: null, payloadJson: "{}", argsJson: { streamDeltaHint: { family: "tree", kind: "move_document", args: { documentId: "doc_1", parentId: "parent_1", sortOrder: 2, }, }, domainEventHint: { family: "tree", eventType: "tree.subtree.moved", }, }, }); mockExecuteRustBridgeMutationTransport.mockResolvedValue({ ok: true, parent_id: "parent_1", sort_order: 1, workspace_id: "ws_1", updated_at: "2026-04-23T00:01:00Z", }); mockLoadSidebarDataFromConvex.mockResolvedValue({ sidebarInitialData: { activeWorkspaceId: "ws_1", workspaces: [], documents: [ { id: "doc_1", workspace_id: "ws_1", parent_id: "parent_old", }, { id: "parent_1", workspace_id: "ws_1", parent_id: null, }, ], rootDocuments: [], sidebarTree: [], trashedDocuments: [], mediaAssets: [], trashedMediaAssets: [], mindmapDocs: [], mindmapAssets: [], trashedMindmapAssets: [], mindmapAssetChildren: {}, tableAssets: [], trashedTableAssets: [], kernelSidebarProjection: { projectionId: "kernel_projection:sidebar_tree:workspace_root", projection: "sidebar_tree", rootNodeId: null, items: [], edges: [], }, kernelFileTreeProjection: { projectionId: "kernel_projection:file_tree:workspace_root", projection: "file_tree", rootNodeId: null, items: [], edges: [], }, }, }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "move", documentId: "doc_1", parentId: "parent_1", sortOrder: 3, workspaceId: "ws_1", }), }), ); expect(response.status).toBe(200); const payload = await response.json() as { result: { action: string; documentId: string; workspaceId: string; parentId: string | null; sortOrder: number | null; updatedAt: string | null; execution: { parent_id: string | null; sort_order: number | null; workspace_id: string | null; }; }; }; expect(payload.result).toMatchObject({ action: "move", documentId: "doc_1", workspaceId: "ws_1", parentId: "parent_1", sortOrder: 1, updatedAt: "2026-04-23T00:01:00Z", execution: { parent_id: "parent_1", sort_order: 1, workspace_id: "ws_1", }, }); expect(client.query).toHaveBeenCalledTimes(1); }); it("move action 应把 self move 的 preflight 交给 Rust,并由 Rust 拒绝", async () => { const client = { mutation: vi.fn(), query: vi.fn(async (name: string, args: { id: string }) => { if (name === "documents:getMeta" && args.id === "doc_1") { return { id: "doc_1", workspace_id: "ws_1", parent_id: null, }; } return null; }), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_move_self", traceId: "trace_tree_move_self", workspaceId: "ws_1", actor: { actorType: "user", actorId: "user_1", sessionId: null, }, source: { channel: "next-route", client: "wolai-frontend", }, tenantId: null, deploymentId: null, projectId: null, authToken: null, idempotencyKey: null, validateOnly: false, dryRun: false, }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockRejectedValue( Object.assign(new Error("不能把页面移动到自身下面"), { status: 400 }), ); mockLoadSidebarDataFromConvex.mockResolvedValue({ sidebarInitialData: { activeWorkspaceId: "ws_1", workspaces: [], documents: [ { id: "doc_1", workspace_id: "ws_1", parent_id: null, }, ], rootDocuments: [], sidebarTree: [], trashedDocuments: [], mediaAssets: [], trashedMediaAssets: [], mindmapDocs: [], mindmapAssets: [], trashedMindmapAssets: [], mindmapAssetChildren: {}, tableAssets: [], trashedTableAssets: [], kernelSidebarProjection: { projectionId: "kernel_projection:sidebar_tree:workspace_root", projection: "sidebar_tree", rootNodeId: null, items: [], edges: [], }, kernelFileTreeProjection: { projectionId: "kernel_projection:file_tree:workspace_root", projection: "file_tree", rootNodeId: null, items: [], edges: [], }, }, }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "move", documentId: "doc_1", parentId: "doc_1", sortOrder: 0, }), }), ); expect(response.status).toBe(400); expect(await response.json()).toEqual({ error: "不能把页面移动到自身下面", }); expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( expect.objectContaining({ name: "tree.subtree.move", preflightData: { documents: [ { id: "doc_1", workspace_id: "ws_1", parent_id: null, }, ], }, payload: expect.objectContaining({ documentId: "doc_1", parentId: "doc_1", sortOrder: 0, }), }), ); expect(mockResolveRustBridgeCommandPlan).toHaveBeenCalledTimes(1); expect(mockExecuteRustBridgeMutationTransport).not.toHaveBeenCalled(); }); it("move action 应把 descendant move 的 preflight 交给 Rust,并由 Rust 拒绝", async () => { const client = { mutation: vi.fn(), query: vi.fn(async (name: string, args: { id: string }) => { if (name === "documents:getMeta" && args.id === "doc_1") { return { id: "doc_1", workspace_id: "ws_1", parent_id: null, }; } if (name === "documents:getMeta" && args.id === "child_1") { return { id: "child_1", workspace_id: "ws_1", parent_id: "doc_1", }; } return null; }), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_move_desc", traceId: "trace_tree_move_desc", workspaceId: "ws_1", actor: { actorType: "user", actorId: "user_1", sessionId: null, }, source: { channel: "next-route", client: "wolai-frontend", }, tenantId: null, deploymentId: null, projectId: null, authToken: null, idempotencyKey: null, validateOnly: false, dryRun: false, }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockRejectedValue( Object.assign(new Error("不能把页面移动到自己的后代下面"), { status: 400 }), ); mockLoadSidebarDataFromConvex.mockResolvedValue({ sidebarInitialData: { activeWorkspaceId: "ws_1", workspaces: [], documents: [ { id: "doc_1", workspace_id: "ws_1", parent_id: null, }, { id: "child_1", workspace_id: "ws_1", parent_id: "doc_1", }, ], rootDocuments: [], sidebarTree: [], trashedDocuments: [], mediaAssets: [], trashedMediaAssets: [], mindmapDocs: [], mindmapAssets: [], trashedMindmapAssets: [], mindmapAssetChildren: {}, tableAssets: [], trashedTableAssets: [], kernelSidebarProjection: { projectionId: "kernel_projection:sidebar_tree:workspace_root", projection: "sidebar_tree", rootNodeId: null, items: [], edges: [], }, kernelFileTreeProjection: { projectionId: "kernel_projection:file_tree:workspace_root", projection: "file_tree", rootNodeId: null, items: [], edges: [], }, }, }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "move", documentId: "doc_1", parentId: "child_1", sortOrder: 0, }), }), ); expect(response.status).toBe(400); expect(await response.json()).toEqual({ error: "不能把页面移动到自己的后代下面", }); expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( expect.objectContaining({ name: "tree.subtree.move", preflightData: { documents: [ { id: "doc_1", workspace_id: "ws_1", parent_id: null, }, { id: "child_1", workspace_id: "ws_1", parent_id: "doc_1", }, ], }, payload: expect.objectContaining({ documentId: "doc_1", parentId: "child_1", sortOrder: 0, }), }), ); expect(mockResolveRustBridgeCommandPlan).toHaveBeenCalledTimes(1); expect(mockExecuteRustBridgeMutationTransport).not.toHaveBeenCalled(); }); it("rename action 走 tree.node.rename,并保留标题 payload", async () => { const client = { mutation: vi.fn(), query: vi.fn(async () => ({ id: "doc_1", workspace_id: "ws_1", })), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_3", traceId: "trace_tree_3", workspaceId: "ws_1", actor: { actorType: "user", actorId: "user_1", sessionId: null, }, source: { channel: "next-route", client: "wolai-frontend", }, tenantId: null, deploymentId: null, projectId: null, authToken: null, idempotencyKey: null, validateOnly: false, dryRun: false, }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockResolvedValue({ kind: "command", commandName: "tree.node.rename", commandId: "cmd_tree_rename_1", functionName: "documents:updateTitle", workspaceId: "ws_1", requestId: "req_tree_3", traceId: "trace_tree_3", actorId: "user_1", idempotencyKey: null, payloadJson: "{}", argsJson: { streamDeltaHint: { family: "tree", kind: "upsert_document_patch", args: { documentId: "doc_1", patch: { title: "新标题" }, }, }, domainEventHint: { family: "tree", eventType: "tree.node.renamed", }, }, }); mockExecuteRustBridgeMutationTransport.mockResolvedValue({ ok: true, updated_at: "2026-04-23T00:00:00Z", }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "rename", documentId: "doc_1", workspaceId: "ws_1", title: "新标题", }), }), ); expect(response.status).toBe(200); const payload = await response.json() as { result: { action: string; documentId: string; workspaceId: string; title: string; }; }; expect(payload.result).toMatchObject({ action: "rename", documentId: "doc_1", workspaceId: "ws_1", title: "新标题", }); expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( expect.objectContaining({ name: "tree.node.rename", payload: { documentId: "doc_1", workspaceId: "ws_1", title: "新标题", }, }), ); expect(mockEnsureDocumentScaffold).not.toHaveBeenCalled(); expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( expect.objectContaining({ envelope: expect.objectContaining({ name: "tree.node.rename", }), plan: expect.objectContaining({ commandName: "tree.node.rename", }), result: expect.objectContaining({ updated_at: "2026-04-23T00:00:00Z", }), }), ); }); it("archive action 走 tree.node.archive", async () => { const client = { mutation: vi.fn(), query: vi.fn(async () => ({ id: "doc_1", workspace_id: "ws_1", })), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_4", traceId: "trace_tree_4", workspaceId: "ws_1", }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockResolvedValue({ kind: "command", commandName: "tree.node.archive", commandId: "cmd_tree_archive_1", functionName: "documents:softDelete", workspaceId: "ws_1", requestId: "req_tree_4", traceId: "trace_tree_4", actorId: "user_1", idempotencyKey: null, payloadJson: "{}", argsJson: { streamDeltaHint: { family: "tree", kind: "remove_document", args: { documentId: "doc_1" }, }, domainEventHint: { family: "tree", eventType: "tree.node.archived", }, }, }); mockExecuteRustBridgeMutationTransport.mockResolvedValue({ ok: true, updated_at: "2026-04-24T00:00:00Z", }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "archive", documentId: "doc_1", workspaceId: "ws_1", }), }), ); expect(response.status).toBe(200); const payload = await response.json() as { result: { action: string; documentId: string; workspaceId: string; }; }; expect(payload.result).toMatchObject({ action: "archive", documentId: "doc_1", workspaceId: "ws_1", }); expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( expect.objectContaining({ name: "tree.node.archive", payload: { documentId: "doc_1", workspaceId: "ws_1", }, }), ); expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( expect.objectContaining({ envelope: expect.objectContaining({ name: "tree.node.archive", }), plan: expect.objectContaining({ commandName: "tree.node.archive", }), result: expect.any(Object), }), ); }); it("embed action 走 tree.node.embed,并把 pageReference 组装交给 Rust Page Aggregate preflight", async () => { const client = { mutation: vi.fn(), query: vi.fn(async (name: string, args: { id: string }) => { if (name === "documents:getMeta" && args.id === "doc_source") { return { id: "doc_source", title: "来源页面", workspace_id: "ws_1", }; } if (name === "documents:getMeta" && args.id === "doc_target") { return { id: "doc_target", workspace_id: "ws_1", embed_default_block_id: "anchor_1", }; } if (name === "documents:getContent" && args.id === "doc_target") { return { content: [ { id: "anchor_1", type: "paragraph" }, ], revision: 7, conflict_detection_key: "doc_target:7", }; } return null; }), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_5", traceId: "trace_tree_5", workspaceId: "ws_1", }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockResolvedValue({ kind: "command", commandName: "tree.node.embed", commandId: "cmd_tree_embed_1", functionName: "documents:updateContent", workspaceId: "ws_1", requestId: "req_tree_5", traceId: "trace_tree_5", actorId: "user_1", idempotencyKey: null, payloadJson: "{}", argsJson: { streamDeltaHint: { family: "tree", kind: "noop", args: {}, }, domainEventHint: { family: "tree", eventType: "tree.node.embedded", }, }, }); mockExecuteRustBridgeMutationTransport.mockResolvedValue({ revision: 8, conflict_detection_key: "doc_target:8", }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "embed", sourceId: "doc_source", targetId: "doc_target", }), }), ); expect(response.status).toBe(200); const payload = await response.json() as { result: { action: string; documentId: string; sourceDocumentId: string; targetDocumentId: string; }; }; expect(payload.result).toMatchObject({ action: "embed", documentId: "doc_target", sourceDocumentId: "doc_source", targetDocumentId: "doc_target", }); expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( expect.objectContaining({ name: "tree.node.embed", payload: expect.objectContaining({ documentId: "doc_target", sourceDocumentId: "doc_source", targetDocumentId: "doc_target", revision: 7, conflictDetectionKey: "doc_target:7", }), preflightData: { pageAggregateEmbed: { sourceDocumentId: "doc_source", sourceTitle: "来源页面", targetDocumentId: "doc_target", targetContent: [ { id: "anchor_1", type: "paragraph" }, ], anchorBlockId: "anchor_1", blockId: expect.any(String), }, }, }), ); expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( expect.objectContaining({ envelope: expect.objectContaining({ name: "tree.node.embed", }), plan: expect.objectContaining({ commandName: "tree.node.embed", }), result: expect.any(Object), }), ); }); it("copy action 走 tree.subtree.copy,并保留 scaffold 与导图文件副作用", async () => { const client = { mutation: vi.fn(), query: vi.fn(async (name: string, args: { id: string }) => { if (name === "documents:getMeta" && args.id === "parent_1") { return { id: "parent_1", workspace_id: "ws_1", }; } return null; }), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_6", traceId: "trace_tree_6", workspaceId: "ws_1", }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockResolvedValue({ kind: "command", commandName: "tree.subtree.copy", commandId: "cmd_tree_copy_1", functionName: "documents:copyTree", workspaceId: "ws_1", requestId: "req_tree_6", traceId: "trace_tree_6", actorId: "user_1", idempotencyKey: null, payloadJson: "{}", argsJson: { streamDeltaHint: { family: "tree", kind: "copy_result", args: { itemsField: "items", documentField: "document" }, }, domainEventHint: { family: "tree", eventType: "tree.subtree.copied", }, }, }); mockExecuteRustBridgeMutationTransport.mockResolvedValue({ items: [ { oldId: "doc_1", newId: "doc_2", title: "复制页面", document: { id: "doc_2", workspace_id: "ws_1", title: "复制页面", parent_id: "parent_1", sort_order: 0, is_starred: false, access_scope: "private", is_template: false, created_at: "2026-04-24T00:00:00Z", updated_at: "2026-04-24T00:00:00Z", }, }, ], }); mockLoadSidebarDataFromConvex.mockResolvedValue({ sidebarInitialData: { activeWorkspaceId: "ws_1", workspaces: [], documents: [], rootDocuments: [], sidebarTree: [], trashedDocuments: [], mediaAssets: [], trashedMediaAssets: [], mindmapDocs: [], mindmapAssets: [], trashedMindmapAssets: [], mindmapAssetChildren: {}, tableAssets: [], trashedTableAssets: [], kernelSidebarProjection: { projectionId: "kernel_projection:sidebar_tree:workspace_root", projection: "sidebar_tree", rootNodeId: null, items: [], edges: [], }, kernelFileTreeProjection: { projectionId: "kernel_projection:file_tree:workspace_root", projection: "file_tree", rootNodeId: null, items: [], edges: [], }, }, }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "copy", targetParentId: "parent_1", items: [{ documentId: "doc_1", recursive: true }], }), }), ); expect(response.status).toBe(200); const payload = await response.json() as { result: { action: string; items: Array<{ oldId: string; newId: string }>; }; }; expect(payload.result).toMatchObject({ action: "copy", items: [{ oldId: "doc_1", newId: "doc_2" }], }); expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( expect.objectContaining({ name: "tree.subtree.copy", payload: { workspaceId: "ws_1", targetParentId: "parent_1", items: [{ documentId: "doc_1", recursive: true }], }, }), ); expect(mockEnsureDocumentScaffold).toHaveBeenCalledWith("doc_2", "复制页面"); expect(mockCopyMindmapFilesIfExists).toHaveBeenCalledWith("doc_1", "doc_2"); expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( expect.objectContaining({ envelope: expect.objectContaining({ name: "tree.subtree.copy", }), plan: expect.objectContaining({ commandName: "tree.subtree.copy", }), result: expect.objectContaining({ items: [expect.objectContaining({ newId: "doc_2" })], }), }), ); }); it("restore action 走 tree.node.restore,并附带 upsert_document delta", async () => { const client = { mutation: vi.fn(), query: vi.fn(async () => ({ id: "doc_restore_1", workspace_id: "ws_1", })), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_restore_1", traceId: "trace_tree_restore_1", workspaceId: "ws_1", }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockResolvedValue({ kind: "command", commandName: "tree.node.restore", commandId: "cmd_tree_restore_1", functionName: "documents:restore", workspaceId: "ws_1", requestId: "req_tree_restore_1", traceId: "trace_tree_restore_1", actorId: "user_1", idempotencyKey: null, payloadJson: "{}", argsJson: { streamDeltaHint: { family: "tree", kind: "document_result", args: { documentField: "document" }, }, domainEventHint: { family: "tree", eventType: "tree.node.restored", }, }, }); mockExecuteRustBridgeMutationTransport.mockResolvedValue({ ok: true, document: { id: "doc_restore_1", workspace_id: "ws_1", title: "恢复页面", parent_id: null, sort_order: 0, is_starred: false, access_scope: "private", is_template: false, created_at: "2026-04-23T00:00:00Z", updated_at: "2026-04-24T00:00:00Z", }, updated_at: "2026-04-24T00:00:00Z", }); mockLoadSidebarDataFromConvex.mockResolvedValue({ sidebarInitialData: { activeWorkspaceId: "ws_1", workspaces: [], documents: [], rootDocuments: [], sidebarTree: [], trashedDocuments: [], mediaAssets: [], trashedMediaAssets: [], mindmapDocs: [], mindmapAssets: [], trashedMindmapAssets: [], mindmapAssetChildren: {}, tableAssets: [], trashedTableAssets: [], kernelSidebarProjection: { projectionId: "kernel_projection:sidebar_tree:workspace_root", projection: "sidebar_tree", rootNodeId: null, items: [], edges: [], }, kernelFileTreeProjection: { projectionId: "kernel_projection:file_tree:workspace_root", projection: "file_tree", rootNodeId: null, items: [], edges: [], }, }, }); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "restore", documentId: "doc_restore_1", workspaceId: "ws_1", }), }), ); expect(response.status).toBe(200); expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( expect.objectContaining({ name: "tree.node.restore", payload: { documentId: "doc_restore_1", workspaceId: "ws_1", }, }), ); expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( expect.objectContaining({ envelope: expect.objectContaining({ name: "tree.node.restore", }), plan: expect.objectContaining({ commandName: "tree.node.restore", }), result: expect.objectContaining({ document: expect.objectContaining({ id: "doc_restore_1" }), }), }), ); }); it("tree mutation 失败时应记录 failure bridge log", async () => { const client = { mutation: vi.fn(), query: vi.fn(async () => ({ id: "doc_1", workspace_id: "ws_1", })), }; mockGetAuthedConvexClient.mockResolvedValue({ auth: { userId: "user_1", email: "dev@example.com", name: "开发用户", }, client, }); mockBuildDocumentBridgeContext.mockResolvedValue({ requestId: "req_tree_fail_1", traceId: "trace_tree_fail_1", workspaceId: "ws_1", actor: { actorType: "user", actorId: "user_1", sessionId: null, }, source: { channel: "next-route", client: "wolai-frontend", }, tenantId: null, deploymentId: null, projectId: null, authToken: null, idempotencyKey: null, validateOnly: false, dryRun: false, }); mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); mockResolveRustBridgeCommandPlan.mockResolvedValue({ kind: "command", commandName: "tree.node.archive", commandId: "cmd_tree_archive_fail_1", functionName: "documents:softDelete", workspaceId: "ws_1", requestId: "req_tree_fail_1", traceId: "trace_tree_fail_1", actorId: "user_1", idempotencyKey: null, payloadJson: "{}", argsJson: { streamDeltaHint: { family: "tree", kind: "remove_document", args: { documentId: "doc_1" }, }, domainEventHint: { family: "tree", eventType: "tree.node.archived", }, }, }); mockExecuteRustBridgeMutationTransport.mockRejectedValue(new Error("archive failed")); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "archive", documentId: "doc_1", workspaceId: "ws_1", }), }), ); expect(response.status).toBe(500); expect(mockRecordBridgeCommandFailureArtifacts).toHaveBeenCalledWith( expect.objectContaining({ envelope: expect.objectContaining({ name: "tree.node.archive", }), error: expect.any(Error), }), ); }); });