4-10 树域 Rust 家族化
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
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();
|
||||
const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) =>
|
||||
Response.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 },
|
||||
),
|
||||
);
|
||||
const mockEnsureDocumentScaffold = vi.fn();
|
||||
|
||||
vi.mock("@/lib/convex/enabled", () => ({
|
||||
isConvexEnabled: () => mockIsConvexEnabled(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/convex/api", () => ({
|
||||
api: {
|
||||
documents: {
|
||||
getMeta: "documents:getMeta",
|
||||
},
|
||||
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),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/documents/page-lifecycle-side-effects", () => ({
|
||||
ensureDocumentScaffold: (...args: unknown[]) => mockEnsureDocumentScaffold(...args),
|
||||
}));
|
||||
|
||||
describe("/api/tree/commands route", () => {
|
||||
beforeEach(() => {
|
||||
mockIsConvexEnabled.mockReturnValue(true);
|
||||
mockGetAuthedConvexClient.mockReset();
|
||||
mockBuildDocumentBridgeContext.mockReset();
|
||||
mockBuildDocumentCommandEnvelope.mockReset();
|
||||
mockResolveRustBridgeCommandPlan.mockReset();
|
||||
mockExecuteRustBridgeMutationTransport.mockReset();
|
||||
mockEnsureDocumentScaffold.mockReset();
|
||||
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", "无标题");
|
||||
});
|
||||
|
||||
it("move action 走 tree.subtree.move,并把 position 归一化为 sortOrder", 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_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,
|
||||
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: "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: 2,
|
||||
workspaceId: "ws_1",
|
||||
});
|
||||
expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "tree.subtree.move",
|
||||
payload: {
|
||||
documentId: "doc_1",
|
||||
parentId: "parent_1",
|
||||
sortOrder: 2,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(mockEnsureDocumentScaffold).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: {},
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user