Implement Rust web sidebar title and tree interactions
This commit is contained in:
@@ -21,8 +21,11 @@ use core_protocol::{
|
||||
KernelProjectionRequest, KernelProjectionResourceKind, KernelProjectionResourceMeta,
|
||||
KernelProjectionResult, KernelProjectionRowKind, KernelRefsPayload, KernelSubtreeRef,
|
||||
KernelSubtreeResult, KernelTraverseGraph, KernelUpdateNode, ListBridgeWorkspaceOverview,
|
||||
MindmapNodeData, MindmapNodeInput, MindmapNodeRef, MindmapOp, MindmapTreeNode, MoveBlock,
|
||||
PatchBlock, PutMindmap, QueryEnvelope, SearchDocuments, SearchRecent, SourcePayload, TargetRef,
|
||||
MindmapCommand, MindmapNodeData, MindmapNodeInput, MindmapNodeRef, MindmapOp,
|
||||
MindmapProjection, MindmapProjectionEdge, MindmapProjectionNode, MindmapProjectionOwner,
|
||||
MindmapTreeNode, MoveBlock, PageAggregateProjection, PageAggregateSource, PageBody, PageHead,
|
||||
PageIdentity, PageLayout, PageOptions, PagePermissions, PageStats, PageTree, PatchBlock,
|
||||
PutMindmap, QueryEnvelope, SearchDocuments, SearchRecent, SourcePayload, TargetRef,
|
||||
ToolExecutionMode, ToolInvocation, UpdatePageOptions, UpdatePageStats,
|
||||
};
|
||||
use event_log::DomainEventRecord;
|
||||
@@ -412,6 +415,13 @@ struct DocumentContentQueryPayload {
|
||||
workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PageAggregateQueryPayload {
|
||||
document_id: String,
|
||||
workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MindmapGetQueryPayload {
|
||||
@@ -686,6 +696,19 @@ struct MindmapPutCommandPayload {
|
||||
create_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MindmapCommandApplyPayload {
|
||||
document_id: String,
|
||||
mindmap_id: String,
|
||||
#[serde(default)]
|
||||
workspace_id: Option<String>,
|
||||
#[serde(default)]
|
||||
commands: Vec<MindmapCommand>,
|
||||
#[serde(default)]
|
||||
projection_revision: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MindmapDeleteCommandPayload {
|
||||
@@ -2927,10 +2950,39 @@ fn execute_query(
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"mindmaps.get" => {
|
||||
let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?;
|
||||
"page.aggregate.get" => {
|
||||
let payload: PageAggregateQueryPayload = parse_payload(query_wire.payload)?;
|
||||
let query = QueryEnvelope {
|
||||
name: "mindmaps.get".into(),
|
||||
name: "page.aggregate.get".into(),
|
||||
payload: core_protocol::GetPageContent {
|
||||
page_id: payload.document_id.clone(),
|
||||
workspace_id: payload.workspace_id.clone(),
|
||||
},
|
||||
};
|
||||
let request = build_query_request(&context, &query)?;
|
||||
Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan {
|
||||
query_name: query.name,
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
actor_id: request.actor_id,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"documentId": payload.document_id,
|
||||
"workspaceId": payload.workspace_id,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"mindmaps.get" | "mindmap.projection.get" => {
|
||||
let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?;
|
||||
let query_name = if query_wire.name == "mindmap.projection.get" {
|
||||
"mindmap.projection.get"
|
||||
} else {
|
||||
"mindmaps.get"
|
||||
};
|
||||
let query = QueryEnvelope {
|
||||
name: query_name.into(),
|
||||
payload: GetMindmap {
|
||||
document_id: payload.document_id.clone(),
|
||||
mindmap_id: payload.mindmap_id.clone(),
|
||||
@@ -2997,15 +3049,20 @@ fn execute_query(
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"search.documents" => {
|
||||
"search.documents" | "search.documents.query" => {
|
||||
let payload: SearchDocumentsQueryPayload = parse_payload(query_wire.payload)?;
|
||||
let query_name = if query_wire.name == "search.documents.query" {
|
||||
"search.documents.query"
|
||||
} else {
|
||||
"search.documents"
|
||||
};
|
||||
let time_range = payload.time_range.clone().unwrap_or_else(|| "any".into());
|
||||
let time_field = payload
|
||||
.time_field
|
||||
.clone()
|
||||
.unwrap_or_else(|| "updated".into());
|
||||
let query = QueryEnvelope {
|
||||
name: "search.documents".into(),
|
||||
name: query_name.into(),
|
||||
payload: SearchDocuments {
|
||||
query: payload.query.clone(),
|
||||
workspace_id: payload.workspace_id.clone(),
|
||||
@@ -5104,6 +5161,157 @@ fn build_document_content_result(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_page_aggregate_projection_result(
|
||||
data: &Value,
|
||||
document_id: &str,
|
||||
workspace_id: Option<&str>,
|
||||
source: PageAggregateSource,
|
||||
) -> Result<PageAggregateProjection, BridgeError> {
|
||||
if data.is_null() {
|
||||
return Err(BridgeError::not_found("page.aggregate.get 未返回页面聚合数据"));
|
||||
}
|
||||
|
||||
let meta = data.get("meta").unwrap_or(data);
|
||||
let content_result = data
|
||||
.get("content")
|
||||
.filter(|value| {
|
||||
value.get("content").is_some()
|
||||
|| value.get("revision").is_some()
|
||||
|| value.get("pageSubtree").is_some()
|
||||
|| value.get("page_subtree").is_some()
|
||||
})
|
||||
.unwrap_or(data);
|
||||
let resolved_document_id = read_trimmed_string_field(meta, &["id", "documentId"])
|
||||
.unwrap_or_else(|| document_id.to_string());
|
||||
let resolved_workspace_id = read_trimmed_string_field(meta, &["workspace_id", "workspaceId"])
|
||||
.or_else(|| workspace_id.map(ToOwned::to_owned))
|
||||
.unwrap_or_else(|| "default".into());
|
||||
let title = read_trimmed_string_field(meta, &["title"]).unwrap_or_else(|| "无标题".into());
|
||||
let updated_at = read_trimmed_string_field(meta, &["updated_at", "updatedAt"]);
|
||||
let parent_id = read_trimmed_string_field(meta, &["parent_id", "parentId"]);
|
||||
let content = content_result
|
||||
.get("content")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Array(vec![]));
|
||||
let revision = content_result
|
||||
.get("revision")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null);
|
||||
let conflict_detection_key = content_result
|
||||
.get("conflictDetectionKey")
|
||||
.or_else(|| content_result.get("conflict_detection_key"))
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null);
|
||||
let page_subtree = content_result
|
||||
.get("pageSubtree")
|
||||
.or_else(|| content_result.get("page_subtree"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({ "rootNodeId": resolved_document_id }));
|
||||
let page_options = PageOptions {
|
||||
wide_layout: bool_field(meta, "wide_layout")
|
||||
.or_else(|| bool_field(meta, "wideLayout"))
|
||||
.unwrap_or(false),
|
||||
small_text: bool_field(meta, "use_small_text")
|
||||
.or_else(|| bool_field(meta, "smallText"))
|
||||
.unwrap_or(false),
|
||||
layout_density: read_trimmed_string_field(meta, &["layout_density", "layoutDensity"])
|
||||
.unwrap_or_else(|| "normal".into()),
|
||||
show_heading_numbers: bool_field(meta, "show_heading_numbers")
|
||||
.or_else(|| bool_field(meta, "showHeadingNumbers"))
|
||||
.unwrap_or(true),
|
||||
show_toc: bool_field(meta, "show_toc")
|
||||
.or_else(|| bool_field(meta, "showToc"))
|
||||
.unwrap_or(false),
|
||||
show_structure: bool_field(meta, "show_structure")
|
||||
.or_else(|| bool_field(meta, "showStructure"))
|
||||
.unwrap_or(false),
|
||||
protect_editing: bool_field(meta, "protect_editing")
|
||||
.or_else(|| bool_field(meta, "protectEditing"))
|
||||
.unwrap_or(false),
|
||||
show_word_count: bool_field(meta, "show_word_count")
|
||||
.or_else(|| bool_field(meta, "showWordCount"))
|
||||
.unwrap_or(true),
|
||||
collapse_backlinks: bool_field(meta, "collapse_backlinks")
|
||||
.or_else(|| bool_field(meta, "collapseBacklinks"))
|
||||
.unwrap_or(false),
|
||||
page_font: read_trimmed_string_field(meta, &["page_font", "pageFont"])
|
||||
.unwrap_or_else(|| "default".into()),
|
||||
hide_child_pages: bool_field(meta, "hide_child_pages")
|
||||
.or_else(|| bool_field(meta, "hideChildPages"))
|
||||
.unwrap_or(false),
|
||||
show_block_ref_count: bool_field(meta, "show_block_ref_count")
|
||||
.or_else(|| bool_field(meta, "showBlockRefCount"))
|
||||
.unwrap_or(false),
|
||||
embed_default_block_id: meta
|
||||
.get("embed_default_block_id")
|
||||
.or_else(|| meta.get("embedDefaultBlockId"))
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
};
|
||||
let layout_options = serde_json::to_value(&page_options).map_err(|error| {
|
||||
BridgeError::transport(format!("PageOptions 序列化失败: {error}"))
|
||||
})?;
|
||||
let revision_ref = revision
|
||||
.as_u64()
|
||||
.map(|value| format!("{resolved_document_id}:{value}"));
|
||||
|
||||
Ok(PageAggregateProjection {
|
||||
schema: PageAggregateProjection::SCHEMA.into(),
|
||||
projection_version: PageAggregateProjection::VERSION,
|
||||
source,
|
||||
page_id: resolved_document_id.clone(),
|
||||
parent_id,
|
||||
title: title.clone(),
|
||||
path: vec![resolved_document_id.clone()],
|
||||
sidebar_tree_membership: vec!["page-tree".into(), "file-tree".into()],
|
||||
body_ref: revision_ref,
|
||||
layout_options,
|
||||
updated_at: updated_at.clone(),
|
||||
identity: PageIdentity {
|
||||
document_id: resolved_document_id,
|
||||
workspace_id: resolved_workspace_id,
|
||||
},
|
||||
head: PageHead {
|
||||
title,
|
||||
updated_at: updated_at.map(Value::String).unwrap_or(Value::Null),
|
||||
permissions: PagePermissions {
|
||||
read_only: bool_field(meta, "can_edit").map(|can_edit| !can_edit).unwrap_or(false),
|
||||
disable_download: bool_field(meta, "disable_download")
|
||||
.or_else(|| bool_field(meta, "disableDownload"))
|
||||
.unwrap_or(false),
|
||||
disable_copy: bool_field(meta, "disable_copy")
|
||||
.or_else(|| bool_field(meta, "disableCopy"))
|
||||
.unwrap_or(false),
|
||||
},
|
||||
},
|
||||
layout: PageLayout { page_options },
|
||||
body: PageBody {
|
||||
content,
|
||||
revision,
|
||||
conflict_detection_key,
|
||||
},
|
||||
tree: PageTree { page_subtree },
|
||||
stats: PageStats {
|
||||
word_count: meta.get("word_count").and_then(Value::as_u64).unwrap_or(0),
|
||||
character_count: meta
|
||||
.get("character_count")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0),
|
||||
block_count: meta.get("block_count").and_then(Value::as_u64).unwrap_or(0),
|
||||
todo_total: meta
|
||||
.get("todo_total")
|
||||
.or_else(|| meta.get("todo_total_count"))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0),
|
||||
todo_done: meta
|
||||
.get("todo_done")
|
||||
.or_else(|| meta.get("todo_done_count"))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_mindmap_from_value(data: &Value) -> Result<MindmapTreeNode, BridgeError> {
|
||||
if data.is_null() {
|
||||
return Ok(default_mindmap_tree());
|
||||
@@ -5131,6 +5339,98 @@ fn normalize_mindmap_from_value(data: &Value) -> Result<MindmapTreeNode, BridgeE
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
fn build_mindmap_projection_result(
|
||||
data: &Value,
|
||||
mindmap_id: &str,
|
||||
) -> Result<MindmapProjection, BridgeError> {
|
||||
let revision = data
|
||||
.get("revision")
|
||||
.or_else(|| data.get("meta").and_then(|meta| meta.get("revision")))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(1);
|
||||
let tree_input = data
|
||||
.get("data")
|
||||
.filter(|value| value.get("data").is_some() || value.get("children").is_some())
|
||||
.unwrap_or(data);
|
||||
let tree = normalize_mindmap_from_value(tree_input)?;
|
||||
let root_node = tree
|
||||
.data
|
||||
.uid
|
||||
.clone()
|
||||
.unwrap_or_else(|| "root".into());
|
||||
let mut nodes = Vec::new();
|
||||
let mut edges = Vec::new();
|
||||
collect_mindmap_projection_rows(&tree, None, &mut nodes, &mut edges);
|
||||
|
||||
Ok(MindmapProjection {
|
||||
schema: "mnote.mindmap_projection.v1".into(),
|
||||
map_id: mindmap_id.into(),
|
||||
root_node,
|
||||
nodes,
|
||||
edges,
|
||||
layout_hints: data
|
||||
.get("layoutHints")
|
||||
.or_else(|| data.get("layout_hints"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({ "layout": "right" })),
|
||||
revision,
|
||||
owner: MindmapProjectionOwner::RustKernel,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_mindmap_projection_rows(
|
||||
node: &MindmapTreeNode,
|
||||
parent_id: Option<&str>,
|
||||
nodes: &mut Vec<MindmapProjectionNode>,
|
||||
edges: &mut Vec<MindmapProjectionEdge>,
|
||||
) {
|
||||
let node_id = node
|
||||
.data
|
||||
.uid
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("node_{}", nodes.len() + 1));
|
||||
let title = node
|
||||
.data
|
||||
.text
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "未命名节点".into());
|
||||
if let Some(parent_id) = parent_id {
|
||||
edges.push(MindmapProjectionEdge {
|
||||
edge_id: format!("{parent_id}->{node_id}"),
|
||||
from_node_id: parent_id.into(),
|
||||
to_node_id: node_id.clone(),
|
||||
edge_kind: "mindmap_child".into(),
|
||||
});
|
||||
}
|
||||
nodes.push(MindmapProjectionNode {
|
||||
node_id: node_id.clone(),
|
||||
title,
|
||||
parent_id: parent_id.map(ToOwned::to_owned),
|
||||
});
|
||||
for child in &node.children {
|
||||
collect_mindmap_projection_rows(child, Some(&node_id), nodes, edges);
|
||||
}
|
||||
}
|
||||
|
||||
fn search_documents_canonical_projection(evaluation: SearchDocumentsEvaluation) -> Value {
|
||||
let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| json!({
|
||||
"enqueueAssetIds": [],
|
||||
"results": []
|
||||
}));
|
||||
if let Some(map) = payload.as_object_mut() {
|
||||
map.insert("projectionOwner".into(), json!("rust-kernel"));
|
||||
if let Some(results) = map.get_mut("results").and_then(Value::as_array_mut) {
|
||||
for result in results {
|
||||
if let Some(result_map) = result.as_object_mut() {
|
||||
result_map.insert("projectionOwner".into(), json!("rust-kernel"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
fn walk_block_summaries(root_blocks: &[Value], max_nodes: usize) -> Vec<RuntimeBlockSummary> {
|
||||
let mut queue = VecDeque::new();
|
||||
for block in root_blocks {
|
||||
@@ -7082,6 +7382,18 @@ fn execute_query_result(
|
||||
BridgeError::transport(format!("documents.content.get result 序列化失败: {error}"))
|
||||
})
|
||||
}
|
||||
"page.aggregate.get" => {
|
||||
let payload: PageAggregateQueryPayload = parse_payload(query_wire.payload)?;
|
||||
let result = build_page_aggregate_projection_result(
|
||||
&data,
|
||||
&payload.document_id,
|
||||
payload.workspace_id.as_deref(),
|
||||
PageAggregateSource::KernelProjection,
|
||||
)?;
|
||||
serde_json::to_value(result).map_err(|error| {
|
||||
BridgeError::transport(format!("page.aggregate.get result 序列化失败: {error}"))
|
||||
})
|
||||
}
|
||||
"kernel.node.get" => {
|
||||
let payload: KernelGetNodeQueryPayload = parse_payload(query_wire.payload)?;
|
||||
let node = normalize_kernel_node_from_record(&data, payload.workspace_id.as_deref())?;
|
||||
@@ -7167,7 +7479,16 @@ fn execute_query_result(
|
||||
BridgeError::transport(format!("mindmaps.get result 序列化失败: {error}"))
|
||||
})
|
||||
}
|
||||
"search.documents" => {
|
||||
"mindmap.projection.get" => {
|
||||
let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?;
|
||||
let result = build_mindmap_projection_result(&data, &payload.mindmap_id)?;
|
||||
serde_json::to_value(result).map_err(|error| {
|
||||
BridgeError::transport(format!(
|
||||
"mindmap.projection.get result 序列化失败: {error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
"search.documents" | "search.documents.query" => {
|
||||
let payload: SearchDocumentsQueryPayload = parse_payload(query_wire.payload)?;
|
||||
let dataset: SearchDocumentsDataset =
|
||||
serde_json::from_value(data).map_err(|error| {
|
||||
@@ -7191,10 +7512,15 @@ fn execute_query_result(
|
||||
},
|
||||
&dataset,
|
||||
);
|
||||
serde_json::to_value(SearchDocumentsEvaluation {
|
||||
let legacy_result = SearchDocumentsEvaluation {
|
||||
enqueue_asset_ids: result.enqueue_asset_ids,
|
||||
results: result.results,
|
||||
})
|
||||
};
|
||||
if query_wire.name == "search.documents.query" {
|
||||
serde_json::to_value(search_documents_canonical_projection(legacy_result))
|
||||
} else {
|
||||
serde_json::to_value(legacy_result)
|
||||
}
|
||||
.map_err(|error| {
|
||||
BridgeError::transport(format!("search.documents result 序列化失败: {error}"))
|
||||
})
|
||||
@@ -7985,6 +8311,45 @@ fn execute_command(
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"mindmap.command.apply" => {
|
||||
let payload: MindmapCommandApplyPayload = parse_payload(command_wire.payload.clone())?;
|
||||
let command = CommandEnvelope {
|
||||
name: "mindmap.command.apply".into(),
|
||||
command_id: command_wire.command_id.clone(),
|
||||
idempotency_key: command_wire.idempotency_key.clone(),
|
||||
actor: to_actor_payload(&command_wire.actor),
|
||||
source: to_source_payload(&command_wire.source),
|
||||
target: to_target_ref(command_wire.target.as_ref()).or(Some(TargetRef {
|
||||
workspace_id: payload.workspace_id.clone(),
|
||||
page_id: Some(payload.document_id.clone()),
|
||||
block_id: None,
|
||||
})),
|
||||
payload: payload.commands.clone(),
|
||||
reason: command_wire.reason,
|
||||
refs: command_wire.refs,
|
||||
dry_run: command_wire.dry_run,
|
||||
validate_only: command_wire.validate_only,
|
||||
};
|
||||
let request = build_write_request(&context, &command)?;
|
||||
Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan {
|
||||
command_name: command.name,
|
||||
command_id: command.command_id,
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
actor_id: request.actor_id,
|
||||
idempotency_key: request.idempotency_key,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"documentId": payload.document_id,
|
||||
"mindmapId": payload.mindmap_id,
|
||||
"commands": payload.commands,
|
||||
"projectionRevision": payload.projection_revision,
|
||||
"canonicalCommand": "mindmap.command.apply",
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"mindmaps.delete" => {
|
||||
let payload: MindmapDeleteCommandPayload = parse_payload(command_wire.payload.clone())?;
|
||||
let command = CommandEnvelope {
|
||||
@@ -9333,6 +9698,164 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_aggregate_get_query_executes_into_core_projection() {
|
||||
let result = execute_runtime_query(RuntimeInput::Query {
|
||||
context: demo_context(),
|
||||
query: RuntimeQueryEnvelopeWire {
|
||||
name: "page.aggregate.get".into(),
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
}),
|
||||
},
|
||||
data: Some(json!({
|
||||
"meta": {
|
||||
"id": "doc_1",
|
||||
"workspace_id": "ws_1",
|
||||
"title": "聚合页面",
|
||||
"parent_id": "root",
|
||||
"updated_at": "2026-04-29T00:00:00Z"
|
||||
},
|
||||
"content": {
|
||||
"content": [],
|
||||
"revision": 7,
|
||||
"conflict_detection_key": "doc_1:7",
|
||||
"pageSubtree": {"rootNodeId": "doc_1"}
|
||||
}
|
||||
})),
|
||||
})
|
||||
.expect("page aggregate query should build");
|
||||
|
||||
assert_eq!(result["schema"], json!("mnote.page_aggregate.v1"));
|
||||
assert_eq!(result["projectionVersion"], json!(1));
|
||||
assert_eq!(result["source"], json!("KernelProjection"));
|
||||
assert_eq!(result["pageId"], json!("doc_1"));
|
||||
assert_eq!(result["identity"]["documentId"], json!("doc_1"));
|
||||
assert_eq!(result["body"]["revision"], json!(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_documents_query_canonical_facade_keeps_projection_owner() {
|
||||
let result = execute_runtime_query(RuntimeInput::Query {
|
||||
context: demo_context(),
|
||||
query: RuntimeQueryEnvelopeWire {
|
||||
name: "search.documents.query".into(),
|
||||
payload: json!({
|
||||
"query": "rust",
|
||||
"workspaceId": "ws_1",
|
||||
"pageId": null,
|
||||
"limit": 10,
|
||||
"titleOnly": false,
|
||||
"exact": false,
|
||||
"includeOcr": true,
|
||||
"timeRange": "any",
|
||||
"timeField": "updated",
|
||||
"customRangeFrom": null,
|
||||
"customRangeTo": null,
|
||||
}),
|
||||
},
|
||||
data: Some(json!({
|
||||
"documents": [
|
||||
{
|
||||
"id": "page_1",
|
||||
"workspaceId": "ws_1",
|
||||
"title": "Rust Notes",
|
||||
"rawText": "这里有 rust 搜索内容",
|
||||
"createdAt": "2026-04-15T00:00:00Z",
|
||||
"updatedAt": "2026-04-15T01:00:00Z"
|
||||
}
|
||||
],
|
||||
"mindmaps": [],
|
||||
"tables": [],
|
||||
"tableRows": [],
|
||||
"assets": []
|
||||
})),
|
||||
})
|
||||
.expect("canonical search query result should build");
|
||||
|
||||
assert_eq!(result["projectionOwner"], json!("rust-kernel"));
|
||||
assert_eq!(result["results"][0]["projectionOwner"], json!("rust-kernel"));
|
||||
assert_eq!(result["results"][0]["id"], json!("page_1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mindmap_projection_get_query_returns_kernel_projection() {
|
||||
let result = execute_runtime_query(RuntimeInput::Query {
|
||||
context: demo_context(),
|
||||
query: RuntimeQueryEnvelopeWire {
|
||||
name: "mindmap.projection.get".into(),
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"mindmapId": "mind_1",
|
||||
"workspaceId": "ws_1",
|
||||
}),
|
||||
},
|
||||
data: Some(json!({
|
||||
"data": {
|
||||
"data": {"uid": "root", "text": "中心主题"},
|
||||
"children": [
|
||||
{"data": {"uid": "child_1", "text": "子节点"}, "children": []}
|
||||
]
|
||||
},
|
||||
"revision": 3
|
||||
})),
|
||||
})
|
||||
.expect("mindmap projection should build");
|
||||
|
||||
assert_eq!(result["schema"], json!("mnote.mindmap_projection.v1"));
|
||||
assert_eq!(result["owner"], json!("rust-kernel"));
|
||||
assert_eq!(result["mapId"], json!("mind_1"));
|
||||
assert_eq!(result["revision"], json!(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mindmap_command_apply_plan_uses_kernel_command_facade() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
context: demo_context(),
|
||||
command: RuntimeCommandEnvelopeWire {
|
||||
name: "mindmap.command.apply".into(),
|
||||
command_id: "cmd_mindmap_apply".into(),
|
||||
idempotency_key: None,
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: "user".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: None,
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: "rust-web".into(),
|
||||
client: "mnote-web".into(),
|
||||
},
|
||||
target: None,
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"mindmapId": "mind_1",
|
||||
"commands": [
|
||||
{"type": "renameNode", "mapId": "mind_1", "nodeId": "root", "title": "新标题"}
|
||||
],
|
||||
"projectionRevision": 3
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("mindmap command facade".into()),
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
.expect("mindmap command facade should plan");
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.command_name, "mindmap.command.apply");
|
||||
assert_eq!(plan.function_name, "mindmaps:applyCommand");
|
||||
assert_eq!(plan.args_json["canonicalCommand"], json!("mindmap.command.apply"));
|
||||
}
|
||||
RuntimeExecutionPlan::Query(_) | RuntimeExecutionPlan::Tool(_) => {
|
||||
panic!("expected command plan")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_documents_query_executes_in_rust_runtime() {
|
||||
let result = execute_runtime_query(RuntimeInput::Query {
|
||||
@@ -10064,6 +10587,128 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_body_save_command_plan_uses_canonical_page_command() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
context: demo_context(),
|
||||
command: RuntimeCommandEnvelopeWire {
|
||||
name: "page.body.save".into(),
|
||||
command_id: "cmd_page_body_save".into(),
|
||||
idempotency_key: None,
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: "user".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: None,
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: "rust-web".into(),
|
||||
client: "mnote-web".into(),
|
||||
},
|
||||
target: Some(RuntimeTargetWire {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: Some("doc_1".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
"revision": 1,
|
||||
"content": [],
|
||||
"conflictDetectionKey": "doc_1:1"
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("canonical body save".into()),
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
.expect("page.body.save plan should build");
|
||||
|
||||
let RuntimeExecutionPlan::Command(plan) = plan else {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
assert_eq!(plan.command_name, "page.body.save");
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_head_update_title_command_plan_uses_canonical_page_command() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
context: demo_context(),
|
||||
command: RuntimeCommandEnvelopeWire {
|
||||
name: "page.head.updateTitle".into(),
|
||||
command_id: "cmd_page_head_title".into(),
|
||||
idempotency_key: None,
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: "user".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: None,
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: "rust-web".into(),
|
||||
client: "mnote-web".into(),
|
||||
},
|
||||
target: None,
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"title": "新标题"
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("canonical title update".into()),
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
.expect("page.head.updateTitle plan should build");
|
||||
|
||||
let RuntimeExecutionPlan::Command(plan) = plan else {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
assert_eq!(plan.command_name, "page.head.updateTitle");
|
||||
assert_eq!(plan.function_name, "documents:updateTitle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_layout_update_options_command_plan_uses_canonical_page_command() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
context: demo_context(),
|
||||
command: RuntimeCommandEnvelopeWire {
|
||||
name: "page.layout.updateOptions".into(),
|
||||
command_id: "cmd_page_layout_options".into(),
|
||||
idempotency_key: None,
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: "user".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: None,
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: "rust-web".into(),
|
||||
client: "mnote-web".into(),
|
||||
},
|
||||
target: None,
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
"options": {"showToc": true}
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("canonical layout update".into()),
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
.expect("page.layout.updateOptions plan should build");
|
||||
|
||||
let RuntimeExecutionPlan::Command(plan) = plan else {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
assert_eq!(plan.command_name, "page.layout.updateOptions");
|
||||
assert_eq!(plan.function_name, "documents:updateOptions");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documents_save_command_plan_falls_back_from_invalid_editor_document_to_tiptap() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
|
||||
Reference in New Issue
Block a user