2026-04-30 06:58:17 +08:00
|
|
|
pub mod ai;
|
2026-04-14 13:22:29 +08:00
|
|
|
pub mod command;
|
|
|
|
|
pub mod common;
|
2026-04-19 21:03:25 +08:00
|
|
|
pub mod editor;
|
2026-04-14 13:22:29 +08:00
|
|
|
pub mod governance;
|
2026-04-16 22:01:51 +08:00
|
|
|
pub mod kernel;
|
2026-04-15 20:01:12 +08:00
|
|
|
pub mod mindmap;
|
2026-04-30 05:46:36 +08:00
|
|
|
pub mod page_aggregate;
|
2026-04-14 13:22:29 +08:00
|
|
|
pub mod query;
|
2026-04-30 05:46:36 +08:00
|
|
|
pub mod search;
|
2026-04-14 13:22:29 +08:00
|
|
|
pub mod tool;
|
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
pub use ai::{
|
|
|
|
|
AiEvent, AiEventKind, AiRuntimeOwner, AiSession, AiStructuredWriteKind,
|
|
|
|
|
AiStructuredWriteResult, AiToolCall,
|
|
|
|
|
};
|
2026-04-14 13:22:29 +08:00
|
|
|
pub use command::{
|
2026-04-15 20:01:12 +08:00
|
|
|
CommandEnvelope, CopyTreeDocumentPages, CreateDocumentPage, CreatePage, CreateWorkspace,
|
|
|
|
|
DeleteBlock, DeleteDocumentPage, DuplicateDocumentPage, EmbedBlock, InsertBlock, MoveBlock,
|
|
|
|
|
MoveDocumentPage, PatchBlock, PatchPageBlock, PutMindmap, ReplaceMediaAssetStorage,
|
|
|
|
|
RestoreDocumentPage, SavePageContent, UpdateBlock, UpdatePageOptions, UpdatePageStats,
|
|
|
|
|
UpdatePageTitle,
|
2026-04-14 13:22:29 +08:00
|
|
|
};
|
|
|
|
|
pub use common::{
|
|
|
|
|
ActorPayload, AffectedObject, ErrorDetail, ErrorPayload, OkPayload, RequestMeta, ResponseMeta,
|
|
|
|
|
SourcePayload, TargetRef,
|
|
|
|
|
};
|
2026-04-19 21:03:25 +08:00
|
|
|
pub use editor::{
|
|
|
|
|
BlockProps, ContentNode, ContentNodePayload, EditorAttachReferenceToken, EditorBlock,
|
|
|
|
|
EditorBlockDocument, EditorBlockType, EditorCommand, EditorCommandCatalog,
|
|
|
|
|
EditorCommandDescriptor, EditorCommandKind, EditorDeleteBlock, EditorDetachReferenceToken,
|
|
|
|
|
EditorIndentBlock, EditorInsertBlockAfter, EditorKernelCommandMapping, EditorMergeWithPrevious,
|
|
|
|
|
EditorMoveBlock, EditorOutdentBlock, EditorReplaceBlock, EditorSplitBlock,
|
|
|
|
|
EditorToggleHeadingCollapse, MarkdownExportBoundary, MarkdownExportMode, MarkdownExportOptions,
|
|
|
|
|
MarkdownExportRequest, MarkdownExportResult, MarkdownFlavor, MarkdownImportBoundary,
|
|
|
|
|
MarkdownImportMode, MarkdownImportOptions, MarkdownImportRequest, MarkdownImportResult,
|
|
|
|
|
ReferenceToken, ReferenceTokenKind, ReferenceTokenStrategy, TextMark,
|
|
|
|
|
};
|
2026-04-16 22:01:51 +08:00
|
|
|
pub use kernel::{
|
2026-04-19 21:03:25 +08:00
|
|
|
DocumentContentResult, DocumentReadEvidenceItem, DocumentReadEvidenceKind, DocumentReadNode,
|
|
|
|
|
DocumentReadNodeMeta, DocumentReadNodeType, DocumentReadOutlineEntry, DocumentReadPageSubtree,
|
|
|
|
|
DocumentReadStats, DocumentReadSubtree, KernelAttachEdge, KernelAuditStamp,
|
2026-05-16 07:38:45 +08:00
|
|
|
KernelBlockAssetRelation, KernelContentPayload, KernelCreateNode, KernelDetachEdge, KernelEdge,
|
|
|
|
|
KernelEdgeListResult, KernelEdgeType, KernelGetNode, KernelGetSubtree, KernelGraphDirection,
|
|
|
|
|
KernelGraphTraversalResult, KernelGraphVisit, KernelListChildren, KernelListEdges,
|
|
|
|
|
KernelMoveSubtree, KernelNode, KernelNodeMetadata, KernelNodeType, KernelObjectIdentity,
|
|
|
|
|
KernelObjectKind, KernelProjectionAssetKind, KernelProjectionCapability,
|
|
|
|
|
KernelProjectionFilter, KernelProjectionItem, KernelProjectionKind, KernelProjectionRequest,
|
|
|
|
|
KernelProjectionResourceKind, KernelProjectionResourceMeta, KernelProjectionResult,
|
|
|
|
|
KernelProjectionRowKind, KernelRefsPayload, KernelSubtreeRef, KernelSubtreeResult,
|
|
|
|
|
KernelTraverseGraph, KernelUpdateNode, WorkspaceSource, WorkspaceSourceCapability,
|
|
|
|
|
WorkspaceSourceKind,
|
2026-04-16 22:01:51 +08:00
|
|
|
};
|
2026-04-30 05:46:36 +08:00
|
|
|
pub use mindmap::{
|
2026-05-11 12:27:40 +08:00
|
|
|
MindmapAdapterProjection, MindmapAssociativeLine, MindmapCommand, MindmapKernelCapabilities,
|
|
|
|
|
MindmapKernelCommand, MindmapKernelEdge, MindmapKernelNode, MindmapKernelProjection,
|
|
|
|
|
MindmapNodeData, MindmapNodeInput, MindmapNodeRef, MindmapOp, MindmapProjection,
|
|
|
|
|
MindmapProjectionEdge, MindmapProjectionNode, MindmapProjectionOwner, MindmapProjectionSource,
|
|
|
|
|
MindmapSummary, MindmapTreeNode,
|
2026-04-30 05:46:36 +08:00
|
|
|
};
|
|
|
|
|
pub use page_aggregate::{
|
|
|
|
|
PageAggregateProjection, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout,
|
|
|
|
|
PageOptions, PagePermissions, PageStats, PageTree,
|
|
|
|
|
};
|
2026-04-15 03:06:29 +08:00
|
|
|
pub use query::{
|
2026-04-15 20:01:12 +08:00
|
|
|
GetBlock, GetBridgeCommand, GetBridgeRequest, GetBridgeTrace, GetMindmap, GetPage,
|
2026-04-19 21:03:25 +08:00
|
|
|
GetPageContent, GetPageMeta, ListBridgeWorkspaceOverview, ListPageBlocks, ListSidebarDataset,
|
|
|
|
|
QueryEnvelope, SearchBlocks, SearchDocuments, SearchPages, SearchRecent,
|
2026-04-15 20:01:12 +08:00
|
|
|
};
|
2026-04-30 05:46:36 +08:00
|
|
|
pub use search::{
|
|
|
|
|
SearchHighlight, SearchProjectionOwner, SearchQuery, SearchResultProjection, SearchScope,
|
|
|
|
|
};
|
2026-04-15 20:01:12 +08:00
|
|
|
pub use tool::{
|
|
|
|
|
default_tool_registry, invocation_kind_label, tool_effect_label, tool_mode_label,
|
|
|
|
|
InvocationKind, ToolEffect, ToolExecutionMode, ToolInvocation, ToolRegistry, ToolSetSpec,
|
|
|
|
|
ToolSpec, BRIDGE_TOOL_COMMAND_GET, BRIDGE_TOOL_REQUEST_GET, BRIDGE_TOOL_TRACE_GET,
|
2026-04-19 21:03:25 +08:00
|
|
|
DOCS_TOOLSET_READ, DOCS_TOOL_READ, DOCS_TOOL_SEARCH, DOC_TOOLSET_READ, DOC_TOOLSET_WRITE,
|
|
|
|
|
DOC_TOOL_FIND, DOC_TOOL_GET, DOC_TOOL_INSERT_BLOCKS, DOC_TOOL_REPLACE_RANGE,
|
|
|
|
|
INDEX_TOOL_REBUILD, MINDMAP_TOOLSET_READ, MINDMAP_TOOLSET_WRITE, MINDMAP_TOOL_APPLY_OPS,
|
|
|
|
|
MINDMAP_TOOL_EMPTY_TRASH, MINDMAP_TOOL_EXPAND_NODE, MINDMAP_TOOL_GET, MINDMAP_TOOL_GET_SUBTREE,
|
|
|
|
|
MINDMAP_TOOL_OUTLINE_TO_MINDMAP, MINDMAP_TOOL_PUT, OBSERVE_TOOLSET_READ,
|
|
|
|
|
ONLYOFFICE_TOOLSET_SERVICE, ONLYOFFICE_TOOL_PREPARE_CALLBACK,
|
2026-04-15 20:01:12 +08:00
|
|
|
ONLYOFFICE_TOOL_PREPARE_FORCESAVE, ONLYOFFICE_TOOL_PREPARE_PROXY,
|
2026-04-19 21:03:25 +08:00
|
|
|
ONLYOFFICE_TOOL_SESSION_RESOLVE, ONLYOFFICE_TOOL_SIGN, RECOVERY_TOOLSET_JOB,
|
|
|
|
|
REPLAY_TOOL_EVENTS,
|
2026-04-15 03:06:29 +08:00
|
|
|
};
|
2026-04-14 13:22:29 +08:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn command_envelope_keeps_validate_only_flag() {
|
|
|
|
|
let envelope = CommandEnvelope::<CreateWorkspace> {
|
|
|
|
|
name: "create_workspace".into(),
|
|
|
|
|
command_id: "cmd_1".into(),
|
|
|
|
|
idempotency_key: Some("idem_1".into()),
|
|
|
|
|
actor: ActorPayload {
|
|
|
|
|
actor_type: "human".into(),
|
|
|
|
|
actor_id: "user_1".into(),
|
|
|
|
|
session_id: Some("session_1".into()),
|
|
|
|
|
},
|
|
|
|
|
source: SourcePayload {
|
|
|
|
|
channel: "cli".into(),
|
|
|
|
|
client: "mnote-cli".into(),
|
|
|
|
|
},
|
|
|
|
|
target: None,
|
|
|
|
|
payload: CreateWorkspace {
|
|
|
|
|
name: "demo".into(),
|
|
|
|
|
slug: Some("demo".into()),
|
|
|
|
|
},
|
|
|
|
|
reason: Some("初始化工作区".into()),
|
|
|
|
|
refs: vec![],
|
|
|
|
|
dry_run: false,
|
|
|
|
|
validate_only: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(envelope.validate_only);
|
|
|
|
|
}
|
2026-04-15 20:01:12 +08:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn default_tool_registry_exposes_doc_tools() {
|
|
|
|
|
let registry = default_tool_registry();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
registry.tool_names(),
|
|
|
|
|
vec![
|
|
|
|
|
"search_web",
|
|
|
|
|
"doc_get",
|
|
|
|
|
"doc_find",
|
2026-04-16 15:24:37 +08:00
|
|
|
"docs_search",
|
|
|
|
|
"docs_read",
|
2026-04-15 20:01:12 +08:00
|
|
|
"image_read",
|
|
|
|
|
"doc_insert_blocks",
|
|
|
|
|
"doc_replace_range",
|
|
|
|
|
"slash_run",
|
|
|
|
|
"mindmap_get",
|
|
|
|
|
"mindmap_get_subtree",
|
|
|
|
|
"mindmap_put",
|
|
|
|
|
"mindmap_apply_ops",
|
|
|
|
|
"mindmap_expand_node",
|
|
|
|
|
"mindmap_empty_trash",
|
|
|
|
|
"mindmap_outline_to_mindmap",
|
|
|
|
|
"bridge_request_get",
|
|
|
|
|
"bridge_trace_get",
|
|
|
|
|
"bridge_command_get",
|
|
|
|
|
"event_replay",
|
|
|
|
|
"index_rebuild",
|
|
|
|
|
"onlyoffice_session_resolve",
|
|
|
|
|
"onlyoffice_sign",
|
|
|
|
|
"onlyoffice_prepare_proxy",
|
|
|
|
|
"onlyoffice_prepare_callback",
|
|
|
|
|
"onlyoffice_prepare_forcesave",
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
registry
|
|
|
|
|
.tool("search_web")
|
|
|
|
|
.expect("search_web should exist")
|
|
|
|
|
.toolset_id,
|
|
|
|
|
"toolset.readonly"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
registry
|
|
|
|
|
.tool("image_read")
|
|
|
|
|
.expect("image_read should exist")
|
|
|
|
|
.toolset_id,
|
|
|
|
|
"toolset.media_read"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
registry
|
|
|
|
|
.tool("slash_run")
|
|
|
|
|
.expect("slash_run should exist")
|
|
|
|
|
.toolset_id,
|
|
|
|
|
"toolset.slash_write"
|
|
|
|
|
);
|
|
|
|
|
let readonly = registry
|
|
|
|
|
.toolset("toolset.readonly")
|
|
|
|
|
.expect("readonly toolset should exist");
|
|
|
|
|
assert!(!readonly.write_toolset);
|
|
|
|
|
assert_eq!(readonly.tool_names, &["search_web"]);
|
|
|
|
|
let media = registry
|
|
|
|
|
.toolset("toolset.media_read")
|
|
|
|
|
.expect("media toolset should exist");
|
|
|
|
|
assert!(!media.write_toolset);
|
|
|
|
|
assert_eq!(media.tool_names, &["image_read"]);
|
|
|
|
|
let slash = registry
|
|
|
|
|
.toolset("toolset.slash_write")
|
|
|
|
|
.expect("slash toolset should exist");
|
|
|
|
|
assert!(slash.write_toolset);
|
|
|
|
|
assert_eq!(slash.tool_names, &["slash_run"]);
|
2026-04-16 15:24:37 +08:00
|
|
|
let docs_read = registry
|
|
|
|
|
.toolset("toolset.docs_read")
|
|
|
|
|
.expect("docs_read toolset should exist");
|
|
|
|
|
assert!(!docs_read.write_toolset);
|
|
|
|
|
assert_eq!(docs_read.tool_names, &["docs_search", "docs_read"]);
|
2026-04-15 20:01:12 +08:00
|
|
|
let doc_write = registry
|
|
|
|
|
.toolset("toolset.doc_write")
|
|
|
|
|
.expect("doc_write toolset should exist");
|
|
|
|
|
assert!(doc_write.write_toolset);
|
|
|
|
|
assert_eq!(
|
2026-04-19 21:03:25 +08:00
|
|
|
doc_write.tool_names,
|
|
|
|
|
&["doc_insert_blocks", "doc_replace_range"]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
registry
|
|
|
|
|
.tool("doc_get")
|
|
|
|
|
.expect("doc_get should exist")
|
|
|
|
|
.toolset_id,
|
2026-04-15 20:01:12 +08:00
|
|
|
"toolset.doc_read"
|
|
|
|
|
);
|
|
|
|
|
let mindmap_write = registry
|
|
|
|
|
.toolset("toolset.mindmap_write")
|
|
|
|
|
.expect("mindmap_write toolset should exist");
|
|
|
|
|
assert!(mindmap_write.write_toolset);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
mindmap_write.tool_names,
|
|
|
|
|
&[
|
|
|
|
|
"mindmap_put",
|
|
|
|
|
"mindmap_apply_ops",
|
|
|
|
|
"mindmap_expand_node",
|
|
|
|
|
"mindmap_empty_trash",
|
|
|
|
|
"mindmap_outline_to_mindmap",
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
let onlyoffice_service = registry
|
|
|
|
|
.toolset("toolset.onlyoffice_service")
|
|
|
|
|
.expect("onlyoffice service toolset should exist");
|
|
|
|
|
assert!(onlyoffice_service.write_toolset);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
onlyoffice_service.tool_names,
|
|
|
|
|
&[
|
|
|
|
|
"onlyoffice_session_resolve",
|
|
|
|
|
"onlyoffice_sign",
|
|
|
|
|
"onlyoffice_prepare_proxy",
|
|
|
|
|
"onlyoffice_prepare_callback",
|
|
|
|
|
"onlyoffice_prepare_forcesave",
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
let observe = registry
|
|
|
|
|
.toolset("toolset.observe_read")
|
|
|
|
|
.expect("observe toolset should exist");
|
|
|
|
|
assert!(!observe.write_toolset);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
observe.tool_names,
|
2026-04-19 21:03:25 +08:00
|
|
|
&[
|
|
|
|
|
"bridge_request_get",
|
|
|
|
|
"bridge_trace_get",
|
|
|
|
|
"bridge_command_get"
|
|
|
|
|
]
|
2026-04-15 20:01:12 +08:00
|
|
|
);
|
|
|
|
|
let recovery = registry
|
|
|
|
|
.toolset("toolset.recovery_job")
|
|
|
|
|
.expect("recovery toolset should exist");
|
|
|
|
|
assert!(recovery.write_toolset);
|
|
|
|
|
assert_eq!(recovery.tool_names, &["event_replay", "index_rebuild"]);
|
|
|
|
|
}
|
2026-04-30 05:46:36 +08:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn page_aggregate_projection_contract_covers_kernel_owned_fields() {
|
|
|
|
|
let projection = PageAggregateProjection {
|
|
|
|
|
schema: "mnote.page_aggregate.v1".into(),
|
|
|
|
|
projection_version: 1,
|
|
|
|
|
source: PageAggregateSource::KernelProjection,
|
|
|
|
|
page_id: "page_1".into(),
|
|
|
|
|
parent_id: Some("root".into()),
|
|
|
|
|
title: "Rust 页面".into(),
|
|
|
|
|
path: vec!["root".into(), "page_1".into()],
|
|
|
|
|
sidebar_tree_membership: vec!["workspace-sidebar".into()],
|
|
|
|
|
body_ref: Some("page_1:body".into()),
|
|
|
|
|
layout_options: serde_json::json!({"wideLayout": true}),
|
|
|
|
|
updated_at: Some("2026-04-29T00:00:00Z".into()),
|
|
|
|
|
identity: page_aggregate::PageIdentity {
|
|
|
|
|
document_id: "page_1".into(),
|
|
|
|
|
workspace_id: "ws_demo".into(),
|
|
|
|
|
},
|
|
|
|
|
head: page_aggregate::PageHead {
|
|
|
|
|
title: "Rust 页面".into(),
|
|
|
|
|
updated_at: serde_json::json!("2026-04-29T00:00:00Z"),
|
|
|
|
|
permissions: page_aggregate::PagePermissions {
|
|
|
|
|
read_only: false,
|
|
|
|
|
disable_download: false,
|
|
|
|
|
disable_copy: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
layout: page_aggregate::PageLayout {
|
|
|
|
|
page_options: page_aggregate::PageOptions::default(),
|
|
|
|
|
},
|
|
|
|
|
body: page_aggregate::PageBody {
|
|
|
|
|
content: serde_json::json!([]),
|
|
|
|
|
revision: serde_json::json!(7),
|
|
|
|
|
conflict_detection_key: serde_json::json!("page_1:7"),
|
|
|
|
|
},
|
|
|
|
|
tree: page_aggregate::PageTree {
|
|
|
|
|
page_subtree: serde_json::json!({"rootNodeId": "page_1"}),
|
|
|
|
|
},
|
|
|
|
|
stats: page_aggregate::PageStats::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(projection.source, PageAggregateSource::KernelProjection);
|
|
|
|
|
assert_eq!(projection.projection_version, 1);
|
|
|
|
|
assert_eq!(projection.page_id, "page_1");
|
|
|
|
|
assert_eq!(projection.identity.document_id, "page_1");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn search_projection_contract_marks_owner_and_scope() {
|
|
|
|
|
let result = SearchResultProjection {
|
|
|
|
|
id: "page_1".into(),
|
|
|
|
|
title: "Rust 搜索".into(),
|
|
|
|
|
snippet: "Rust Web".into(),
|
|
|
|
|
updated_at: Some("2026-04-29T00:00:00Z".into()),
|
|
|
|
|
created_at: None,
|
|
|
|
|
match_field: "title".into(),
|
|
|
|
|
has_ocr: false,
|
|
|
|
|
public_path: "/documents/page_1".into(),
|
|
|
|
|
node_id: Some("page_1".into()),
|
|
|
|
|
subtree_root_id: Some("page_1".into()),
|
|
|
|
|
evidence: vec![],
|
|
|
|
|
score: 3.0,
|
|
|
|
|
projection_owner: SearchProjectionOwner::RustKernel,
|
|
|
|
|
};
|
|
|
|
|
let query = SearchQuery {
|
|
|
|
|
query: "Rust".into(),
|
|
|
|
|
workspace_id: "ws_demo".into(),
|
|
|
|
|
scope: SearchScope::Workspace,
|
|
|
|
|
page_id: None,
|
|
|
|
|
limit: 20,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(query.scope, SearchScope::Workspace);
|
|
|
|
|
assert_eq!(result.projection_owner, SearchProjectionOwner::RustKernel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ai_protocol_contract_exposes_structured_write_result() {
|
|
|
|
|
let result = AiStructuredWriteResult {
|
|
|
|
|
ok: true,
|
|
|
|
|
write_kind: AiStructuredWriteKind::SummaryNode,
|
|
|
|
|
command_name: "tree.node.create".into(),
|
|
|
|
|
target_id: Some("summary_1".into()),
|
|
|
|
|
revision: Some(2),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(result.write_kind, AiStructuredWriteKind::SummaryNode);
|
|
|
|
|
assert_eq!(result.command_name, "tree.node.create");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn mindmap_projection_and_command_contract_cover_kernel_truth() {
|
|
|
|
|
let projection = MindmapProjection {
|
|
|
|
|
schema: "mnote.mindmap_projection.v1".into(),
|
|
|
|
|
map_id: "mind_1".into(),
|
|
|
|
|
root_node: "root".into(),
|
|
|
|
|
nodes: vec![],
|
|
|
|
|
edges: vec![],
|
|
|
|
|
layout_hints: serde_json::json!({"layout": "right"}),
|
|
|
|
|
revision: 1,
|
|
|
|
|
owner: MindmapProjectionOwner::RustKernel,
|
|
|
|
|
};
|
|
|
|
|
let command = MindmapCommand::RenameNode {
|
|
|
|
|
map_id: "mind_1".into(),
|
|
|
|
|
node_id: "root".into(),
|
|
|
|
|
title: "新标题".into(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(projection.owner, MindmapProjectionOwner::RustKernel);
|
|
|
|
|
assert!(matches!(command, MindmapCommand::RenameNode { .. }));
|
|
|
|
|
}
|
2026-05-11 12:27:40 +08:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn mindmap_kernel_and_adapter_projection_serialize_as_camel_case() {
|
|
|
|
|
let kernel_projection = MindmapKernelProjection {
|
|
|
|
|
schema: "mnote.mindmap.kernel_projection.v1".into(),
|
|
|
|
|
document_id: "doc_1".into(),
|
|
|
|
|
mindmap_id: "mind_1".into(),
|
|
|
|
|
root_node_id: "root".into(),
|
|
|
|
|
revision: 7,
|
|
|
|
|
nodes: vec![
|
|
|
|
|
MindmapKernelNode {
|
|
|
|
|
node_id: "root".into(),
|
|
|
|
|
parent_id: None,
|
|
|
|
|
text: "KMIND".into(),
|
|
|
|
|
order: 0,
|
|
|
|
|
collapsed: false,
|
|
|
|
|
style: Default::default(),
|
|
|
|
|
refs: Default::default(),
|
|
|
|
|
},
|
|
|
|
|
MindmapKernelNode {
|
|
|
|
|
node_id: "child_1".into(),
|
|
|
|
|
parent_id: Some("root".into()),
|
|
|
|
|
text: "二级节点".into(),
|
|
|
|
|
order: 1,
|
|
|
|
|
collapsed: false,
|
|
|
|
|
style: Default::default(),
|
|
|
|
|
refs: Default::default(),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
edges: vec![MindmapKernelEdge {
|
|
|
|
|
edge_id: "root->child_1".into(),
|
|
|
|
|
from_node_id: "root".into(),
|
|
|
|
|
to_node_id: "child_1".into(),
|
|
|
|
|
edge_kind: "tree".into(),
|
|
|
|
|
}],
|
|
|
|
|
summaries: vec![MindmapSummary {
|
|
|
|
|
summary_id: "summary_1".into(),
|
|
|
|
|
parent_node_id: "child_1".into(),
|
|
|
|
|
node_ids: vec!["child_1".into()],
|
|
|
|
|
text: "概要".into(),
|
|
|
|
|
style: Default::default(),
|
|
|
|
|
}],
|
|
|
|
|
associative_lines: vec![MindmapAssociativeLine {
|
|
|
|
|
line_id: "line_1".into(),
|
|
|
|
|
from_node_id: "root".into(),
|
|
|
|
|
to_node_id: "child_1".into(),
|
|
|
|
|
text: Some("关联".into()),
|
|
|
|
|
style: Default::default(),
|
|
|
|
|
}],
|
|
|
|
|
layout: serde_json::json!({ "type": "logicalStructure" }),
|
|
|
|
|
theme: serde_json::json!({ "template": "classic" }),
|
|
|
|
|
view: serde_json::json!({ "zoom": 1.0 }),
|
|
|
|
|
capabilities: MindmapKernelCapabilities {
|
|
|
|
|
can_edit: true,
|
|
|
|
|
can_edit_text: true,
|
|
|
|
|
can_insert_child: true,
|
|
|
|
|
can_insert_sibling_after: true,
|
|
|
|
|
can_delete_node: true,
|
|
|
|
|
can_move_node: true,
|
|
|
|
|
can_patch_view: true,
|
|
|
|
|
can_set_layout: true,
|
|
|
|
|
can_set_theme: true,
|
|
|
|
|
},
|
|
|
|
|
source: MindmapProjectionSource::Kernel,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let adapter_projection = MindmapAdapterProjection {
|
|
|
|
|
schema: "mnote.mindmap.simple_mind_map_scene.v1".into(),
|
|
|
|
|
runtime: "simple-mind-map".into(),
|
|
|
|
|
root: serde_json::json!({
|
|
|
|
|
"data": { "uid": "root", "text": "KMIND" },
|
|
|
|
|
"children": []
|
|
|
|
|
}),
|
|
|
|
|
layout: serde_json::json!("logicalStructure"),
|
|
|
|
|
theme: serde_json::json!("classic"),
|
|
|
|
|
theme_config: serde_json::json!({}),
|
|
|
|
|
view: serde_json::json!({ "scale": 1.0 }),
|
|
|
|
|
config: serde_json::json!({}),
|
|
|
|
|
compat_payload: serde_json::json!({}),
|
|
|
|
|
kernel_revision: 7,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let kernel_value =
|
|
|
|
|
serde_json::to_value(kernel_projection).expect("kernel projection should serialize");
|
|
|
|
|
let adapter_value =
|
|
|
|
|
serde_json::to_value(adapter_projection).expect("adapter projection should serialize");
|
|
|
|
|
|
|
|
|
|
assert_eq!(kernel_value["documentId"], serde_json::json!("doc_1"));
|
|
|
|
|
assert_eq!(kernel_value["mindmapId"], serde_json::json!("mind_1"));
|
|
|
|
|
assert_eq!(kernel_value["rootNodeId"], serde_json::json!("root"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
kernel_value["nodes"][1]["parentId"],
|
|
|
|
|
serde_json::json!("root")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
kernel_value["associativeLines"][0]["fromNodeId"],
|
|
|
|
|
serde_json::json!("root")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
kernel_value["capabilities"]["canEditText"],
|
|
|
|
|
serde_json::json!(true)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(kernel_value["source"], serde_json::json!("kernel"));
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
adapter_value["runtime"],
|
|
|
|
|
serde_json::json!("simple-mind-map")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(adapter_value["themeConfig"], serde_json::json!({}));
|
|
|
|
|
assert_eq!(adapter_value["compatPayload"], serde_json::json!({}));
|
|
|
|
|
assert_eq!(adapter_value["kernelRevision"], serde_json::json!(7));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn mindmap_kernel_command_contract_covers_minimum_write_surface() {
|
|
|
|
|
let commands = vec![
|
|
|
|
|
MindmapKernelCommand::UpdateText {
|
|
|
|
|
mindmap_id: "mind_1".into(),
|
|
|
|
|
node_id: "root".into(),
|
|
|
|
|
text: "新标题".into(),
|
|
|
|
|
},
|
|
|
|
|
MindmapKernelCommand::InsertChild {
|
|
|
|
|
mindmap_id: "mind_1".into(),
|
|
|
|
|
parent_node_id: "root".into(),
|
|
|
|
|
node: MindmapNodeInput {
|
|
|
|
|
uid: Some("child_1".into()),
|
|
|
|
|
text: "子节点".into(),
|
|
|
|
|
hyperlink: None,
|
|
|
|
|
note: None,
|
|
|
|
|
refs: None,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
MindmapKernelCommand::InsertSiblingAfter {
|
|
|
|
|
mindmap_id: "mind_1".into(),
|
|
|
|
|
target_node_id: "child_1".into(),
|
|
|
|
|
node: MindmapNodeInput {
|
|
|
|
|
uid: Some("child_2".into()),
|
|
|
|
|
text: "同级节点".into(),
|
|
|
|
|
hyperlink: None,
|
|
|
|
|
note: None,
|
|
|
|
|
refs: None,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
MindmapKernelCommand::DeleteNode {
|
|
|
|
|
mindmap_id: "mind_1".into(),
|
|
|
|
|
node_id: "child_2".into(),
|
|
|
|
|
},
|
|
|
|
|
MindmapKernelCommand::MoveNode {
|
|
|
|
|
mindmap_id: "mind_1".into(),
|
|
|
|
|
node_id: "child_1".into(),
|
|
|
|
|
new_parent_node_id: "root".into(),
|
|
|
|
|
order: Some(1),
|
|
|
|
|
},
|
|
|
|
|
MindmapKernelCommand::PatchView {
|
|
|
|
|
mindmap_id: "mind_1".into(),
|
|
|
|
|
patch: serde_json::json!({ "scale": 1.2 }),
|
|
|
|
|
},
|
|
|
|
|
MindmapKernelCommand::SetLayout {
|
|
|
|
|
mindmap_id: "mind_1".into(),
|
|
|
|
|
layout: serde_json::json!({ "type": "mindMap" }),
|
|
|
|
|
},
|
|
|
|
|
MindmapKernelCommand::SetTheme {
|
|
|
|
|
mindmap_id: "mind_1".into(),
|
|
|
|
|
theme: serde_json::json!({ "template": "classic" }),
|
|
|
|
|
theme_config: serde_json::json!({ "lineColor": "#f59e0b" }),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let encoded = serde_json::to_value(&commands).expect("commands should serialize");
|
|
|
|
|
|
|
|
|
|
assert_eq!(encoded[0]["type"], serde_json::json!("updateText"));
|
|
|
|
|
assert_eq!(encoded[1]["type"], serde_json::json!("insertChild"));
|
|
|
|
|
assert_eq!(encoded[2]["type"], serde_json::json!("insertSiblingAfter"));
|
|
|
|
|
assert_eq!(encoded[3]["type"], serde_json::json!("deleteNode"));
|
|
|
|
|
assert_eq!(encoded[4]["type"], serde_json::json!("moveNode"));
|
|
|
|
|
assert_eq!(encoded[5]["type"], serde_json::json!("patchView"));
|
|
|
|
|
assert_eq!(encoded[6]["type"], serde_json::json!("setLayout"));
|
|
|
|
|
assert_eq!(encoded[7]["type"], serde_json::json!("setTheme"));
|
|
|
|
|
assert_eq!(encoded[0]["mindmapId"], serde_json::json!("mind_1"));
|
|
|
|
|
assert_eq!(encoded[1]["parentNodeId"], serde_json::json!("root"));
|
|
|
|
|
assert_eq!(encoded[4]["newParentNodeId"], serde_json::json!("root"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
encoded[7]["themeConfig"]["lineColor"],
|
|
|
|
|
serde_json::json!("#f59e0b")
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-14 13:22:29 +08:00
|
|
|
}
|