feat: consolidate local-first mnote web runtime
This commit is contained in:
@@ -3,45 +3,45 @@ use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::command_support::{
|
||||
build_runtime_command_plan, build_tree_target, ensure_non_empty, ensure_sort_order,
|
||||
execute_runtime_command_via_convex_with_artifacts, read_optional_non_empty,
|
||||
execute_runtime_command_via_legacy_cloud_with_artifacts, read_optional_non_empty,
|
||||
};
|
||||
use crate::routes::local_folder_source::{
|
||||
ensure_local_workspace_access_with_state, ensure_local_workspace_read_access_with_state,
|
||||
execute_local_tree_command_with_sort, load_local_folder_file_tree_snapshot,
|
||||
load_local_folder_page_tree_snapshot, local_folder_watch_revision,
|
||||
local_workspace_id_from_root_uri, LocalAccessMode,
|
||||
LocalAccessMode, ensure_local_workspace_access_with_state,
|
||||
ensure_local_workspace_read_access_with_state, execute_local_tree_command_with_sort,
|
||||
load_local_folder_file_tree_snapshot, load_local_folder_page_tree_snapshot,
|
||||
local_folder_watch_revision, local_workspace_id_from_root_uri,
|
||||
};
|
||||
use crate::routes::query_support::{
|
||||
fetch_documents_meta_via_convex, resolve_effective_workspace_id,
|
||||
fetch_documents_meta_via_legacy_cloud, resolve_effective_workspace_id,
|
||||
};
|
||||
use crate::routes::snapshot_support::{load_projection_snapshot, ProjectionSnapshotSpec};
|
||||
use crate::transport::convex::execute_convex_mutation_by_name;
|
||||
use crate::routes::snapshot_support::{ProjectionSnapshotSpec, load_projection_snapshot};
|
||||
use crate::transport::convex::execute_retired_mutation_by_name;
|
||||
use crate::tree_shell::filetree_renderer::{
|
||||
render_initial_filetree_html, FileTreeInitialRenderInput, FileTreeRenderRow,
|
||||
FileTreeInitialRenderInput, FileTreeRenderRow, render_initial_filetree_html,
|
||||
};
|
||||
use crate::tree_shell::filetree_selection::FileTreeSelectionState;
|
||||
use crate::tree_shell::page_renderer::{
|
||||
render_initial_page_tree_html, PageTreeInitialRenderInput, PageTreeRenderRow,
|
||||
PageTreeInitialRenderInput, PageTreeRenderRow, render_initial_page_tree_html,
|
||||
};
|
||||
use crate::tree_shell::picker_renderer::{
|
||||
render_initial_picker_html, PickerInitialRenderInput, PickerRenderRow,
|
||||
PickerInitialRenderInput, PickerRenderRow, render_initial_picker_html,
|
||||
};
|
||||
use crate::tree_shell::renderer_input::{
|
||||
FileTreeRendererInput, PageTreeRendererInput, PickerRendererInput, TreeShellCommandDispatcher,
|
||||
TreeShellRendererInput,
|
||||
};
|
||||
use crate::tree_shell::runtime_api::{
|
||||
reduce_tree_shell_runtime as reduce_tree_shell_runtime_request, TreeShellRuntimeRequest,
|
||||
TreeShellRuntimeResult,
|
||||
TreeShellRuntimeRequest, TreeShellRuntimeResult,
|
||||
reduce_tree_shell_runtime as reduce_tree_shell_runtime_request,
|
||||
};
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{header, HeaderValue, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{HeaderValue, StatusCode, header};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use bridge_runtime::RuntimeCommandEnvelopeWire;
|
||||
use core_protocol::KernelProjectionKind;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -137,6 +137,69 @@ impl TreeCommandEnvelopeContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct TreeCompatAliasCatalogEntry {
|
||||
pub compat_command: &'static str,
|
||||
pub preferred_command: &'static str,
|
||||
pub source_kind: &'static str,
|
||||
pub retained_for: &'static str,
|
||||
pub retirement_condition: &'static str,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const TREE_DOCUMENT_COMPAT_ALIAS_CATALOG: &[TreeCompatAliasCatalogEntry] = &[
|
||||
TreeCompatAliasCatalogEntry {
|
||||
compat_command: "documents.create",
|
||||
preferred_command: "tree.node.create",
|
||||
source_kind: "convex_workspace",
|
||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
||||
retirement_condition: "all cloud and remote callers emit tree.node.create",
|
||||
},
|
||||
TreeCompatAliasCatalogEntry {
|
||||
compat_command: "documents.title.update",
|
||||
preferred_command: "tree.node.rename",
|
||||
source_kind: "convex_workspace",
|
||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
||||
retirement_condition: "all cloud and remote callers emit tree.node.rename",
|
||||
},
|
||||
TreeCompatAliasCatalogEntry {
|
||||
compat_command: "documents.move",
|
||||
preferred_command: "tree.subtree.move",
|
||||
source_kind: "convex_workspace",
|
||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
||||
retirement_condition: "all cloud and remote callers emit tree.subtree.move",
|
||||
},
|
||||
TreeCompatAliasCatalogEntry {
|
||||
compat_command: "documents.delete",
|
||||
preferred_command: "tree.node.archive",
|
||||
source_kind: "convex_workspace",
|
||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
||||
retirement_condition: "all cloud and remote callers emit tree.node.archive",
|
||||
},
|
||||
TreeCompatAliasCatalogEntry {
|
||||
compat_command: "documents.restore",
|
||||
preferred_command: "tree.node.restore",
|
||||
source_kind: "convex_workspace",
|
||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
||||
retirement_condition: "all cloud and remote callers emit tree.node.restore",
|
||||
},
|
||||
TreeCompatAliasCatalogEntry {
|
||||
compat_command: "documents.purge",
|
||||
preferred_command: "tree.node.purge",
|
||||
source_kind: "convex_workspace",
|
||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
||||
retirement_condition: "all cloud and remote callers emit tree.node.purge",
|
||||
},
|
||||
TreeCompatAliasCatalogEntry {
|
||||
compat_command: "documents.copy_tree",
|
||||
preferred_command: "tree.subtree.copy",
|
||||
source_kind: "convex_workspace",
|
||||
retained_for: "legacy cloud / remote callers still emitting documents.*",
|
||||
retirement_condition: "all cloud and remote callers emit tree.subtree.copy",
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TreeCommandRequest {
|
||||
Create {
|
||||
@@ -2074,7 +2137,7 @@ async fn resolve_tree_create_workspace_id(
|
||||
|
||||
if let Some(parent_id) = parent_id.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
let parent_meta =
|
||||
fetch_documents_meta_via_convex(state.config(), context, None, parent_id).await?;
|
||||
fetch_documents_meta_via_legacy_cloud(state.config(), context, None, parent_id).await?;
|
||||
if let Some(workspace_id) = parent_meta
|
||||
.get("workspace_id")
|
||||
.or_else(|| parent_meta.get("workspaceId"))
|
||||
@@ -2086,7 +2149,7 @@ async fn resolve_tree_create_workspace_id(
|
||||
}
|
||||
}
|
||||
|
||||
let bootstrap = execute_convex_mutation_by_name(
|
||||
let bootstrap = execute_retired_mutation_by_name(
|
||||
state.config(),
|
||||
context,
|
||||
"workspaces:ensureDefaultWorkspace",
|
||||
@@ -2201,7 +2264,15 @@ pub async fn tree_command(
|
||||
.with_context(&context)
|
||||
.with_header("x-error-phase", "tree_command_decode")
|
||||
})?;
|
||||
let envelope_context = TreeCommandEnvelopeContext::from_envelope(&raw_request);
|
||||
let mut envelope_context = TreeCommandEnvelopeContext::from_envelope(&raw_request);
|
||||
if envelope_context.source_kind.is_none()
|
||||
&& envelope_context
|
||||
.root_uri
|
||||
.as_deref()
|
||||
.is_some_and(|root_uri| root_uri.trim().starts_with("file://"))
|
||||
{
|
||||
envelope_context.source_kind = Some("local_folder".into());
|
||||
}
|
||||
let request = match raw_request.action.trim() {
|
||||
"create" => TreeCommandRequest::Create {
|
||||
workspace_id: raw_request.workspace_id,
|
||||
@@ -2453,7 +2524,7 @@ pub async fn tree_command(
|
||||
command_wire.preflight_data =
|
||||
Some(load_tree_move_preflight_data(&state, &context, &effective_workspace_id).await?);
|
||||
}
|
||||
let execution = execute_runtime_command_via_convex_with_artifacts(
|
||||
let execution = execute_runtime_command_via_legacy_cloud_with_artifacts(
|
||||
&state,
|
||||
&context,
|
||||
Some(&effective_workspace_id),
|
||||
@@ -2513,10 +2584,10 @@ mod tests {
|
||||
include_str!("../../browser/tree-shell-filetree-dnd-runtime.js");
|
||||
|
||||
use super::{
|
||||
collect_filetree_render_rows, create_command_wire, TreeCommandEnvelopeContext,
|
||||
TreeCommandRequest,
|
||||
TREE_DOCUMENT_COMPAT_ALIAS_CATALOG, TreeCommandEnvelopeContext, TreeCommandRequest,
|
||||
collect_filetree_render_rows, create_command_wire,
|
||||
};
|
||||
use crate::app::{build_app, AppConfig, AppState};
|
||||
use crate::app::{AppConfig, AppState, build_app};
|
||||
use crate::context::RequestContext;
|
||||
use crate::routes::command_support::build_runtime_command_plan;
|
||||
use axum::body::Body;
|
||||
@@ -2540,7 +2611,7 @@ mod tests {
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: Some(r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"media_assets":[{"id":"asset_file_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"封面.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_child_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"节点图片.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_pdf_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"说明书.pdf","mime_type":"application/pdf","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_book_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"小说.epub","mime_type":"application/epub+zip","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_assets":[{"id":"mind_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"mindmap","file_name":"头脑风暴.json","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"table_assets":[{"id":"table_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"luckysheet","file_name":"预算.luckysheet","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_asset_children":{"mind_1":["asset_child_1"]}}}"#.into()),
|
||||
query_fixtures_json: Some(r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"media_assets":[{"id":"asset_file_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"封面.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_child_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"节点图片.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_pdf_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"说明书.pdf","mime_type":"application/pdf","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_book_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"小说.epub","mime_type":"application/epub+zip","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_assets":[{"id":"mind_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"mindmap","file_name":"头脑风暴.json","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"table_assets":[{"id":"table_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"table","file_name":"预算.table","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_asset_children":{"mind_1":["asset_child_1"]}}}"#.into()),
|
||||
mutation_fixtures_json: Some(r#"{"workspaces:ensureDefaultWorkspace":{"workspaces":[{"id":"ws_demo","name":"我的空间","type":"personal","iconUrl":null,"memberCount":1,"isDefault":true}],"activeWorkspaceId":"ws_demo"},"documents:createWithParentReference":{"ok":true,"id":"page_new","updated_at":"2026-04-17T00:00:00Z"},"documents:updateTitle":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"},"documents:move":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"},"documents:purge":{"ok":true,"deletedCount":1},"bridgeLogs:recordCommandLog":{"ok":true,"id":"clog_fixture"},"bridgeLogs:recordDomainEvent":{"ok":true,"id":"evt_fixture"}}"#.into()),
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
@@ -2681,14 +2752,20 @@ mod tests {
|
||||
assert!(html.contains("data-rust-action=\"toggle\""));
|
||||
assert!(html.contains("data-testid=\"tree-node-toggle\""));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("hydrateInitialPageTree"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS
|
||||
.contains("const usedRustInitialPageRenderer = hydrateInitialPageTree();"));
|
||||
assert!(
|
||||
TREE_SHELL_RUNTIME_JS
|
||||
.contains("const usedRustInitialPageRenderer = hydrateInitialPageTree();")
|
||||
);
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("applyRemovedDocumentLocally"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS
|
||||
.contains("sourceKind === \"convex_workspace\" && applyCreatedDocumentLocally"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS
|
||||
.contains("if (!deletableItems.every((item) => !fileTreeRowById.has(item.rowId)))"));
|
||||
assert!(
|
||||
TREE_SHELL_RUNTIME_JS
|
||||
.contains("sourceKind === \"convex_workspace\" && applyCreatedDocumentLocally")
|
||||
);
|
||||
assert!(
|
||||
TREE_SHELL_RUNTIME_JS
|
||||
.contains("if (!deletableItems.every((item) => !fileTreeRowById.has(item.rowId)))")
|
||||
);
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("application/x-mnote-page-tree-node"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("页面已拖放到"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("setAttribute(\"role\", \"treeitem\")"));
|
||||
@@ -2713,8 +2790,11 @@ mod tests {
|
||||
.expect("body");
|
||||
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(html.contains("id=\"tree-shell-state\""));
|
||||
assert!(html
|
||||
.contains("type=\"module\" src=\"/api/mnote-browser-runtime/tree-shell-runtime.js\""));
|
||||
assert!(
|
||||
html.contains(
|
||||
"type=\"module\" src=\"/api/mnote-browser-runtime/tree-shell-runtime.js\""
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
!html.contains("const stateElement = document.getElementById(\"tree-shell-state\")"),
|
||||
"debug /tree runtime should live in browser/tree-shell-runtime.js, not inline Rust HTML"
|
||||
@@ -2749,17 +2829,23 @@ mod tests {
|
||||
assert!(html.contains("\"contractName\":\"rust_picker_state_reducer_v1\""));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("applyPickerStateAction"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("postPickerPickResultToHost"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS
|
||||
.contains("applyPickerFocusByItemKey(\"__root__\", { focusDom: true })"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS
|
||||
.contains("applyPickerFocusByItemKey(item.nodeId, { focusDom: true })"));
|
||||
assert!(
|
||||
TREE_SHELL_RUNTIME_JS
|
||||
.contains("applyPickerFocusByItemKey(\"__root__\", { focusDom: true })")
|
||||
);
|
||||
assert!(
|
||||
TREE_SHELL_RUNTIME_JS
|
||||
.contains("applyPickerFocusByItemKey(item.nodeId, { focusDom: true })")
|
||||
);
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("const shouldFocusDom = options.focusDom === true"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("if (shouldFocusDom) focusPickerRowElement"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("patchPickerActiveDom"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("hydrateInitialPickerTree"));
|
||||
assert!(html.contains("tabindex=\""));
|
||||
assert!(TREE_SHELL_RUNTIME_JS
|
||||
.contains("const usedRustInitialRenderer = hydrateInitialRenderer();"));
|
||||
assert!(
|
||||
TREE_SHELL_RUNTIME_JS
|
||||
.contains("const usedRustInitialRenderer = hydrateInitialRenderer();")
|
||||
);
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("__MNOTE_TREE_SHELL_OVERRIDE__"));
|
||||
}
|
||||
|
||||
@@ -2799,12 +2885,18 @@ mod tests {
|
||||
assert!(
|
||||
TREE_SHELL_FILETREE_RUNTIME_JS.contains("function getFileTreeRowOwnerDocumentId(item)")
|
||||
);
|
||||
assert!(TREE_SHELL_FILETREE_MENU_RUNTIME_JS
|
||||
.contains("function buildFileTreeMenuTarget(context"));
|
||||
assert!(TREE_SHELL_FILETREE_DND_RUNTIME_JS
|
||||
.contains("function createTreeShellFileTreeDndRuntime(context)"));
|
||||
assert!(TREE_SHELL_RENDER_RUNTIME_JS
|
||||
.contains("row.dataset.ownerDocumentId = ownerDocumentId || \"\";"));
|
||||
assert!(
|
||||
TREE_SHELL_FILETREE_MENU_RUNTIME_JS
|
||||
.contains("function buildFileTreeMenuTarget(context")
|
||||
);
|
||||
assert!(
|
||||
TREE_SHELL_FILETREE_DND_RUNTIME_JS
|
||||
.contains("function createTreeShellFileTreeDndRuntime(context)")
|
||||
);
|
||||
assert!(
|
||||
TREE_SHELL_RENDER_RUNTIME_JS
|
||||
.contains("row.dataset.ownerDocumentId = ownerDocumentId || \"\";")
|
||||
);
|
||||
assert!(
|
||||
TREE_SHELL_RUNTIME_JS.contains("if (rowId && getFileTreeRowDocumentId(renameItem))")
|
||||
);
|
||||
@@ -2812,8 +2904,10 @@ mod tests {
|
||||
assert!(TREE_SHELL_RENDER_RUNTIME_JS.contains("documentId: ownerDocumentId || null"));
|
||||
assert!(TREE_SHELL_RENDER_RUNTIME_JS.contains("dragover"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS.contains("hydrateInitialFileTree"));
|
||||
assert!(TREE_SHELL_RUNTIME_JS
|
||||
.contains("const usedRustInitialRenderer = hydrateInitialRenderer();"));
|
||||
assert!(
|
||||
TREE_SHELL_RUNTIME_JS
|
||||
.contains("const usedRustInitialRenderer = hydrateInitialRenderer();")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3026,8 +3120,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tree_command_local_folder_create_rename_copy_trash_restore_and_purge_use_same_endpoint(
|
||||
) {
|
||||
async fn tree_command_local_folder_create_rename_copy_trash_restore_and_purge_use_same_endpoint()
|
||||
{
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("mnote-local-tree-command-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
@@ -3142,11 +3236,12 @@ mod tests {
|
||||
String::from_utf8_lossy(&move_body)
|
||||
);
|
||||
assert!(!root.join("重命名页面").exists());
|
||||
assert!(root
|
||||
.join("docs")
|
||||
.join("重命名页面")
|
||||
.join("重命名页面.md")
|
||||
.exists());
|
||||
assert!(
|
||||
root.join("docs")
|
||||
.join("重命名页面")
|
||||
.join("重命名页面.md")
|
||||
.exists()
|
||||
);
|
||||
let move_payload: Value = serde_json::from_slice(&move_body).expect("move json");
|
||||
let moved_document_id = move_payload["result"]["documentId"]
|
||||
.as_str()
|
||||
@@ -3186,11 +3281,12 @@ mod tests {
|
||||
.as_str()
|
||||
.expect("copied document id")
|
||||
.to_string();
|
||||
assert!(root
|
||||
.join("docs")
|
||||
.join("重命名页面 2")
|
||||
.join("重命名页面 2.md")
|
||||
.exists());
|
||||
assert!(
|
||||
root.join("docs")
|
||||
.join("重命名页面 2")
|
||||
.join("重命名页面 2.md")
|
||||
.exists()
|
||||
);
|
||||
|
||||
let folder_response = app()
|
||||
.oneshot(
|
||||
@@ -3223,12 +3319,13 @@ mod tests {
|
||||
.expect("response");
|
||||
assert_eq!(delete_response.status(), StatusCode::OK);
|
||||
assert!(!root.join("docs").join("重命名页面").exists());
|
||||
assert!(root
|
||||
.join(".mnote")
|
||||
.join("trash")
|
||||
.join("重命名页面")
|
||||
.join("重命名页面.md")
|
||||
.exists());
|
||||
assert!(
|
||||
root.join(".mnote")
|
||||
.join("trash")
|
||||
.join("重命名页面")
|
||||
.join("重命名页面.md")
|
||||
.exists()
|
||||
);
|
||||
assert!(root.join(".mnote").join("trash-index.json").exists());
|
||||
assert!(!root.join(".mnote").join("page-ids.json").exists());
|
||||
|
||||
@@ -3255,11 +3352,12 @@ mod tests {
|
||||
"{}",
|
||||
String::from_utf8_lossy(&restore_body)
|
||||
);
|
||||
assert!(root
|
||||
.join("docs")
|
||||
.join("重命名页面")
|
||||
.join("重命名页面.md")
|
||||
.exists());
|
||||
assert!(
|
||||
root.join("docs")
|
||||
.join("重命名页面")
|
||||
.join("重命名页面.md")
|
||||
.exists()
|
||||
);
|
||||
let restore_payload: Value = serde_json::from_slice(&restore_body).expect("restore json");
|
||||
assert_eq!(
|
||||
restore_payload["result"]["documentId"].as_str(),
|
||||
@@ -3353,7 +3451,21 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(restore_response.status(), StatusCode::OK);
|
||||
let restore_status = restore_response.status();
|
||||
let restore_body = axum::body::to_bytes(restore_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("restore body");
|
||||
assert_eq!(
|
||||
restore_status,
|
||||
StatusCode::OK,
|
||||
"{}",
|
||||
String::from_utf8_lossy(&restore_body)
|
||||
);
|
||||
let restore_payload: Value = serde_json::from_slice(&restore_body).expect("restore json");
|
||||
assert_eq!(
|
||||
restore_payload["result"]["execution"]["canonicalCommand"],
|
||||
"tree.resource.restore"
|
||||
);
|
||||
assert!(root.join("docs").join("photo.png").exists());
|
||||
assert!(!root.join(".mnote").join("trash").join("photo.png").exists());
|
||||
|
||||
@@ -3364,13 +3476,28 @@ mod tests {
|
||||
.uri("/api/tree/commands")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(format!(
|
||||
r#"{{"action":"delete","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{asset_id}"}}"#
|
||||
r#"{{"action":"delete","rootUri":"{root_uri}","documentId":"{asset_id}"}}"#
|
||||
)))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(delete_again_response.status(), StatusCode::OK);
|
||||
let delete_again_status = delete_again_response.status();
|
||||
let delete_again_body = axum::body::to_bytes(delete_again_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("delete again body");
|
||||
assert_eq!(
|
||||
delete_again_status,
|
||||
StatusCode::OK,
|
||||
"{}",
|
||||
String::from_utf8_lossy(&delete_again_body)
|
||||
);
|
||||
let delete_again_payload: Value =
|
||||
serde_json::from_slice(&delete_again_body).expect("delete again json");
|
||||
assert_eq!(
|
||||
delete_again_payload["result"]["execution"]["canonicalCommand"],
|
||||
"tree.resource.archive"
|
||||
);
|
||||
|
||||
let purge_response = app()
|
||||
.oneshot(
|
||||
@@ -3385,7 +3512,21 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(purge_response.status(), StatusCode::OK);
|
||||
let purge_status = purge_response.status();
|
||||
let purge_body = axum::body::to_bytes(purge_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("purge body");
|
||||
assert_eq!(
|
||||
purge_status,
|
||||
StatusCode::OK,
|
||||
"{}",
|
||||
String::from_utf8_lossy(&purge_body)
|
||||
);
|
||||
let purge_payload: Value = serde_json::from_slice(&purge_body).expect("purge json");
|
||||
assert_eq!(
|
||||
purge_payload["result"]["execution"]["canonicalCommand"],
|
||||
"tree.resource.purge"
|
||||
);
|
||||
assert!(!root.join("docs").join("photo.png").exists());
|
||||
assert!(!root.join(".mnote").join("trash").join("photo.png").exists());
|
||||
let trash_index_after =
|
||||
@@ -3432,10 +3573,12 @@ mod tests {
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert_eq!(payload["ok"], false);
|
||||
assert_eq!(payload["code"], "local_folder_root_escape");
|
||||
assert!(payload["message"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("root"));
|
||||
assert!(
|
||||
payload["message"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("root")
|
||||
);
|
||||
assert!(payload["requestId"].as_str().unwrap_or_default().len() > 0);
|
||||
assert_eq!(
|
||||
headers
|
||||
@@ -3604,8 +3747,10 @@ mod tests {
|
||||
assert!(filetree_html.contains(
|
||||
"\"wasmModuleUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm\""
|
||||
));
|
||||
assert!(filetree_html
|
||||
.contains("\"jsGlueUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime.js\""));
|
||||
assert!(
|
||||
filetree_html
|
||||
.contains("\"jsGlueUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime.js\"")
|
||||
);
|
||||
assert!(filetree_html.contains(
|
||||
"\"outputChannels\":[\"commandDispatchEvent\",\"domPatch\",\"intentEvent\"]"
|
||||
));
|
||||
@@ -3633,8 +3778,10 @@ mod tests {
|
||||
assert!(picker_html.contains(
|
||||
"\"wasmModuleUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm\""
|
||||
));
|
||||
assert!(picker_html
|
||||
.contains("\"jsGlueUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime.js\""));
|
||||
assert!(
|
||||
picker_html
|
||||
.contains("\"jsGlueUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime.js\"")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4050,11 +4197,13 @@ mod tests {
|
||||
Some("convex://workspace/ws_demo")
|
||||
);
|
||||
assert_eq!(create_wire.source.workspace_id.as_deref(), Some("ws_demo"));
|
||||
assert!(create_wire
|
||||
.source
|
||||
.capabilities
|
||||
.iter()
|
||||
.any(|capability| capability == "execute-command"));
|
||||
assert!(
|
||||
create_wire
|
||||
.source
|
||||
.capabilities
|
||||
.iter()
|
||||
.any(|capability| capability == "execute-command")
|
||||
);
|
||||
|
||||
let rename_wire = create_command_wire(
|
||||
&context,
|
||||
@@ -4242,4 +4391,42 @@ mod tests {
|
||||
assert_eq!(tree_move_plan.function_name, "documents:move");
|
||||
assert_eq!(tree_move_plan.function_name, compat_move_plan.function_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_documents_compat_alias_catalog_marks_cloud_retirement_boundary() {
|
||||
let aliases = TREE_DOCUMENT_COMPAT_ALIAS_CATALOG;
|
||||
assert!(
|
||||
aliases
|
||||
.iter()
|
||||
.all(|entry| entry.compat_command.starts_with("documents."))
|
||||
);
|
||||
assert!(
|
||||
aliases
|
||||
.iter()
|
||||
.all(|entry| entry.preferred_command.starts_with("tree."))
|
||||
);
|
||||
assert!(
|
||||
aliases
|
||||
.iter()
|
||||
.all(|entry| entry.source_kind == "convex_workspace")
|
||||
);
|
||||
assert!(
|
||||
aliases
|
||||
.iter()
|
||||
.all(|entry| entry.retained_for.contains("legacy cloud"))
|
||||
);
|
||||
assert!(
|
||||
aliases
|
||||
.iter()
|
||||
.all(|entry| entry.retirement_condition.contains("emit tree."))
|
||||
);
|
||||
assert!(aliases.iter().any(|entry| {
|
||||
entry.compat_command == "documents.delete"
|
||||
&& entry.preferred_command == "tree.node.archive"
|
||||
}));
|
||||
assert!(aliases.iter().any(|entry| {
|
||||
entry.compat_command == "documents.copy_tree"
|
||||
&& entry.preferred_command == "tree.subtree.copy"
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user