feat: 收口本地文件夹入口并推进 page aggregate rust-first
- 为 Rust Web 主入口补齐本地文件夹/云空间切换、最近目录与路径回填体验\n- 对齐 local markdown media 与 inline marks 的 Rust shell / TipTap converter 语义\n- 让 documents/page 优先消费 Rust page aggregate snapshot,并保留 TS fallback\n- 补强 tree live、local markdown 与主入口 smoke,并同步设计稿状态
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/convex/enabled", () => ({
|
||||
isConvexEnabled: vi.fn(() => true),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth/authContext", () => ({
|
||||
HttpError: class HttpError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
},
|
||||
requireAuthContext: vi.fn(async () => ({
|
||||
userId: "user_1",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/convex/route", () => ({
|
||||
getAuthedConvexClient: vi.fn(async () => ({
|
||||
auth: { userId: "user_1" },
|
||||
client: {
|
||||
query: vi.fn(async () => ({
|
||||
id: "doc_1",
|
||||
workspace_id: "ws_1",
|
||||
title: "Next fallback 页面",
|
||||
updated_at: null,
|
||||
can_edit: true,
|
||||
})),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/documents/rust-runtime", () => ({
|
||||
resolveRustBridgeQueryPlan: vi.fn(async () => ({
|
||||
kind: "query",
|
||||
queryName: "documents.content.get",
|
||||
functionName: "documents:getContent",
|
||||
workspaceId: "ws_1",
|
||||
requestId: "req_next",
|
||||
traceId: "trace_next",
|
||||
actorId: "user_1",
|
||||
payloadJson: "{}",
|
||||
argsJson: { id: "doc_1" },
|
||||
})),
|
||||
executeRustBridgeQueryTransport: vi.fn(async () => ({
|
||||
content: [{ id: "block_1", type: "paragraph", content: [] }],
|
||||
revision: 1,
|
||||
conflict_detection_key: "doc_1:1",
|
||||
pageSubtree: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/mnote-web/internal-url", () => ({
|
||||
resolveMnoteWebInternalUrl: vi.fn(async () => "http://127.0.0.1:3104"),
|
||||
}));
|
||||
|
||||
import { GET } from "@/app/api/documents/page/route";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import { resolveMnoteWebInternalUrl } from "@/lib/mnote-web/internal-url";
|
||||
|
||||
function rustPageAggregateSnapshot() {
|
||||
return {
|
||||
schema: "mnote.page_aggregate.v1" as const,
|
||||
projectionVersion: 1,
|
||||
source: "KernelProjection",
|
||||
identity: { documentId: "doc_1", workspaceId: "ws_1" },
|
||||
head: {
|
||||
title: "Rust 聚合页面",
|
||||
updatedAt: null,
|
||||
permissions: {
|
||||
readOnly: false,
|
||||
disableDownload: false,
|
||||
disableCopy: false,
|
||||
},
|
||||
},
|
||||
layout: { pageOptions: { wideLayout: false } },
|
||||
body: { content: [], revision: 7, conflictDetectionKey: "doc_1:7" },
|
||||
tree: { pageSubtree: null },
|
||||
stats: { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("documents/page route", () => {
|
||||
it("优先返回 Rust page aggregate snapshot,而不是重新组装 meta + content", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
schema: "mnote.page_aggregate.v1",
|
||||
result: rustPageAggregateSnapshot(),
|
||||
requestId: "req_rust",
|
||||
traceId: "trace_rust",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await GET(
|
||||
new Request("http://localhost/api/documents/page?documentId=doc_1&workspaceId=ws_1", {
|
||||
headers: {
|
||||
"authorization": "Bearer route-token",
|
||||
"cookie": "convex-auth=test-cookie",
|
||||
"x-request-id": "req_route",
|
||||
"x-trace-id": "trace_route",
|
||||
"x-session-id": "sess_route",
|
||||
"x-source-channel": "next-route",
|
||||
"x-source-client": "vitest",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const payload = await response.json() as {
|
||||
page: { schema?: string; identity: { documentId: string }; head: { title: string } };
|
||||
meta: { requestId: string; traceId: string; queryName: string };
|
||||
};
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(resolveMnoteWebInternalUrl).toHaveBeenCalled();
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://127.0.0.1:3104/api/page-aggregate/doc_1?workspaceId=ws_1",
|
||||
expect.objectContaining({
|
||||
method: "GET",
|
||||
cache: "no-store",
|
||||
signal: expect.any(AbortSignal),
|
||||
headers: expect.any(Headers),
|
||||
}),
|
||||
);
|
||||
const fetchHeaders = fetchMock.mock.calls[0]?.[1]?.headers as Headers;
|
||||
expect(fetchHeaders.get("accept")).toBe("application/json");
|
||||
expect(fetchHeaders.get("authorization")).toBe("Bearer route-token");
|
||||
expect(fetchHeaders.get("cookie")).toBe("convex-auth=test-cookie");
|
||||
expect(fetchHeaders.get("x-request-id")).toBe("req_route");
|
||||
expect(fetchHeaders.get("x-mnote-request-id")).toBe("req_route");
|
||||
expect(fetchHeaders.get("x-trace-id")).toBe("trace_route");
|
||||
expect(fetchHeaders.get("x-mnote-trace-id")).toBe("trace_route");
|
||||
expect(fetchHeaders.get("x-session-id")).toBe("sess_route");
|
||||
expect(fetchHeaders.get("x-mnote-session-id")).toBe("sess_route");
|
||||
expect(fetchHeaders.get("x-source-channel")).toBe("next-route");
|
||||
expect(fetchHeaders.get("x-mnote-source-channel")).toBe("next-route");
|
||||
expect(fetchHeaders.get("x-source-client")).toBe("vitest");
|
||||
expect(fetchHeaders.get("x-mnote-source-client")).toBe("vitest");
|
||||
expect(getAuthedConvexClient).not.toHaveBeenCalled();
|
||||
expect(payload.page.schema).toBe("mnote.page_aggregate.v1");
|
||||
expect(payload.page.identity.documentId).toBe("doc_1");
|
||||
expect(payload.page.head.title).toBe("Rust 聚合页面");
|
||||
expect(payload.meta).toEqual({
|
||||
requestId: "req_rust",
|
||||
traceId: "trace_rust",
|
||||
queryName: "documents.page.get",
|
||||
});
|
||||
});
|
||||
|
||||
it("Rust snapshot 返回畸形 projection 时保留 TS builder fallback", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
schema: "mnote.page_aggregate.v1",
|
||||
result: {
|
||||
schema: "mnote.page_aggregate.v1",
|
||||
projectionVersion: 1,
|
||||
source: "KernelProjection",
|
||||
identity: { documentId: "doc_1", workspaceId: "ws_1" },
|
||||
head: { title: "错误页面", updatedAt: null },
|
||||
layout: {},
|
||||
body: { content: [], revision: "bad_revision", conflictDetectionKey: 7 },
|
||||
tree: { pageSubtree: null },
|
||||
stats: null,
|
||||
},
|
||||
requestId: "req_bad",
|
||||
traceId: "trace_bad",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await GET(
|
||||
new Request("http://localhost/api/documents/page?documentId=doc_1&workspaceId=ws_1", {
|
||||
headers: {
|
||||
"x-request-id": "req_next",
|
||||
"x-trace-id": "trace_next",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const payload = await response.json() as {
|
||||
page: { schema?: string; head: { title: string }; body: { revision: number | null } };
|
||||
meta: { requestId: string; traceId: string; queryName: string };
|
||||
};
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(getAuthedConvexClient).toHaveBeenCalled();
|
||||
expect(payload.page.schema).toBeUndefined();
|
||||
expect(payload.page.head.title).toBe("Next fallback 页面");
|
||||
expect(payload.page.body.revision).toBe(1);
|
||||
expect(payload.meta).toEqual({
|
||||
requestId: "req_next",
|
||||
traceId: "trace_next",
|
||||
queryName: "documents.page.get",
|
||||
});
|
||||
});
|
||||
|
||||
it("Rust internal base 不可信时直接 fallback,且不会外发凭据", async () => {
|
||||
vi.mocked(resolveMnoteWebInternalUrl).mockResolvedValueOnce("https://example.com");
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch");
|
||||
|
||||
const response = await GET(
|
||||
new Request("http://localhost/api/documents/page?documentId=doc_1&workspaceId=ws_1", {
|
||||
headers: {
|
||||
"authorization": "Bearer route-token",
|
||||
"cookie": "convex-auth=test-cookie",
|
||||
"x-request-id": "req_untrusted",
|
||||
"x-trace-id": "trace_untrusted",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const payload = await response.json() as {
|
||||
page: { schema?: string; head: { title: string } };
|
||||
meta: { requestId: string; traceId: string; queryName: string };
|
||||
};
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(getAuthedConvexClient).toHaveBeenCalled();
|
||||
expect(payload.page.schema).toBeUndefined();
|
||||
expect(payload.page.head.title).toBe("Next fallback 页面");
|
||||
expect(payload.meta).toEqual({
|
||||
requestId: "req_untrusted",
|
||||
traceId: "trace_untrusted",
|
||||
queryName: "documents.page.get",
|
||||
});
|
||||
});
|
||||
|
||||
it("Rust snapshot fetch 抛错时保留 TS builder fallback", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("timeout"));
|
||||
|
||||
const response = await GET(
|
||||
new Request("http://localhost/api/documents/page?documentId=doc_1&workspaceId=ws_1", {
|
||||
headers: {
|
||||
"x-request-id": "req_timeout",
|
||||
"x-trace-id": "trace_timeout",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const payload = await response.json() as {
|
||||
page: { schema?: string; head: { title: string } };
|
||||
meta: { requestId: string; traceId: string; queryName: string };
|
||||
};
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(getAuthedConvexClient).toHaveBeenCalled();
|
||||
expect(payload.page.schema).toBeUndefined();
|
||||
expect(payload.page.head.title).toBe("Next fallback 页面");
|
||||
expect(payload.meta).toEqual({
|
||||
requestId: "req_timeout",
|
||||
traceId: "trace_timeout",
|
||||
queryName: "documents.page.get",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/convex/enabled", () => ({
|
||||
isConvexEnabled: vi.fn(() => true),
|
||||
@@ -28,10 +28,8 @@ vi.mock("@/lib/api-utils", () => ({
|
||||
|
||||
vi.mock("@/lib/documents/page-command-adapter", () => ({
|
||||
executeDocumentCreateChildBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
||||
executeDocumentEmbedBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
||||
executeDocumentTemplateBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
||||
executeDocumentEmptyTrashBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
||||
executeDocumentPurgeBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/documents/page-write-command-adapter", () => ({
|
||||
@@ -72,18 +70,11 @@ vi.mock("@/lib/documents/page-aggregate-loader", () => ({
|
||||
}));
|
||||
|
||||
import { POST as postCreateChild } from "@/app/api/documents/create-child/route";
|
||||
import { POST as postDelete } from "@/app/api/documents/delete/route";
|
||||
import { POST as postEmbed } from "@/app/api/documents/embed/route";
|
||||
import { POST as postTemplate } from "@/app/api/documents/template/route";
|
||||
import { POST as postEmptyTrash } from "@/app/api/documents/empty-trash/route";
|
||||
import { POST as postPurge } from "@/app/api/documents/purge/route";
|
||||
import { POST as postRestore } from "@/app/api/documents/restore/route";
|
||||
import { POST as postTitle } from "@/app/api/documents/title/route";
|
||||
import { POST as postOptions } from "@/app/api/documents/options/route";
|
||||
import { POST as postSave } from "@/app/api/documents/save/route";
|
||||
import { POST as postCopyTree } from "@/app/api/documents/copy-tree/route";
|
||||
import { POST as postCreate } from "@/app/api/documents/create/route";
|
||||
import { POST as postMove } from "@/app/api/documents/move/route";
|
||||
import { GET as getPage } from "@/app/api/documents/page/route";
|
||||
import {
|
||||
executeDocumentCreateChildBridgeCommand,
|
||||
@@ -93,107 +84,12 @@ import {
|
||||
import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter";
|
||||
import { loadPageAggregate } from "@/lib/documents/page-aggregate-loader";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("documents route adapters", () => {
|
||||
it("create route 作为 compat 壳委托 tree commands 主路径", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
requestId: "req_tree_create_1",
|
||||
traceId: "trace_tree_create_1",
|
||||
result: {
|
||||
action: "create",
|
||||
workspaceId: "ws_1",
|
||||
documentId: "doc_new",
|
||||
parentId: null,
|
||||
title: "无标题",
|
||||
sortOrder: 0,
|
||||
updatedAt: "2026-04-23T00:00:00Z",
|
||||
execution: {
|
||||
access_scope: "private",
|
||||
is_template: false,
|
||||
created_at: "2026-04-23T00:00:00Z",
|
||||
updated_at: "2026-04-23T00:00:00Z",
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await postCreate(new Request("http://localhost/api/documents/create", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ parentId: null }),
|
||||
}));
|
||||
const payload = await response.json() as {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
meta: { commandName: string };
|
||||
};
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://localhost/api/tree/commands",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.any(Headers),
|
||||
body: JSON.stringify({
|
||||
action: "create",
|
||||
parentId: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(payload.id).toBe("doc_new");
|
||||
expect(payload.workspace_id).toBe("ws_1");
|
||||
expect(payload.meta.commandName).toBe("tree.node.create");
|
||||
});
|
||||
|
||||
it("move route 作为 compat 壳委托 tree commands 主路径", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
requestId: "req_tree_move_1",
|
||||
traceId: "trace_tree_move_1",
|
||||
result: {
|
||||
action: "move",
|
||||
workspaceId: "ws_1",
|
||||
documentId: "doc_1",
|
||||
parentId: "parent_1",
|
||||
sortOrder: 2,
|
||||
updatedAt: "2026-04-23T00:00:00Z",
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await postMove(new Request("http://localhost/api/documents/move", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ documentId: "doc_1", parentId: "parent_1", position: 2.7, workspaceId: "ws_1" }),
|
||||
}));
|
||||
const payload = await response.json() as {
|
||||
ok: boolean;
|
||||
meta: { commandName: string };
|
||||
};
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://localhost/api/tree/commands",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.any(Headers),
|
||||
body: JSON.stringify({
|
||||
action: "move",
|
||||
documentId: "doc_1",
|
||||
parentId: "parent_1",
|
||||
sortOrder: 2,
|
||||
workspaceId: "ws_1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.meta.commandName).toBe("tree.subtree.move");
|
||||
});
|
||||
|
||||
it("title route 在树重命名兼容请求下委托 tree commands 主路径", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
@@ -260,117 +156,6 @@ describe("documents route adapters", () => {
|
||||
expect(executeDocumentCreateChildBridgeCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delete route 作为 compat 壳委托 tree commands 主路径", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
requestId: "req_tree_archive_1",
|
||||
traceId: "trace_tree_archive_1",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await postDelete(new Request("http://localhost/api/documents/delete", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1" }),
|
||||
}));
|
||||
const payload = await response.json() as {
|
||||
success: boolean;
|
||||
meta: { commandName: string };
|
||||
};
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://localhost/api/tree/commands",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.any(Headers),
|
||||
body: JSON.stringify({
|
||||
action: "archive",
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(payload.success).toBe(true);
|
||||
expect(payload.meta.commandName).toBe("tree.node.archive");
|
||||
});
|
||||
|
||||
it("restore route 作为 compat 壳委托 tree commands 主路径", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
requestId: "req_tree_restore_1",
|
||||
traceId: "trace_tree_restore_1",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await postRestore(new Request("http://localhost/api/documents/restore", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1" }),
|
||||
}));
|
||||
const payload = await response.json() as {
|
||||
success: boolean;
|
||||
meta: { commandName: string };
|
||||
};
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://localhost/api/tree/commands",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.any(Headers),
|
||||
body: JSON.stringify({
|
||||
action: "restore",
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(payload.success).toBe(true);
|
||||
expect(payload.meta.commandName).toBe("tree.node.restore");
|
||||
});
|
||||
|
||||
it("embed route 作为 compat 壳委托 tree commands 主路径", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
requestId: "req_tree_embed_1",
|
||||
traceId: "trace_tree_embed_1",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await postEmbed(new Request("http://localhost/api/documents/embed", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ sourceId: "doc_1", targetId: "doc_2" }),
|
||||
}));
|
||||
const payload = await response.json() as {
|
||||
ok: boolean;
|
||||
meta: { commandName: string };
|
||||
};
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://localhost/api/tree/commands",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.any(Headers),
|
||||
body: JSON.stringify({
|
||||
action: "embed",
|
||||
sourceId: "doc_1",
|
||||
targetId: "doc_2",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.meta.commandName).toBe("tree.node.embed");
|
||||
});
|
||||
|
||||
it("template route delegates to unified adapter", async () => {
|
||||
await postTemplate(new Request("http://localhost/api/documents/template", {
|
||||
method: "POST",
|
||||
@@ -387,87 +172,6 @@ describe("documents route adapters", () => {
|
||||
expect(executeDocumentEmptyTrashBridgeCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("purge route 作为 compat 壳委托 tree commands 主路径", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
requestId: "req_tree_purge_1",
|
||||
traceId: "trace_tree_purge_1",
|
||||
result: {
|
||||
purged: true,
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await postPurge(new Request("http://localhost/api/documents/purge", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ documentId: "doc_1" }),
|
||||
}));
|
||||
const payload = await response.json() as {
|
||||
success: boolean;
|
||||
purged: boolean;
|
||||
meta: { commandName: string };
|
||||
};
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://localhost/api/tree/commands",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.any(Headers),
|
||||
body: JSON.stringify({
|
||||
action: "purge",
|
||||
documentId: "doc_1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(payload.success).toBe(true);
|
||||
expect(payload.purged).toBe(true);
|
||||
expect(payload.meta.commandName).toBe("tree.node.purge");
|
||||
});
|
||||
|
||||
it("copy-tree route 作为 compat 壳委托 tree commands 主路径", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
requestId: "req_tree_copy_1",
|
||||
traceId: "trace_tree_copy_1",
|
||||
result: {
|
||||
items: [{ oldId: "doc_1", newId: "doc_2" }],
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await postCopyTree(new Request("http://localhost/api/documents/copy-tree", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ targetParentId: null, items: [{ documentId: "doc_1", recursive: true }] }),
|
||||
}));
|
||||
const payload = await response.json() as {
|
||||
items: Array<{ oldId: string; newId: string }>;
|
||||
meta: { commandName: string };
|
||||
};
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://localhost/api/tree/commands",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.any(Headers),
|
||||
body: JSON.stringify({
|
||||
action: "copy",
|
||||
targetParentId: null,
|
||||
items: [{ documentId: "doc_1", recursive: true }],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(payload.items).toEqual([{ oldId: "doc_1", newId: "doc_2" }]);
|
||||
expect(payload.meta.commandName).toBe("tree.subtree.copy");
|
||||
});
|
||||
|
||||
it("page route delegates to unified aggregate loader", async () => {
|
||||
const response = await getPage(
|
||||
new Request("http://localhost/api/documents/page?documentId=doc_1&workspaceId=ws_1"),
|
||||
|
||||
Reference in New Issue
Block a user