Files
mnote/wolai-frontend/src/lib/tree-stream/server.test.ts
T

500 lines
15 KiB
TypeScript
Raw Normal View History

2026-04-26 04:29:23 +08:00
import { describe, expect, it, vi } from "vitest";
import { streamTreeFrames } from "./server";
import type { TreeStreamCommandLogCursorRow } from "./server";
function buildOverview(rows: TreeStreamCommandLogCursorRow[]) {
return {
command_logs: rows,
domain_events: [],
has_more: false,
next_cursor: null,
generated_at: "2026-04-24T00:00:00Z",
};
}
async function collectFrames<T>(generator: AsyncGenerator<T>) {
const frames: T[] = [];
for await (const frame of generator) {
frames.push(frame);
}
return frames;
}
describe("tree-stream/server", () => {
it("workspace scope 首帧应发 snapshot,并固定 sidebar_tree + cursor", async () => {
const frames = await collectFrames(
streamTreeFrames({
workspaceId: "ws_1",
pollMs: 1,
maxPolls: 0,
loadOverview: vi.fn().mockResolvedValue(
buildOverview([{ id: "clog_2", created_at: "2026-04-24T00:00:01Z" }]),
),
loadSnapshot: vi.fn().mockResolvedValue({
requestId: "req_1",
traceId: "trace_1",
data: { activeWorkspaceId: "ws_1", documents: [] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] }, tree: { items: [] } },
}),
sleep: vi.fn().mockResolvedValue(undefined),
}),
);
expect(frames).toHaveLength(1);
expect(frames[0]).toMatchObject({
event: "snapshot",
payload: {
kind: "snapshot",
stream: "workspace",
workspaceId: "ws_1",
rootNodeId: null,
projection: "sidebar_tree",
cursor: JSON.stringify({
createdAt: "2026-04-24T00:00:01Z",
id: "clog_2",
}),
},
});
});
it("subtree scope 首帧应切到 subtree + page_tree", async () => {
const frames = await collectFrames(
streamTreeFrames({
workspaceId: "ws_1",
rootNodeId: "page_root",
pollMs: 1,
maxPolls: 0,
loadOverview: vi.fn().mockResolvedValue(
buildOverview([{ id: "clog_2", created_at: "2026-04-24T00:00:01Z" }]),
),
loadSnapshot: vi.fn().mockResolvedValue({
requestId: "req_subtree_1",
traceId: "trace_subtree_1",
data: { nodes: [] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] }, tree: { nodes: [] } },
}),
sleep: vi.fn().mockResolvedValue(undefined),
}),
);
expect(frames).toHaveLength(1);
expect(frames[0]).toMatchObject({
event: "snapshot",
payload: {
kind: "snapshot",
stream: "subtree",
workspaceId: "ws_1",
rootNodeId: "page_root",
projection: "page_tree",
},
});
});
it("检测到 cursor 之后出现新命令时,应发 resync 而不是静默结束", async () => {
const loadOverview = vi
.fn()
.mockResolvedValueOnce(
buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
)
.mockResolvedValueOnce(
buildOverview([
{ id: "clog_2", created_at: "2026-04-24T00:00:02Z" },
{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
]),
);
const loadSnapshot = vi
.fn()
.mockResolvedValueOnce({
requestId: "req_1",
traceId: "trace_1",
data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }] }, tree: { items: [] } },
})
.mockResolvedValueOnce({
requestId: "req_2",
traceId: "trace_2",
data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }, { id: "page_2" }] },
snapshot: {
dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }, { id: "page_2" }] },
tree: { items: [] },
},
});
const frames = await collectFrames(
streamTreeFrames({
workspaceId: "ws_1",
pollMs: 1,
maxPolls: 1,
loadOverview,
loadSnapshot,
sleep: vi.fn().mockResolvedValue(undefined),
}),
);
expect(frames).toHaveLength(2);
expect(frames[0]).toMatchObject({ event: "snapshot" });
expect(frames[1]).toMatchObject({
event: "resync",
payload: {
kind: "resync",
stream: "workspace",
workspaceId: "ws_1",
projection: "sidebar_tree",
cursor: JSON.stringify({
createdAt: "2026-04-24T00:00:02Z",
id: "clog_2",
}),
},
});
expect(loadSnapshot).toHaveBeenCalledTimes(2);
});
it("检测到带 streamDelta 的单条新命令时,应直接发 delta", async () => {
const loadOverview = vi
.fn()
.mockResolvedValueOnce(
buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
)
.mockResolvedValueOnce(
buildOverview([
{
id: "clog_2",
created_at: "2026-04-24T00:00:02Z",
command_name: "tree.node.archive",
payload: {
documentId: "page_2",
streamDelta: {
op: "remove_document",
documentId: "page_2",
},
},
},
{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
]),
);
const loadSnapshot = vi.fn().mockResolvedValue({
requestId: "req_1",
traceId: "trace_1",
data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }] }, tree: { items: [] } },
});
const frames = await collectFrames(
streamTreeFrames({
workspaceId: "ws_1",
pollMs: 1,
maxPolls: 1,
loadOverview,
loadSnapshot,
sleep: vi.fn().mockResolvedValue(undefined),
}),
);
expect(frames).toHaveLength(2);
expect(frames[1]).toMatchObject({
event: "delta",
payload: {
kind: "delta",
cursor: JSON.stringify({
createdAt: "2026-04-24T00:00:02Z",
id: "clog_2",
}),
data: {
op: "remove_document",
documentId: "page_2",
},
},
});
expect(loadSnapshot).toHaveBeenCalledTimes(1);
});
it("只有 domain event 推进时,也应刷新 cursor 并触发 resync", async () => {
const loadOverview = vi
.fn()
.mockResolvedValueOnce(
buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
)
.mockResolvedValueOnce({
command_logs: [{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }],
domain_events: [
{
event_id: "evt_2",
created_at: "2026-04-24T00:00:02Z",
},
],
has_more: false,
next_cursor: null,
generated_at: "2026-04-24T00:00:02Z",
});
const loadSnapshot = vi
.fn()
.mockResolvedValueOnce({
requestId: "req_1",
traceId: "trace_1",
data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }] }, tree: { items: [] } },
})
.mockResolvedValueOnce({
requestId: "req_2",
traceId: "trace_2",
data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }] }, tree: { items: [] } },
});
const frames = await collectFrames(
streamTreeFrames({
workspaceId: "ws_1",
pollMs: 1,
maxPolls: 1,
loadOverview,
loadSnapshot,
sleep: vi.fn().mockResolvedValue(undefined),
}),
);
expect(frames).toHaveLength(2);
expect(frames[1]).toMatchObject({
event: "resync",
payload: {
kind: "resync",
cursor: JSON.stringify({
createdAt: "2026-04-24T00:00:02Z",
id: "domain_event:evt_2",
}),
},
});
expect(loadSnapshot).toHaveBeenCalledTimes(2);
});
it("单条新命令缺少可稳定解释的 streamDelta 时,应回退 resync", async () => {
const loadOverview = vi
.fn()
.mockResolvedValueOnce(
buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
)
.mockResolvedValueOnce(
buildOverview([
{
id: "clog_2",
created_at: "2026-04-24T00:00:02Z",
command_name: "tree.subtree.move",
payload: {
documentId: "page_2",
},
},
{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
]),
);
const loadSnapshot = vi
.fn()
.mockResolvedValueOnce({
requestId: "req_1",
traceId: "trace_1",
data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }] }, tree: { items: [] } },
})
.mockResolvedValueOnce({
requestId: "req_2",
traceId: "trace_2",
data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }, { id: "page_2" }] },
snapshot: {
dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }, { id: "page_2" }] },
tree: { items: [] },
},
});
const frames = await collectFrames(
streamTreeFrames({
workspaceId: "ws_1",
pollMs: 1,
maxPolls: 1,
loadOverview,
loadSnapshot,
sleep: vi.fn().mockResolvedValue(undefined),
}),
);
expect(frames).toHaveLength(2);
expect(frames[1]).toMatchObject({
event: "resync",
payload: {
kind: "resync",
},
});
expect(loadSnapshot).toHaveBeenCalledTimes(2);
});
it("正文保存这类无树结构影响的命令应降级为 noop delta,而不是触发 resync", async () => {
const loadOverview = vi
.fn()
.mockResolvedValueOnce(
buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
)
.mockResolvedValueOnce(
buildOverview([
{
id: "clog_2",
created_at: "2026-04-24T00:00:02Z",
command_name: "page.body.save",
payload: {
documentId: "page_1",
},
},
{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
]),
);
const loadSnapshot = vi.fn().mockResolvedValue({
requestId: "req_1",
traceId: "trace_1",
data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }] }, tree: { items: [] } },
});
const frames = await collectFrames(
streamTreeFrames({
workspaceId: "ws_1",
pollMs: 1,
maxPolls: 1,
loadOverview,
loadSnapshot,
sleep: vi.fn().mockResolvedValue(undefined),
}),
);
expect(frames).toHaveLength(2);
expect(frames[1]).toMatchObject({
event: "delta",
payload: {
kind: "delta",
data: {
op: "noop",
},
},
});
expect(loadSnapshot).toHaveBeenCalledTimes(1);
});
it("move 这类附带 replace_documents 的新命令应直接发 delta,而不是触发 resync", async () => {
const sidebarSnapshot = {
activeWorkspaceId: "ws_1",
workspaces: [],
documents: [
{
id: "page_1",
workspace_id: "ws_1",
title: "页面 1",
parent_id: null,
sort_order: 0,
access_scope: "private",
is_starred: false,
is_template: false,
created_at: "2026-04-24T00:00:00Z",
updated_at: "2026-04-24T00:01:00Z",
},
],
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: [],
},
kernelSidebarTree: [],
trashedDocuments: [],
mediaAssets: [],
mindmapDocs: [],
mindmapAssets: [],
mindmapAssetChildren: {},
tableAssets: [],
};
const loadOverview = vi
.fn()
.mockResolvedValueOnce(
buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
)
.mockResolvedValueOnce(
buildOverview([
{
id: "clog_2",
created_at: "2026-04-24T00:00:02Z",
command_name: "tree.subtree.move",
payload: {
documentId: "page_1",
streamDelta: {
op: "replace_documents",
documents: sidebarSnapshot.documents,
},
},
},
{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
]),
);
const loadSnapshot = vi.fn().mockResolvedValue({
requestId: "req_1",
traceId: "trace_1",
data: { activeWorkspaceId: "ws_1", documents: [] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] }, tree: { items: [] } },
});
const frames = await collectFrames(
streamTreeFrames({
workspaceId: "ws_1",
pollMs: 1,
maxPolls: 1,
loadOverview,
loadSnapshot,
sleep: vi.fn().mockResolvedValue(undefined),
}),
);
expect(frames).toHaveLength(2);
expect(frames[1]).toMatchObject({
event: "delta",
payload: {
kind: "delta",
data: {
op: "replace_documents",
documents: expect.arrayContaining([
expect.objectContaining({
id: "page_1",
}),
]),
},
},
});
expect(loadSnapshot).toHaveBeenCalledTimes(1);
});
it("轮询期间没有新 cursor 时,不应额外发 resync", async () => {
const loadOverview = vi
.fn()
.mockResolvedValue(buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]));
const frames = await collectFrames(
streamTreeFrames({
workspaceId: "ws_1",
pollMs: 1,
maxPolls: 1,
loadOverview,
loadSnapshot: vi.fn().mockResolvedValue({
requestId: "req_1",
traceId: "trace_1",
data: { activeWorkspaceId: "ws_1", documents: [] },
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] }, tree: { items: [] } },
}),
sleep: vi.fn().mockResolvedValue(undefined),
}),
);
expect(frames).toHaveLength(1);
expect(frames[0]).toMatchObject({ event: "snapshot" });
});
});