4-10 树域 Rust 家族化

This commit is contained in:
lix-2026
2026-04-24 06:10:18 +08:00
parent 41e958769e
commit 94631f3636
49 changed files with 5751 additions and 557 deletions
@@ -5,6 +5,11 @@ const mockBuildDocumentBridgeContext = vi.fn();
const mockBuildDocumentQueryEnvelope = vi.fn();
const mockExecuteRustBridgeQueryTransport = vi.fn();
const mockResolveRustBridgeQueryPlan = vi.fn();
const mockResolveKernelFileTreeProjection = vi.fn();
const mockAttachKernelFileTreeProjection = vi.fn((input: { dataset: unknown; projection: unknown }) => ({
...(input.dataset as Record<string, unknown>),
kernel_file_tree_projection: input.projection,
}));
const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) =>
Response.json(
{
@@ -29,6 +34,11 @@ vi.mock("@/lib/documents/rust-runtime", () => ({
resolveRustBridgeQueryPlan: mockResolveRustBridgeQueryPlan,
}));
vi.mock("@/lib/server/kernel-file-tree", () => ({
resolveKernelFileTreeProjection: (...args: unknown[]) => mockResolveKernelFileTreeProjection(...args),
attachKernelFileTreeProjection: (...args: unknown[]) => mockAttachKernelFileTreeProjection(...args),
}));
describe("/api/mnote-web/stream route", () => {
beforeEach(() => {
mockGetAuthedConvexClient.mockReset();
@@ -36,11 +46,18 @@ describe("/api/mnote-web/stream route", () => {
mockBuildDocumentQueryEnvelope.mockReset();
mockExecuteRustBridgeQueryTransport.mockReset();
mockResolveRustBridgeQueryPlan.mockReset();
mockResolveKernelFileTreeProjection.mockReset();
mockAttachKernelFileTreeProjection.mockClear();
mockDocumentBridgeErrorResponse.mockClear();
});
it("直接在 3000 内生成 snapshot SSE,不再回源 mnote-web", async () => {
mockGetAuthedConvexClient.mockResolvedValue({
auth: {
userId: "user_1",
email: "dev@example.com",
name: "开发用户",
},
client: { query: vi.fn() },
});
mockBuildDocumentBridgeContext.mockResolvedValue({
@@ -107,6 +124,13 @@ describe("/api/mnote-web/stream route", () => {
has_more: false,
generated_at: "2026-04-22T00:00:00Z",
});
mockResolveKernelFileTreeProjection.mockResolvedValue({
projectionId: "kernel_projection:file_tree:root",
projection: "file_tree",
rootNodeId: null,
items: [],
edges: [],
});
const { GET } = await import("./route");
const response = await GET(
@@ -124,7 +148,103 @@ describe("/api/mnote-web/stream route", () => {
expect(text).toContain('"projection":"sidebar_tree"');
expect(text).toContain('"workspaceId":"ws_1"');
expect(text).toContain('"activeWorkspaceId":"ws_1"');
expect(text).toContain('"kernelFileTreeProjection"');
expect(mockResolveRustBridgeQueryPlan).toHaveBeenCalledTimes(2);
expect(mockExecuteRustBridgeQueryTransport).toHaveBeenCalledTimes(2);
expect(mockResolveKernelFileTreeProjection).toHaveBeenCalledTimes(1);
});
it("应把请求 cursor 继续透传到 overview query 和 snapshot envelope", async () => {
mockGetAuthedConvexClient.mockResolvedValue({
auth: {
userId: "user_1",
email: "dev@example.com",
name: "开发用户",
},
client: { query: vi.fn() },
});
mockBuildDocumentBridgeContext.mockResolvedValue({
requestId: "req_stream_2",
traceId: "trace_stream_2",
workspaceId: "ws_1",
});
mockBuildDocumentQueryEnvelope
.mockReturnValueOnce({
name: "sidebar.dataset.list",
payload: { workspaceId: "ws_1" },
})
.mockReturnValueOnce({
name: "bridge.workspace.overview",
payload: {
workspaceId: "ws_1",
limit: 20,
cursor: "evt_9",
commandStatus: null,
eventStatus: null,
targetPageId: null,
targetBlockId: null,
aggregateType: null,
aggregateId: null,
},
});
mockResolveRustBridgeQueryPlan
.mockResolvedValueOnce({ kind: "query", functionName: "sidebar:datasetList", argsJson: { workspaceId: "ws_1" } })
.mockResolvedValueOnce({
kind: "query",
functionName: "bridgeLogs:listWorkspaceOverview",
argsJson: { workspaceId: "ws_1", cursor: "evt_9" },
});
mockExecuteRustBridgeQueryTransport
.mockResolvedValueOnce({
active_workspace_id: "ws_1",
workspaces: [],
documents: [],
trashed_documents: [],
media_assets: [],
trashed_media_assets: [],
mindmap_assets: [],
trashed_mindmap_assets: [],
table_assets: [],
trashed_table_assets: [],
mindmap_docs: [],
mindmap_asset_children: {},
})
.mockResolvedValueOnce({
workspace_id: "ws_1",
command_logs: [],
domain_events: [],
counts: { command_logs: 0, domain_events: 0 },
filters: null,
next_cursor: "evt_10",
has_more: true,
generated_at: "2026-04-22T00:00:00Z",
});
mockResolveKernelFileTreeProjection.mockResolvedValue({
projectionId: "kernel_projection:file_tree:root",
projection: "file_tree",
rootNodeId: null,
items: [],
edges: [],
});
const { GET } = await import("./route");
const response = await GET(
new Request("http://127.0.0.1:3000/api/mnote-web/stream?workspaceId=ws_1&cursor=evt_9", {
method: "GET",
}),
);
expect(response.status).toBe(200);
const text = await response.text();
expect(text).toContain('"cursor":"evt_9"');
expect(mockBuildDocumentQueryEnvelope).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
payload: expect.objectContaining({
workspaceId: "ws_1",
cursor: "evt_9",
}),
}),
);
});
});
@@ -9,6 +9,10 @@ import {
resolveRustBridgeQueryPlan,
} from "@/lib/documents/rust-runtime";
import { mapSidebarDatasetListQueryResultToInitialData } from "@/lib/sidebar-data";
import {
attachKernelFileTreeProjection,
resolveKernelFileTreeProjection,
} from "@/lib/server/kernel-file-tree";
export const dynamic = "force-dynamic";
@@ -26,7 +30,7 @@ export async function GET(request: Request) {
return Response.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const { client } = await getAuthedConvexClient();
const { auth, client } = await getAuthedConvexClient();
const context = await buildDocumentBridgeContext({
request,
workspaceId,
@@ -46,6 +50,20 @@ export async function GET(request: Request) {
client,
plan: sidebarPlan,
});
const sidebarDatasetWithFileTree = attachKernelFileTreeProjection({
dataset: sidebarDataset,
projection: await resolveKernelFileTreeProjection({
client,
request,
workspaceId,
actor: {
actorType: "user",
actorId: auth.userId,
sessionId: null,
},
dataset: sidebarDataset,
}),
});
const overviewEnvelope = buildDocumentQueryEnvelope({
name: "bridge.workspace.overview",
@@ -79,10 +97,13 @@ export async function GET(request: Request) {
cursor,
requestId: context.requestId,
traceId: context.traceId,
data: mapSidebarDatasetListQueryResultToInitialData(sidebarDataset),
data: mapSidebarDatasetListQueryResultToInitialData(sidebarDatasetWithFileTree),
snapshot: {
dataset: sidebarDataset,
tree: sidebarDataset.kernel_sidebar_projection ?? sidebarDataset.kernelSidebarProjection ?? null,
dataset: sidebarDatasetWithFileTree,
tree:
sidebarDatasetWithFileTree.kernel_sidebar_projection ??
sidebarDatasetWithFileTree.kernelSidebarProjection ??
null,
},
overview,
};