feat: consolidate local-first mnote web runtime

This commit is contained in:
lix-2026
2026-05-28 22:01:44 +08:00
parent 7354807ee9
commit 39b9a0183a
154 changed files with 13591 additions and 12728 deletions
@@ -10,9 +10,9 @@ use crate::routes::local_markdown_parser::{
};
use crate::routes::local_search_index;
use crate::routes::snapshot_support::ProjectionSnapshot;
use axum::extract::{Extension, Multipart, Path as AxumPath, Query, State};
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
use axum::Json;
use axum::extract::{Extension, Multipart, Path as AxumPath, Query, State};
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
use bridge_runtime::project_legacy_content_to_block_document;
use control_plane::AppendAuditInput;
use control_plane::{
@@ -24,7 +24,7 @@ use core_protocol::{
};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use serde_json::{Map, Value, json};
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeMap, BTreeSet};
@@ -41,6 +41,9 @@ static LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS: std::sync::atomic::AtomicU64 =
#[cfg(test)]
static LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[cfg(test)]
static LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS_BY_ROOT: OnceLock<Mutex<BTreeMap<String, u64>>> =
OnceLock::new();
#[derive(Debug, Clone)]
struct LocalPageTreeSnapshotCacheEntry {
@@ -52,8 +55,8 @@ static LOCAL_PAGE_TREE_SNAPSHOT_CACHE: OnceLock<
Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>>,
> = OnceLock::new();
fn local_page_tree_snapshot_cache(
) -> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
fn local_page_tree_snapshot_cache()
-> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
}
@@ -61,6 +64,11 @@ fn local_page_tree_snapshot_cache(
pub(crate) fn reset_local_page_tree_snapshot_test_loads() {
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
if let Some(loads) = LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS_BY_ROOT.get() {
if let Ok(mut loads) = loads.lock() {
loads.clear();
}
}
if let Some(cache) = LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get() {
if let Ok(mut cache) = cache.lock() {
cache.clear();
@@ -69,8 +77,13 @@ pub(crate) fn reset_local_page_tree_snapshot_test_loads() {
}
#[cfg(test)]
pub(crate) fn local_page_tree_snapshot_test_loads() -> u64 {
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.load(std::sync::atomic::Ordering::SeqCst)
pub(crate) fn local_page_tree_snapshot_test_loads_for_root(root_uri: &str) -> u64 {
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS_BY_ROOT
.get_or_init(|| Mutex::new(BTreeMap::new()))
.lock()
.ok()
.and_then(|loads| loads.get(root_uri).copied())
.unwrap_or(0)
}
#[cfg(test)]
@@ -2784,6 +2797,15 @@ fn load_local_folder_page_tree_snapshot_for_scope(
) -> Result<ProjectionSnapshot, WebError> {
#[cfg(test)]
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
#[cfg(test)]
{
if let Ok(mut loads) = LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS_BY_ROOT
.get_or_init(|| Mutex::new(BTreeMap::new()))
.lock()
{
*loads.entry(root_uri.to_string()).or_insert(0) += 1;
}
}
let root_path = parse_file_root_uri(root_uri)?;
let canonical_root = root_path.canonicalize().map_err(|error| {
@@ -3255,10 +3277,12 @@ pub fn write_local_markdown_page_body(
relative_path,
&request.document_id,
);
store.mark_saved(
store.mark_saved_with_operation(
&ws_path,
file_version.to_string(),
format!("sha256:{file_version}"),
request.write_intent_id.clone(),
request.save_operation_id.clone(),
);
}
}
@@ -3283,6 +3307,22 @@ pub fn write_local_markdown_page_body(
.map(Value::from)
.unwrap_or(Value::Null),
);
map.insert(
"writeIntentId".into(),
request
.write_intent_id
.as_deref()
.map(Value::from)
.unwrap_or(Value::Null),
);
map.insert(
"saveOperationId".into(),
request
.save_operation_id
.as_deref()
.map(Value::from)
.unwrap_or(Value::Null),
);
}
Ok(result)
}
@@ -3818,7 +3858,7 @@ pub(crate) fn write_local_folder_file_upload(
"上传目标必须是本地 root 内的目录",
));
}
let sanitized_name = sanitize_file_name(&file.name, "附件");
let sanitized_name = sanitize_uploaded_file_name(&file.name, "附件");
let target = next_available_raw_path(&target_dir, &sanitized_name);
fs::write(&target, &file.bytes).map_err(|error| {
WebError::bad_request_code(
@@ -3908,7 +3948,7 @@ pub(crate) fn write_local_markdown_asset(
)
})?;
let sanitized_name = sanitize_file_name(&file.name, "附件");
let sanitized_name = sanitize_uploaded_file_name(&file.name, "附件");
let target = next_available_asset_path(&asset_dir, &sanitized_name);
if !target.starts_with(&canonical_root) {
return Err(WebError::bad_request_code(
@@ -7023,11 +7063,7 @@ fn parent_key_for_relative_path(relative_path: &str) -> String {
.map(|component| component.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("/");
if value.is_empty() {
None
} else {
Some(value)
}
if value.is_empty() { None } else { Some(value) }
})
.map(|value| normalize_file_order_parent_key(&value))
.unwrap_or_else(|| ".".to_string())
@@ -7517,11 +7553,16 @@ fn local_folder_row_to_projection_item(row: &LocalFolderRow) -> Value {
}
"asset" => match row.icon_hint.as_str() {
"mindmap" => KernelObjectKind::Mindmap,
"onlyoffice" | "office" => KernelObjectKind::OnlyOffice,
"onlyoffice" | "office" | "word" | "ppt" | "sheet" => KernelObjectKind::OnlyOffice,
_ => KernelObjectKind::Attachment,
},
_ => KernelObjectKind::Attachment,
};
let object_asset_id = if row.row_kind == "asset" && !row.relative_path.trim().is_empty() {
Some(format!("local-file:{}", row.relative_path))
} else {
None
};
let workspace_path = ObjectWorkspacePath {
workspace_id: row.workspace_id.clone(),
source_kind: WorkspaceSourceKind::LocalFolder,
@@ -7531,7 +7572,7 @@ fn local_folder_row_to_projection_item(row: &LocalFolderRow) -> Value {
object_kind,
document_id: row.document_id.clone(),
block_id: None,
asset_id: None,
asset_id: object_asset_id,
},
resource_kind: Some(row.row_kind.clone()),
};
@@ -7733,21 +7774,22 @@ fn icon_hint_for_entry(entry: &LocalFolderEntry) -> String {
"pdf"
} else if matches!(
extension(&lower).as_deref(),
Some(
"doc"
| "docx"
| "odt"
| "rtf"
| "ppt"
| "pptx"
| "odp"
| "xls"
| "xlsx"
| "ods"
| "csv"
)
Some("doc" | "docx" | "odt" | "rtf")
) {
"office"
"word"
} else if matches!(extension(&lower).as_deref(), Some("ppt" | "pptx" | "odp")) {
"ppt"
} else if matches!(
extension(&lower).as_deref(),
Some("xls" | "xlsx" | "ods" | "csv")
) {
"sheet"
} else if is_web_file_name(&lower) {
"web"
} else if is_config_file_name(&lower) {
"config"
} else if is_code_file_name(&lower) {
"code"
} else if matches!(extension(&lower).as_deref(), Some("epub" | "mobi")) {
"book"
} else {
@@ -7756,6 +7798,135 @@ fn icon_hint_for_entry(entry: &LocalFolderEntry) -> String {
.to_string()
}
fn is_web_file_name(file_name: &str) -> bool {
matches!(
extension(file_name).as_deref(),
Some(
"html"
| "htm"
| "css"
| "scss"
| "less"
| "js"
| "jsx"
| "ts"
| "tsx"
| "mjs"
| "cjs"
| "vue"
| "svelte"
| "astro"
)
)
}
fn is_config_file_name(file_name: &str) -> bool {
let code_file_names = [
".dockerignore",
".editorconfig",
".env",
".eslintrc",
".gitattributes",
".gitignore",
".npmrc",
".prettierrc",
"dockerfile",
"containerfile",
"makefile",
"cmakelists.txt",
"gemfile",
"rakefile",
"procfile",
];
code_file_names.contains(&file_name)
|| matches!(
extension(file_name).as_deref(),
Some(
"json"
| "jsonc"
| "json5"
| "toml"
| "yaml"
| "yml"
| "ini"
| "env"
| "xml"
| "lock"
| "hcl"
| "tf"
| "tfvars"
| "nix"
| "properties"
| "conf"
| "cfg"
| "config"
| "service"
| "desktop"
| "gitignore"
| "gitattributes"
| "editorconfig"
| "npmrc"
| "prettierrc"
| "eslintrc"
)
)
}
fn is_code_file_name(file_name: &str) -> bool {
matches!(
extension(file_name).as_deref(),
Some(
"rs" | "py"
| "go"
| "java"
| "c"
| "cpp"
| "h"
| "hpp"
| "cs"
| "php"
| "rb"
| "sh"
| "bash"
| "zsh"
| "sql"
| "lua"
| "dart"
| "kt"
| "kts"
| "swift"
| "scala"
| "gradle"
| "groovy"
| "clj"
| "ex"
| "exs"
| "erl"
| "hrl"
| "fs"
| "fsx"
| "r"
| "jl"
| "m"
| "mm"
| "pl"
| "pm"
| "ps1"
| "bat"
| "cmd"
| "psm1"
| "psd1"
| "proto"
| "graphql"
| "gql"
| "prisma"
| "cmake"
| "bazel"
| "bzl"
)
)
}
fn extension(file_name: &str) -> Option<String> {
Path::new(file_name)
.extension()
@@ -7850,6 +8021,15 @@ fn sanitize_file_name(value: &str, fallback: &str) -> String {
}
}
fn sanitize_uploaded_file_name(value: &str, fallback: &str) -> String {
let trimmed = value.trim();
let leaf = trimmed
.rsplit(['/', '\\'])
.find(|part| !part.trim().is_empty())
.unwrap_or(trimmed);
sanitize_file_name(leaf, fallback)
}
fn next_available_path(directory: &Path, stem: &str, extension: &str) -> PathBuf {
let first = directory.join(format!("{stem}.{extension}"));
if !first.exists() {
@@ -8919,7 +9099,7 @@ fn system_time_ms(time: SystemTime) -> u128 {
.unwrap_or(0)
}
fn local_markdown_conflict_detection_key(
pub(crate) fn local_markdown_conflict_detection_key(
document_id: &str,
markdown_path: &Path,
) -> Result<String, WebError> {
@@ -9044,6 +9224,10 @@ fn markdown_page_subtree(document_id: &str, title: &str, content: &Value) -> Val
#[cfg(test)]
mod tests {
use super::{
LocalAccessGrantRequest, LocalAccessValidateRootRequest, LocalFileOpenQuery,
LocalResourceReadQuery, LocalResourceWriteRequest, LocalShareGrantRequest,
LocalShareLinkRequest, LocalUploadFile, ShareLinkListQuery, SharedCacheRecordRequest,
SyncConflictReportRequest, SyncPendingChangeRequest,
add_sqlite_local_access_grant_for_context,
create_default_local_workspace_for_actor_at_base, create_local_access_grant,
create_share_grant, create_share_link, create_user_access_grant, create_user_share_grant,
@@ -9062,18 +9246,15 @@ mod tests {
resolve_local_markdown_page_aggregate, save_local_markdown_page,
update_local_markdown_title, validate_local_access_root, write_local_markdown_asset,
write_local_markdown_page_body, write_local_mindmap_data, write_local_resource,
write_sync_conflict_report, LocalAccessGrantRequest, LocalAccessValidateRootRequest,
LocalFileOpenQuery, LocalResourceReadQuery, LocalResourceWriteRequest,
LocalShareGrantRequest, LocalShareLinkRequest, LocalUploadFile, ShareLinkListQuery,
SharedCacheRecordRequest, SyncConflictReportRequest, SyncPendingChangeRequest,
write_sync_conflict_report,
};
use crate::app::{AppConfig, AppState};
use crate::context::RequestContext;
use axum::Json;
use axum::extract::{Extension, Path as AxumPath, Query, State};
use axum::http::{HeaderMap, Method, StatusCode};
use axum::Json;
use control_plane::{DirectoryGrantInput, UpsertUserInput};
use serde_json::{json, Value};
use serde_json::{Value, json};
use std::sync::Mutex;
fn env_lock() -> &'static Mutex<()> {
@@ -9345,28 +9526,25 @@ mod tests {
"tableCell"
);
assert_eq!(
table["props"]["tiptapTable"]["content"][0]["content"][0]["content"][0]["content"][0]
["text"],
table["props"]["tiptapTable"]["content"][0]["content"][0]["content"][0]["content"][0]["text"],
""
);
assert_eq!(
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]
["text"],
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]["text"],
"A"
);
assert_eq!(
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]
["marks"][0]["type"],
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]["marks"]
[0]["type"],
"code"
);
assert_eq!(
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]
["text"],
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]["text"],
"B"
);
assert_eq!(
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]
["marks"][0]["type"],
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]["marks"]
[0]["type"],
"bold"
);
}
@@ -9469,10 +9647,12 @@ mod tests {
let body = serde_json::to_value(&aggregate.body).expect("body json");
assert_eq!(body["fileVersion"], body["conflictDetectionKey"]);
assert!(body["fileVersion"]
.as_str()
.expect("file version")
.starts_with("local-md:local-md:README.md:"));
assert!(
body["fileVersion"]
.as_str()
.expect("file version")
.starts_with("local-md:local-md:README.md:")
);
let _ = std::fs::remove_dir_all(&root);
}
@@ -9494,10 +9674,12 @@ mod tests {
.expect("save");
assert_eq!(result["fileVersion"], result["conflict_detection_key"]);
assert!(result["fileVersion"]
.as_str()
.expect("file version")
.starts_with("local-md:local-md:README.md:"));
assert!(
result["fileVersion"]
.as_str()
.expect("file version")
.starts_with("local-md:local-md:README.md:")
);
let _ = std::fs::remove_dir_all(&root);
}
@@ -9516,6 +9698,8 @@ mod tests {
source_kind: core_protocol::WorkspaceSourceKind::LocalFolder,
root_uri: root_uri.clone(),
expected_file_version: aggregate.body.file_version.as_str().map(ToOwned::to_owned),
write_intent_id: Some("intent:test:page-body-write".into()),
save_operation_id: Some("save:test:page-body-write".into()),
base_content_hash: Some("sha256:test-base".into()),
content_format: "editorBlocks".into(),
content: serde_json::json!([
@@ -9530,6 +9714,8 @@ mod tests {
assert_eq!(result["compatCommand"], "page.body.save");
assert_eq!(result["contentFormat"], "editorBlocks");
assert_eq!(result["editorSource"], "unit-test");
assert_eq!(result["writeIntentId"], "intent:test:page-body-write");
assert_eq!(result["saveOperationId"], "save:test:page-body-write");
assert_eq!(result["fileVersion"], result["conflict_detection_key"]);
let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
assert!(saved.contains("# Written"));
@@ -10614,10 +10800,12 @@ fn main() {}
assert_eq!(created["grant"]["ownerUserId"], "user_owner");
assert_eq!(created["grant"]["targetUserId"], "user_target");
assert_eq!(created["grant"]["permission"], "write");
assert!(created["grant"]["shareId"]
.as_str()
.unwrap_or_default()
.starts_with("folder_"));
assert!(
created["grant"]["shareId"]
.as_str()
.unwrap_or_default()
.starts_with("folder_")
);
let outside_error = create_user_share_grant(
Extension(request_context("user_owner", "user")),
@@ -10887,11 +11075,13 @@ fn main() {}
assert_eq!(stored.len(), 1);
assert_eq!(stored[0].id, link_id);
assert_ne!(stored[0].token_hash, "visible-token");
assert!(state
.control_plane()
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
.expect("resolve share link")
.is_some());
assert!(
state
.control_plane()
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
.expect("resolve share link")
.is_some()
);
let (_, Json(listed)) = get_share_links(
State(state.clone()),
@@ -10924,11 +11114,13 @@ fn main() {}
.expect("share revoked broadcast delta");
assert_eq!(revoked_delta["kind"], "control_plane_event");
assert_eq!(revoked_delta["eventType"], "control.share.revoked");
assert!(state
.control_plane()
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
.expect("resolve revoked share link")
.is_none());
assert!(
state
.control_plane()
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
.expect("resolve revoked share link")
.is_none()
);
assert_eq!(
state
.control_plane()
@@ -11132,11 +11324,13 @@ fn main() {}
payload["workspace"]["manifest"]["ownerId"],
"user@example.com"
);
assert!(payload["workspace"]["manifest"]["capabilities"]
.as_array()
.expect("capabilities")
.iter()
.any(|value| value.as_str() == Some("markdown_edit")));
assert!(
payload["workspace"]["manifest"]["capabilities"]
.as_array()
.expect("capabilities")
.iter()
.any(|value| value.as_str() == Some("markdown_edit"))
);
ensure_local_workspace_access_for_actor("user@example.com", "user", &root_uri)
.expect("owner can access managed workspace");
@@ -11387,11 +11581,12 @@ fn main() {}
execute_local_tree_command(&root_uri, "delete", "local-md:Renamed.md", None, None)
.expect("delete loose markdown");
assert!(!root.join("Renamed.md").exists());
assert!(root
.join(".mnote")
.join("trash")
.join("Renamed.md")
.is_file());
assert!(
root.join(".mnote")
.join("trash")
.join("Renamed.md")
.is_file()
);
assert_eq!(deleted["resourceKind"].as_str(), Some("markdown"));
let restored =
@@ -12441,6 +12636,17 @@ fn main() {}
std::fs::write(root.join("Page").join("Page.md"), "").expect("write md");
std::fs::write(root.join("Page").join("思维导图123456.json"), "{}").expect("write mindmap");
std::fs::write(root.join("Page").join("report.docx"), b"docx").expect("write docx");
std::fs::write(root.join("Page").join("sheet.xlsx"), b"xlsx").expect("write xlsx");
std::fs::write(root.join("Page").join("slides.pptx"), b"pptx").expect("write pptx");
std::fs::write(root.join("Page").join("main.rs"), b"fn main() {}").expect("write rs");
std::fs::write(
root.join("Page").join("app.tsx"),
b"export const App = () => null;",
)
.expect("write tsx");
std::fs::write(root.join("Page").join("package.json"), b"{}").expect("write package");
std::fs::write(root.join("Page").join(".env"), b"KEY=value").expect("write env");
std::fs::write(root.join("Page").join("config.yaml"), b"name: mnote").expect("write yaml");
let root_uri = format!("file://{}", root.display());
let snapshot =
@@ -12456,17 +12662,60 @@ fn main() {}
mindmap["resourceMeta"]["workspacePath"]["objectIdentity"]["objectKind"].as_str(),
Some("mindmap")
);
assert_eq!(
mindmap["resourceMeta"]["workspacePath"]["objectIdentity"]["assetId"].as_str(),
Some("local-file:Page/思维导图123456.json")
);
let office = items
.iter()
.find(|item| item["title"].as_str() == Some("report.docx"))
.expect("office row");
assert_eq!(office["rowKind"].as_str(), Some("asset"));
assert_eq!(office["iconHint"].as_str(), Some("office"));
assert_eq!(office["iconHint"].as_str(), Some("word"));
assert_eq!(
office["resourceMeta"]["workspacePath"]["objectIdentity"]["objectKind"].as_str(),
Some("only_office")
);
assert_eq!(
office["resourceMeta"]["workspacePath"]["objectIdentity"]["assetId"].as_str(),
Some("local-file:Page/report.docx")
);
let sheet = items
.iter()
.find(|item| item["title"].as_str() == Some("sheet.xlsx"))
.expect("sheet row");
assert_eq!(sheet["iconHint"].as_str(), Some("sheet"));
let ppt = items
.iter()
.find(|item| item["title"].as_str() == Some("slides.pptx"))
.expect("ppt row");
assert_eq!(ppt["iconHint"].as_str(), Some("ppt"));
let rust = items
.iter()
.find(|item| item["title"].as_str() == Some("main.rs"))
.expect("rust row");
assert_eq!(rust["iconHint"].as_str(), Some("code"));
let tsx = items
.iter()
.find(|item| item["title"].as_str() == Some("app.tsx"))
.expect("tsx row");
assert_eq!(tsx["iconHint"].as_str(), Some("web"));
let package_json = items
.iter()
.find(|item| item["title"].as_str() == Some("package.json"))
.expect("package row");
assert_eq!(package_json["iconHint"].as_str(), Some("config"));
let env = items
.iter()
.find(|item| item["title"].as_str() == Some(".env"))
.expect("env row");
assert_eq!(env["iconHint"].as_str(), Some("config"));
let yaml = items
.iter()
.find(|item| item["title"].as_str() == Some("config.yaml"))
.expect("yaml row");
assert_eq!(yaml["iconHint"].as_str(), Some("config"));
let _ = std::fs::remove_dir_all(&root);
}
@@ -12497,6 +12746,8 @@ fn main() {}
source_kind: core_protocol::WorkspaceSourceKind::LocalFolder,
root_uri: root_uri.clone(),
expected_file_version: None,
write_intent_id: Some("intent:test:buffer-save".into()),
save_operation_id: Some("save:test:buffer-save".into()),
base_content_hash: None,
content_format: "editorBlocks".into(),
content: serde_json::json!([
@@ -12875,6 +13126,30 @@ fn main() {}
markdown_asset["ownerDocumentId"],
"local-md:docs~2FREADME~2FREADME.md"
);
let windows_path_asset = write_local_markdown_asset(
&root_uri,
"local-md:docs~2FREADME~2FREADME.md",
"attachment",
LocalUploadFile {
name: "C:\\Users\\liaib\\Downloads\\1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx".to_string(),
content_type: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
.to_string(),
bytes: b"pptx".to_vec(),
},
)
.expect("upload windows path named pptx asset");
assert_eq!(
windows_path_asset["file_name"],
"1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
);
assert_eq!(
windows_path_asset["sourcePath"],
"1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
);
assert_eq!(
windows_path_asset["rootRelativePath"],
"docs/README/1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
);
let uploaded_asset_index =
std::fs::read_to_string(root.join(".mnote").join("uploaded-assets.json"))
.expect("uploaded asset index");