- 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
1025 lines
30 KiB
TypeScript
1025 lines
30 KiB
TypeScript
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("单条新 domain event 携带 streamDelta 时,应直接发 delta", async () => {
|
|
const loadOverview = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
command_logs: [],
|
|
domain_events: [
|
|
{
|
|
event_id: "evt_1",
|
|
created_at: "2026-04-24T00:00:01Z",
|
|
},
|
|
],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
generated_at: "2026-04-24T00:00:01Z",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
command_logs: [],
|
|
domain_events: [
|
|
{
|
|
event_id: "evt_2",
|
|
created_at: "2026-04-24T00:00:02Z",
|
|
payload: {
|
|
command_name: "tree.node.rename",
|
|
streamDelta: {
|
|
op: "upsert_document",
|
|
document: {
|
|
id: "page_2",
|
|
title: "新标题",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
event_id: "evt_1",
|
|
created_at: "2026-04-24T00:00:01Z",
|
|
},
|
|
],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
generated_at: "2026-04-24T00:00:02Z",
|
|
});
|
|
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: "domain_event:evt_2",
|
|
}),
|
|
data: {
|
|
op: "upsert_document",
|
|
document: {
|
|
id: "page_2",
|
|
title: "新标题",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(loadSnapshot).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("单条资源 domain event 携带 upsert_assets 时,应直接发 delta", async () => {
|
|
const streamDelta = {
|
|
op: "upsert_assets",
|
|
upsertAssets: [
|
|
{
|
|
id: "asset_1",
|
|
workspace_id: "ws_1",
|
|
document_id: "doc_target",
|
|
asset_type: "file",
|
|
file_url: "/file.pdf",
|
|
thumbnail_url: "/file.pdf",
|
|
file_name: "file.pdf",
|
|
file_size: 1024,
|
|
mime_type: "application/pdf",
|
|
created_at: "2026-04-26T00:00:00Z",
|
|
updated_at: "2026-04-26T00:00:00Z",
|
|
},
|
|
],
|
|
};
|
|
const loadOverview = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
command_logs: [],
|
|
domain_events: [
|
|
{
|
|
event_id: "evt_1",
|
|
created_at: "2026-04-24T00:00:01Z",
|
|
},
|
|
],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
generated_at: "2026-04-24T00:00:01Z",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
command_logs: [],
|
|
domain_events: [
|
|
{
|
|
event_id: "evt_2",
|
|
created_at: "2026-04-24T00:00:02Z",
|
|
payload: {
|
|
schema: "mnote.tree.domain_event",
|
|
eventType: "tree.resource.moved",
|
|
streamDelta,
|
|
},
|
|
},
|
|
{
|
|
event_id: "evt_1",
|
|
created_at: "2026-04-24T00:00:01Z",
|
|
},
|
|
],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
generated_at: "2026-04-24T00:00:02Z",
|
|
});
|
|
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: streamDelta,
|
|
},
|
|
});
|
|
expect(loadSnapshot).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("单条新 domain event 缺少可识别 streamDelta 时,应保守回退 resync", async () => {
|
|
const loadOverview = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
command_logs: [],
|
|
domain_events: [
|
|
{
|
|
event_id: "evt_1",
|
|
created_at: "2026-04-24T00:00:01Z",
|
|
},
|
|
],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
generated_at: "2026-04-24T00:00:01Z",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
command_logs: [],
|
|
domain_events: [
|
|
{
|
|
event_id: "evt_2",
|
|
created_at: "2026-04-24T00:00:02Z",
|
|
payload: {
|
|
schema: "mnote.tree.domain_event",
|
|
schemaVersion: 1,
|
|
eventType: "tree.node.unknown",
|
|
},
|
|
},
|
|
{
|
|
event_id: "evt_1",
|
|
created_at: "2026-04-24T00:00:01Z",
|
|
},
|
|
],
|
|
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_2" }] },
|
|
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ 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",
|
|
cursor: JSON.stringify({
|
|
createdAt: "2026-04-24T00:00:02Z",
|
|
id: "domain_event:evt_2",
|
|
}),
|
|
},
|
|
});
|
|
expect(loadSnapshot).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("同一命令的 command log 与 domain event 同时推进且 delta 一致时,应发一次 delta", async () => {
|
|
const streamDelta = {
|
|
op: "upsert_document",
|
|
document: { id: "page_2", title: "同一标题" },
|
|
};
|
|
const loadOverview = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
buildOverview([{ id: "clog_1", command_id: "cmd_1", created_at: "2026-04-24T00:00:01Z" }]),
|
|
)
|
|
.mockResolvedValueOnce({
|
|
command_logs: [
|
|
{
|
|
id: "clog_2",
|
|
command_id: "cmd_2",
|
|
created_at: "2026-04-24T00:00:02Z",
|
|
command_name: "tree.node.rename",
|
|
payload: { streamDelta },
|
|
},
|
|
{ id: "clog_1", command_id: "cmd_1", created_at: "2026-04-24T00:00:01Z" },
|
|
],
|
|
domain_events: [
|
|
{
|
|
event_id: "evt_2",
|
|
command_id: "cmd_2",
|
|
created_at: "2026-04-24T00:00:02Z",
|
|
payload: { streamDelta },
|
|
},
|
|
],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
generated_at: "2026-04-24T00:00:02Z",
|
|
});
|
|
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: streamDelta,
|
|
},
|
|
});
|
|
expect(loadSnapshot).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("上一帧 cursor 来自 command log 时,应按时间排除旧 domain event 后再做双写去重", async () => {
|
|
const streamDelta = {
|
|
op: "move_document",
|
|
documentId: "page_2",
|
|
parentId: "page_1",
|
|
sortOrder: 2,
|
|
};
|
|
const loadOverview = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
buildOverview([{ id: "clog_1", command_id: "cmd_1", created_at: "2026-04-24T00:00:01Z" }]),
|
|
)
|
|
.mockResolvedValueOnce({
|
|
command_logs: [
|
|
{
|
|
id: "clog_2",
|
|
command_id: "cmd_2",
|
|
created_at: "2026-04-24T00:00:02Z",
|
|
command_name: "tree.subtree.move",
|
|
payload: { streamDelta },
|
|
},
|
|
{ id: "clog_1", command_id: "cmd_1", created_at: "2026-04-24T00:00:01Z" },
|
|
],
|
|
domain_events: [
|
|
{
|
|
event_id: "evt_2",
|
|
command_id: "cmd_2",
|
|
created_at: "2026-04-24T00:00:02Z",
|
|
payload: { streamDelta },
|
|
},
|
|
{
|
|
event_id: "evt_1",
|
|
command_id: "cmd_1",
|
|
created_at: "2026-04-24T00:00:01Z",
|
|
payload: {
|
|
streamDelta: {
|
|
op: "noop",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
generated_at: "2026-04-24T00:00:02Z",
|
|
});
|
|
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: streamDelta,
|
|
},
|
|
});
|
|
expect(loadSnapshot).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("command log 与 domain event 同时推进且 delta 不一致时,应回退 resync 避免漏发", async () => {
|
|
const loadOverview = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
|
|
)
|
|
.mockResolvedValueOnce({
|
|
command_logs: [
|
|
{
|
|
id: "clog_2",
|
|
created_at: "2026-04-24T00:00:03Z",
|
|
command_name: "tree.node.rename",
|
|
payload: {
|
|
streamDelta: {
|
|
op: "upsert_document",
|
|
document: { id: "page_2", title: "命令标题" },
|
|
},
|
|
},
|
|
},
|
|
{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
|
|
],
|
|
domain_events: [
|
|
{
|
|
event_id: "evt_2",
|
|
created_at: "2026-04-24T00:00:02Z",
|
|
payload: {
|
|
streamDelta: {
|
|
op: "remove_document",
|
|
documentId: "page_3",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
generated_at: "2026-04-24T00:00:03Z",
|
|
});
|
|
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_2" }] },
|
|
snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ 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("单条新命令缺少可稳定解释的 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 这类附带 move_document 的新命令应直接发 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: "move_document",
|
|
documentId: "page_1",
|
|
parentId: null,
|
|
sortOrder: 0,
|
|
updatedAt: "2026-04-24T00:01:00Z",
|
|
},
|
|
},
|
|
},
|
|
{ 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: "move_document",
|
|
documentId: "page_1",
|
|
parentId: null,
|
|
sortOrder: 0,
|
|
updatedAt: "2026-04-24T00:01:00Z",
|
|
},
|
|
},
|
|
});
|
|
expect(loadSnapshot).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("copy 这类附带 upsert_documents 的新命令应保留批量文档字段", 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.copy",
|
|
payload: {
|
|
streamDelta: {
|
|
op: "upsert_documents",
|
|
upsertDocuments: [
|
|
{
|
|
id: "copy_1",
|
|
workspace_id: "ws_1",
|
|
title: "Copy",
|
|
parent_id: null,
|
|
sort_order: 2,
|
|
is_starred: false,
|
|
access_scope: "private",
|
|
is_template: false,
|
|
created_at: "2026-04-24T00:00:02Z",
|
|
updated_at: "2026-04-24T00:00:02Z",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{ 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[1]).toMatchObject({
|
|
event: "delta",
|
|
payload: {
|
|
kind: "delta",
|
|
data: {
|
|
op: "upsert_documents",
|
|
upsertDocuments: [
|
|
expect.objectContaining({
|
|
id: "copy_1",
|
|
workspace_id: "ws_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" });
|
|
});
|
|
});
|