feat: continue tree rust family cutover
- add rust renderer/state-family scaffolds and inline compat host thinning for page tree, file tree, and picker - route tree/filetree preflight, file projection, resource artifact, and stream delta contracts through rust plans - preserve canonical move-order validation, file-tree search projection, and related frontend/runtime regression coverage
This commit is contained in:
@@ -6,6 +6,7 @@ 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) =>
|
||||
@@ -62,12 +63,112 @@ vi.mock("@/lib/documents/bridge", () => ({
|
||||
},
|
||||
buildDocumentBridgeContext: mockBuildDocumentBridgeContext,
|
||||
buildDocumentCommandEnvelope: mockBuildDocumentCommandEnvelope,
|
||||
documentBridgeErrorResponse: (...args: unknown[]) => mockDocumentBridgeErrorResponse(...args),
|
||||
documentBridgeErrorResponse: (...args: Parameters<typeof mockDocumentBridgeErrorResponse>) =>
|
||||
mockDocumentBridgeErrorResponse(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/documents/rust-runtime", () => ({
|
||||
resolveRustBridgeCommandPlan: (...args: unknown[]) => mockResolveRustBridgeCommandPlan(...args),
|
||||
executeRustBridgeMutationTransport: (...args: unknown[]) => mockExecuteRustBridgeMutationTransport(...args),
|
||||
resolveRustBridgeCommandPlan: (...args: Parameters<typeof mockResolveRustBridgeCommandPlan>) =>
|
||||
mockResolveRustBridgeCommandPlan(...args),
|
||||
executeRustBridgeMutationTransport: (...args: Parameters<typeof mockExecuteRustBridgeMutationTransport>) =>
|
||||
mockExecuteRustBridgeMutationTransport(...args),
|
||||
recordRustBridgeCommandArtifacts: (...args: Parameters<typeof mockRecordRustBridgeCommandArtifacts>) =>
|
||||
mockRecordRustBridgeCommandArtifacts(...args),
|
||||
readRustTreeDomainEventType: (plan: { argsJson?: Record<string, unknown> }) => {
|
||||
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<string, unknown> };
|
||||
streamDelta?: Record<string, unknown> | 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<string, unknown> }; result: unknown }) => {
|
||||
const hint = input.plan.argsJson?.streamDeltaHint as
|
||||
| {
|
||||
family?: string;
|
||||
kind?: string;
|
||||
args?: Record<string, unknown>;
|
||||
}
|
||||
| undefined;
|
||||
if (hint?.family !== "tree" || !hint.kind) return null;
|
||||
const args = hint.args ?? {};
|
||||
const result = input.result as Record<string, unknown>;
|
||||
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<string, unknown>),
|
||||
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", () => ({
|
||||
@@ -92,6 +193,7 @@ describe("/api/tree/commands route", () => {
|
||||
mockBuildDocumentCommandEnvelope.mockReset();
|
||||
mockResolveRustBridgeCommandPlan.mockReset();
|
||||
mockExecuteRustBridgeMutationTransport.mockReset();
|
||||
mockRecordRustBridgeCommandArtifacts.mockReset().mockResolvedValue(null);
|
||||
mockRecordBridgeCommandArtifacts.mockReset();
|
||||
mockRecordBridgeCommandFailureArtifacts.mockReset();
|
||||
mockEnsureDocumentScaffold.mockReset();
|
||||
@@ -146,7 +248,17 @@ describe("/api/tree/commands route", () => {
|
||||
actorId: "user_1",
|
||||
idempotencyKey: null,
|
||||
payloadJson: "{}",
|
||||
argsJson: {},
|
||||
argsJson: {
|
||||
streamDeltaHint: {
|
||||
family: "tree",
|
||||
kind: "document_result",
|
||||
args: { documentField: "document" },
|
||||
},
|
||||
domainEventHint: {
|
||||
family: "tree",
|
||||
eventType: "tree.node.created",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockExecuteRustBridgeMutationTransport.mockResolvedValue({
|
||||
id: "doc_new",
|
||||
@@ -158,6 +270,18 @@ describe("/api/tree/commands route", () => {
|
||||
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");
|
||||
@@ -196,25 +320,26 @@ describe("/api/tree/commands route", () => {
|
||||
}),
|
||||
);
|
||||
expect(mockEnsureDocumentScaffold).toHaveBeenCalledWith("doc_new", "无标题");
|
||||
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
context: expect.objectContaining({
|
||||
requestId: "req_tree_1",
|
||||
traceId: "trace_tree_1",
|
||||
}),
|
||||
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,
|
||||
}),
|
||||
},
|
||||
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 () => {
|
||||
@@ -283,7 +408,21 @@ describe("/api/tree/commands route", () => {
|
||||
actorId: "user_1",
|
||||
idempotencyKey: null,
|
||||
payloadJson: "{}",
|
||||
argsJson: {},
|
||||
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,
|
||||
@@ -392,27 +531,18 @@ describe("/api/tree/commands route", () => {
|
||||
}),
|
||||
);
|
||||
expect(mockEnsureDocumentScaffold).not.toHaveBeenCalled();
|
||||
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect(mockRecordRustBridgeCommandArtifacts).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,
|
||||
},
|
||||
],
|
||||
},
|
||||
plan: expect.objectContaining({
|
||||
commandName: "tree.subtree.move",
|
||||
functionName: "documents:move",
|
||||
}),
|
||||
result: expect.objectContaining({
|
||||
parent_id: "parent_1",
|
||||
sort_order: 1,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -482,7 +612,21 @@ describe("/api/tree/commands route", () => {
|
||||
actorId: "user_1",
|
||||
idempotencyKey: null,
|
||||
payloadJson: "{}",
|
||||
argsJson: {},
|
||||
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,
|
||||
@@ -902,7 +1046,20 @@ describe("/api/tree/commands route", () => {
|
||||
actorId: "user_1",
|
||||
idempotencyKey: null,
|
||||
payloadJson: "{}",
|
||||
argsJson: {},
|
||||
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,
|
||||
@@ -949,19 +1106,16 @@ describe("/api/tree/commands route", () => {
|
||||
}),
|
||||
);
|
||||
expect(mockEnsureDocumentScaffold).not.toHaveBeenCalled();
|
||||
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
envelope: expect.objectContaining({
|
||||
name: "tree.node.rename",
|
||||
}),
|
||||
commandPayload: expect.objectContaining({
|
||||
streamDelta: {
|
||||
op: "upsert_document",
|
||||
document: expect.objectContaining({
|
||||
id: "doc_1",
|
||||
title: "新标题",
|
||||
}),
|
||||
},
|
||||
plan: expect.objectContaining({
|
||||
commandName: "tree.node.rename",
|
||||
}),
|
||||
result: expect.objectContaining({
|
||||
updated_at: "2026-04-23T00:00:00Z",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -1000,7 +1154,17 @@ describe("/api/tree/commands route", () => {
|
||||
actorId: "user_1",
|
||||
idempotencyKey: null,
|
||||
payloadJson: "{}",
|
||||
argsJson: {},
|
||||
argsJson: {
|
||||
streamDeltaHint: {
|
||||
family: "tree",
|
||||
kind: "remove_document",
|
||||
args: { documentId: "doc_1" },
|
||||
},
|
||||
domainEventHint: {
|
||||
family: "tree",
|
||||
eventType: "tree.node.archived",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockExecuteRustBridgeMutationTransport.mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -1042,17 +1206,15 @@ describe("/api/tree/commands route", () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
envelope: expect.objectContaining({
|
||||
name: "tree.node.archive",
|
||||
}),
|
||||
commandPayload: expect.objectContaining({
|
||||
streamDelta: {
|
||||
op: "remove_document",
|
||||
documentId: "doc_1",
|
||||
},
|
||||
plan: expect.objectContaining({
|
||||
commandName: "tree.node.archive",
|
||||
}),
|
||||
result: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1112,7 +1274,17 @@ describe("/api/tree/commands route", () => {
|
||||
actorId: "user_1",
|
||||
idempotencyKey: null,
|
||||
payloadJson: "{}",
|
||||
argsJson: {},
|
||||
argsJson: {
|
||||
streamDeltaHint: {
|
||||
family: "tree",
|
||||
kind: "noop",
|
||||
args: {},
|
||||
},
|
||||
domainEventHint: {
|
||||
family: "tree",
|
||||
eventType: "tree.node.embedded",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockExecuteRustBridgeMutationTransport.mockResolvedValue({
|
||||
revision: 8,
|
||||
@@ -1170,16 +1342,15 @@ describe("/api/tree/commands route", () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
envelope: expect.objectContaining({
|
||||
name: "tree.node.embed",
|
||||
}),
|
||||
commandPayload: expect.objectContaining({
|
||||
streamDelta: {
|
||||
op: "noop",
|
||||
},
|
||||
plan: expect.objectContaining({
|
||||
commandName: "tree.node.embed",
|
||||
}),
|
||||
result: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1222,7 +1393,17 @@ describe("/api/tree/commands route", () => {
|
||||
actorId: "user_1",
|
||||
idempotencyKey: null,
|
||||
payloadJson: "{}",
|
||||
argsJson: {},
|
||||
argsJson: {
|
||||
streamDeltaHint: {
|
||||
family: "tree",
|
||||
kind: "copy_result",
|
||||
args: { itemsField: "items", documentField: "document" },
|
||||
},
|
||||
domainEventHint: {
|
||||
family: "tree",
|
||||
eventType: "tree.subtree.copied",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockExecuteRustBridgeMutationTransport.mockResolvedValue({
|
||||
items: [
|
||||
@@ -1230,6 +1411,18 @@ describe("/api/tree/commands route", () => {
|
||||
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",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1302,22 +1495,22 @@ describe("/api/tree/commands route", () => {
|
||||
);
|
||||
expect(mockEnsureDocumentScaffold).toHaveBeenCalledWith("doc_2", "复制页面");
|
||||
expect(mockCopyMindmapFilesIfExists).toHaveBeenCalledWith("doc_1", "doc_2");
|
||||
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
envelope: expect.objectContaining({
|
||||
name: "tree.subtree.copy",
|
||||
}),
|
||||
commandPayload: expect.objectContaining({
|
||||
streamDelta: {
|
||||
op: "replace_documents",
|
||||
documents: [],
|
||||
},
|
||||
plan: expect.objectContaining({
|
||||
commandName: "tree.subtree.copy",
|
||||
}),
|
||||
result: expect.objectContaining({
|
||||
items: [expect.objectContaining({ newId: "doc_2" })],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("restore action 走 tree.node.restore,并附带 replace_documents delta", async () => {
|
||||
it("restore action 走 tree.node.restore,并附带 upsert_document delta", async () => {
|
||||
const client = {
|
||||
mutation: vi.fn(),
|
||||
query: vi.fn(async () => ({
|
||||
@@ -1350,10 +1543,32 @@ describe("/api/tree/commands route", () => {
|
||||
actorId: "user_1",
|
||||
idempotencyKey: null,
|
||||
payloadJson: "{}",
|
||||
argsJson: {},
|
||||
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({
|
||||
@@ -1412,16 +1627,16 @@ describe("/api/tree/commands route", () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(mockRecordBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
envelope: expect.objectContaining({
|
||||
name: "tree.node.restore",
|
||||
}),
|
||||
commandPayload: expect.objectContaining({
|
||||
streamDelta: {
|
||||
op: "replace_documents",
|
||||
documents: [],
|
||||
},
|
||||
plan: expect.objectContaining({
|
||||
commandName: "tree.node.restore",
|
||||
}),
|
||||
result: expect.objectContaining({
|
||||
document: expect.objectContaining({ id: "doc_restore_1" }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -1476,7 +1691,17 @@ describe("/api/tree/commands route", () => {
|
||||
actorId: "user_1",
|
||||
idempotencyKey: null,
|
||||
payloadJson: "{}",
|
||||
argsJson: {},
|
||||
argsJson: {
|
||||
streamDeltaHint: {
|
||||
family: "tree",
|
||||
kind: "remove_document",
|
||||
args: { documentId: "doc_1" },
|
||||
},
|
||||
domainEventHint: {
|
||||
family: "tree",
|
||||
eventType: "tree.node.archived",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockExecuteRustBridgeMutationTransport.mockRejectedValue(new Error("archive failed"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user