Files
mnote/wolai-frontend/src/app/api/tree/commands/route.test.ts
T

1507 lines
43 KiB
TypeScript
Raw Normal View History

2026-04-24 06:10:18 +08:00
import { beforeEach, describe, expect, it, vi } from "vitest";
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();
2026-04-26 04:29:23 +08:00
const mockRecordBridgeCommandArtifacts = vi.fn();
const mockRecordBridgeCommandFailureArtifacts = vi.fn();
2026-04-24 06:10:18 +08:00
const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) =>
Response.json(
{
error: error instanceof Error ? error.message : String(error),
},
2026-04-26 04:29:23 +08:00
{
status:
typeof (error as { status?: unknown })?.status === "number"
? ((error as { status: number }).status ?? 500)
: 500,
},
2026-04-24 06:10:18 +08:00
),
);
const mockEnsureDocumentScaffold = vi.fn();
2026-04-26 04:29:23 +08:00
const mockCopyMindmapFilesIfExists = vi.fn();
const mockLoadSidebarDataFromConvex = vi.fn();
2026-04-24 06:10:18 +08:00
vi.mock("@/lib/convex/enabled", () => ({
isConvexEnabled: () => mockIsConvexEnabled(),
}));
vi.mock("@/lib/convex/api", () => ({
api: {
documents: {
getMeta: "documents:getMeta",
2026-04-26 04:29:23 +08:00
getContent: "documents:getContent",
2026-04-24 06:10:18 +08:00
},
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: unknown[]) => mockDocumentBridgeErrorResponse(...args),
}));
vi.mock("@/lib/documents/rust-runtime", () => ({
resolveRustBridgeCommandPlan: (...args: unknown[]) => mockResolveRustBridgeCommandPlan(...args),
executeRustBridgeMutationTransport: (...args: unknown[]) => mockExecuteRustBridgeMutationTransport(...args),
}));
2026-04-26 04:29:23 +08:00
vi.mock("@/lib/documents/bridge-log", () => ({
recordBridgeCommandArtifacts: (...args: unknown[]) => mockRecordBridgeCommandArtifacts(...args),
recordBridgeCommandFailureArtifacts: (...args: unknown[]) => mockRecordBridgeCommandFailureArtifacts(...args),
}));
2026-04-24 06:10:18 +08:00
vi.mock("@/lib/documents/page-lifecycle-side-effects", () => ({
ensureDocumentScaffold: (...args: unknown[]) => mockEnsureDocumentScaffold(...args),
2026-04-26 04:29:23 +08:00
copyMindmapFilesIfExists: (...args: unknown[]) => mockCopyMindmapFilesIfExists(...args),
}));
vi.mock("@/lib/server/sidebar-data", () => ({
loadSidebarDataFromConvex: (...args: unknown[]) => mockLoadSidebarDataFromConvex(...args),
2026-04-24 06:10:18 +08:00
}));
describe("/api/tree/commands route", () => {
beforeEach(() => {
mockIsConvexEnabled.mockReturnValue(true);
mockGetAuthedConvexClient.mockReset();
mockBuildDocumentBridgeContext.mockReset();
mockBuildDocumentCommandEnvelope.mockReset();
mockResolveRustBridgeCommandPlan.mockReset();
mockExecuteRustBridgeMutationTransport.mockReset();
2026-04-26 04:29:23 +08:00
mockRecordBridgeCommandArtifacts.mockReset();
mockRecordBridgeCommandFailureArtifacts.mockReset();
2026-04-24 06:10:18 +08:00
mockEnsureDocumentScaffold.mockReset();
2026-04-26 04:29:23 +08:00
mockCopyMindmapFilesIfExists.mockReset();
mockLoadSidebarDataFromConvex.mockReset();
2026-04-24 06:10:18 +08:00
mockDocumentBridgeErrorResponse.mockClear();
});
it("create action 走 tree.node.create,并保留本地 scaffold 副作用", async () => {
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: {},
});
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",
});
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", "无标题");
2026-04-26 04:29:23 +08:00
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
expect.objectContaining({
envelope: expect.objectContaining({
name: "tree.node.create",
}),
commandPayload: expect.objectContaining({
streamDelta: {
op: "upsert_document",
document: expect.objectContaining({
id: "doc_new",
workspace_id: "ws_root",
title: "无标题",
parent_id: null,
sort_order: 0,
}),
},
}),
}),
);
2026-04-24 06:10:18 +08:00
});
it("move action 走 tree.subtree.move,并把 position 归一化为 sortOrder", async () => {
2026-04-26 04:29:23 +08:00
let docMetaReadCount = 0;
2026-04-24 06:10:18 +08:00
const client = {
mutation: vi.fn(),
2026-04-26 04:29:23 +08:00
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;
}),
2026-04-24 06:10:18 +08:00
};
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: {},
});
mockExecuteRustBridgeMutationTransport.mockResolvedValue({
ok: true,
2026-04-26 04:29:23 +08:00
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: [],
},
},
2026-04-24 06:10:18 +08:00
});
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",
2026-04-26 04:29:23 +08:00
sortOrder: 1,
2026-04-24 06:10:18 +08:00
workspaceId: "ws_1",
});
expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith(
expect.objectContaining({
name: "tree.subtree.move",
2026-04-26 04:29:23 +08:00
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({
2026-04-24 06:10:18 +08:00
documentId: "doc_1",
parentId: "parent_1",
sortOrder: 2,
2026-04-26 04:29:23 +08:00
}),
2026-04-24 06:10:18 +08:00
}),
);
expect(mockEnsureDocumentScaffold).not.toHaveBeenCalled();
2026-04-26 04:29:23 +08:00
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
expect.objectContaining({
envelope: expect.objectContaining({
name: "tree.subtree.move",
}),
commandPayload: expect.objectContaining({
streamDelta: {
op: "replace_documents",
documents: [
{
id: "doc_1",
workspace_id: "ws_1",
parent_id: null,
},
{
id: "parent_1",
workspace_id: "ws_1",
parent_id: null,
},
],
},
}),
}),
);
});
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: {},
});
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();
2026-04-24 06:10:18 +08:00
});
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: {},
});
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();
2026-04-26 04:29:23 +08:00
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
expect.objectContaining({
envelope: expect.objectContaining({
name: "tree.node.rename",
}),
commandPayload: expect.objectContaining({
streamDelta: {
op: "upsert_document",
document: expect.objectContaining({
id: "doc_1",
title: "新标题",
}),
},
}),
}),
);
});
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: {},
});
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(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
expect.objectContaining({
envelope: expect.objectContaining({
name: "tree.node.archive",
}),
commandPayload: expect.objectContaining({
streamDelta: {
op: "remove_document",
documentId: "doc_1",
},
}),
}),
);
});
it("embed action 走 tree.node.embed,并生成 pageReference 保存 payload", 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: {},
});
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",
content: [
{ id: "anchor_1", type: "paragraph" },
{
id: expect.any(String),
type: "pageReference",
props: {
pageId: "doc_source",
title: "来源页面",
},
},
],
}),
}),
);
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
expect.objectContaining({
envelope: expect.objectContaining({
name: "tree.node.embed",
}),
commandPayload: expect.objectContaining({
streamDelta: {
op: "noop",
},
}),
}),
);
});
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: {},
});
mockExecuteRustBridgeMutationTransport.mockResolvedValue({
items: [
{
oldId: "doc_1",
newId: "doc_2",
title: "复制页面",
},
],
});
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(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
expect.objectContaining({
envelope: expect.objectContaining({
name: "tree.subtree.copy",
}),
commandPayload: expect.objectContaining({
streamDelta: {
op: "replace_documents",
documents: [],
},
}),
}),
);
});
it("restore action 走 tree.node.restore,并附带 replace_documents 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: {},
});
mockExecuteRustBridgeMutationTransport.mockResolvedValue({
ok: true,
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(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
expect.objectContaining({
envelope: expect.objectContaining({
name: "tree.node.restore",
}),
commandPayload: expect.objectContaining({
streamDelta: {
op: "replace_documents",
documents: [],
},
}),
}),
);
});
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: {},
});
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),
}),
);
2026-04-24 06:10:18 +08:00
});
});