feat: advance local-first workspace checklist
- add admin access-policy UI and local access control surfaces - add local markdown conflict resolution UI and smoke coverage - add ACP local agent changed-files audit scaffold and read-only write guard - document current P0-P2 checklist progress and verification evidence
This commit is contained in:
@@ -170,6 +170,31 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
}));
|
||||
return Err(error);
|
||||
}
|
||||
if !dry_run && !is_read_tool(&input.tool_name) && is_shared_read_scope(&input) {
|
||||
let error = WebError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"mnote_tool_shared_read_write_forbidden",
|
||||
"共享只读 AI 上下文不能执行写工具",
|
||||
)
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.with_header(HEADER_HERMES_TOOL_OWNER, "mnote-web-hermes-tools");
|
||||
audit_push(json!({
|
||||
"phase": "failed",
|
||||
"traceId": trace_id,
|
||||
"sessionId": input.session_id,
|
||||
"runId": input.run_id,
|
||||
"toolCallId": tool_call_id,
|
||||
"toolName": input.tool_name,
|
||||
"workspaceId": workspace_id,
|
||||
"documentId": document_id,
|
||||
"actorId": input.actor_id,
|
||||
"status": error.status().as_u16(),
|
||||
"message": error.message(),
|
||||
"permissionLevel": "shared_read"
|
||||
}));
|
||||
return Err(error);
|
||||
}
|
||||
if let Some(cached) = idempotency_key.as_deref().and_then(idempotency_cache_get) {
|
||||
info!(
|
||||
trace_id = %trace_id,
|
||||
@@ -301,6 +326,27 @@ fn is_read_tool(tool_name: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn is_shared_read_scope(input: &ToolCallInput) -> bool {
|
||||
let direct = input
|
||||
.arg_string("permissionLevel")
|
||||
.or_else(|| input.arg_string("permission_level"));
|
||||
if matches!(direct.as_deref(), Some("shared_read")) {
|
||||
return true;
|
||||
}
|
||||
input
|
||||
.arg_value("aiAccessScope")
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("permissionLevel")
|
||||
.or_else(|| value.get("permission_level"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
.as_deref()
|
||||
== Some("shared_read")
|
||||
}
|
||||
|
||||
fn audit_log() -> &'static Mutex<Vec<Value>> {
|
||||
static LOG: OnceLock<Mutex<Vec<Value>>> = OnceLock::new();
|
||||
LOG.get_or_init(|| Mutex::new(Vec::new()))
|
||||
@@ -882,6 +928,47 @@ mod tests {
|
||||
.any(|rule| rule["required"] == json!(["full_content"])));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_manifest_marks_write_tools_as_compat_fallbacks() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/hermes/tools/mnote/manifest")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
let tools = payload["manifest"]["tools"].as_array().expect("tools");
|
||||
let markdown_edit = tools
|
||||
.iter()
|
||||
.find(|tool| tool["name"] == "mnote.doc.markdown_edit")
|
||||
.expect("markdown edit tool");
|
||||
let page_save = tools
|
||||
.iter()
|
||||
.find(|tool| tool["name"] == "mnote.page.save")
|
||||
.expect("page save tool");
|
||||
|
||||
assert!(markdown_edit["description"]
|
||||
.as_str()
|
||||
.expect("description")
|
||||
.contains("兼容"));
|
||||
assert!(markdown_edit["description"]
|
||||
.as_str()
|
||||
.expect("description")
|
||||
.contains("agent 原生 patch/diff"));
|
||||
assert_eq!(
|
||||
page_save["annotations"]["requiresWritePermission"],
|
||||
Value::Bool(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_manifest_describes_block_tools_selection_scope() {
|
||||
let response = app()
|
||||
@@ -1129,6 +1216,8 @@ mod tests {
|
||||
assert_eq!(payload["toolName"], "mnote.doc.fetch");
|
||||
assert_eq!(payload["audit"]["effect"], "read");
|
||||
assert_eq!(payload["result"]["revision"], json!(7));
|
||||
assert_eq!(payload["result"]["conflictDetectionKey"], json!("doc_1:7"));
|
||||
assert_eq!(payload["result"]["fileVersion"], json!("doc_1:7"));
|
||||
assert_eq!(
|
||||
payload["result"]["blocks"][0]["blockId"],
|
||||
json!("heading_1")
|
||||
@@ -1140,6 +1229,90 @@ mod tests {
|
||||
.starts_with("pageRev:7:block:heading_1:hash:"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_doc_fetch_rejects_out_of_scope_ai_resource() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.doc.fetch",
|
||||
"workspaceId": "ws_demo",
|
||||
"documentId": "doc_2",
|
||||
"sessionId": "sess_scope_read",
|
||||
"runId": "run_scope_read",
|
||||
"toolCallId": "call_scope_read",
|
||||
"traceId": "trace_scope_read",
|
||||
"args": {
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "shared_read",
|
||||
"allowedResourceIds": ["doc_1"],
|
||||
"shareContext": {"shareId": "share_read_1"}
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-error-code")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote_tool_ai_scope_read_forbidden")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_page_get_rejects_out_of_scope_ai_resource() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.page.get",
|
||||
"workspaceId": "ws_demo",
|
||||
"documentId": "doc_2",
|
||||
"sessionId": "sess_page_scope_read",
|
||||
"runId": "run_page_scope_read",
|
||||
"toolCallId": "call_page_scope_read",
|
||||
"traceId": "trace_page_scope_read",
|
||||
"args": {
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "shared_read",
|
||||
"allowedResourceIds": ["doc_1"],
|
||||
"shareContext": {"shareId": "share_read_1"}
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-error-code")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote_tool_ai_scope_read_forbidden")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_doc_fetch_supports_selection_and_page_xml() {
|
||||
let response = app()
|
||||
@@ -2116,6 +2289,70 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_page_save_local_folder_writes_markdown_file() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-page-save-local-folder-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join(".mnote")).expect("metadata");
|
||||
fs::write(
|
||||
root.join(".mnote").join("workspace.json"),
|
||||
r#"{"workspaceId":"local-ws-user-1","ownerId":"user_1","createdAt":"2026-05-18T00:00:00Z","capabilities":["local_files"]}"#,
|
||||
)
|
||||
.expect("manifest");
|
||||
fs::write(root.join("README.md"), "# Old\n\n旧正文\n").expect("markdown");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.page.save",
|
||||
"workspaceId": "local-ws-user-1",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"sessionId": "sess_page_save_local",
|
||||
"runId": "run_page_save_local",
|
||||
"toolCallId": "call_page_save_local",
|
||||
"traceId": "trace_page_save_local",
|
||||
"idempotencyKey": "idem_page_save_local",
|
||||
"dryRun": false,
|
||||
"args": {
|
||||
"content": [
|
||||
{"type": "paragraph", "content": [{"type": "text", "text": "本地 page.save 写入"}]}
|
||||
]
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
let status = response.status();
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let text = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert_eq!(status, StatusCode::OK, "{text}");
|
||||
let payload: Value = serde_json::from_str(&text).expect("json");
|
||||
assert_eq!(payload["result"]["source"], "local_folder");
|
||||
assert_eq!(payload["result"]["commandName"], "page.body.write");
|
||||
let saved = fs::read_to_string(root.join("README.md")).expect("read");
|
||||
assert!(saved.contains("本地 page.save 写入"), "{saved}");
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_page_save_dry_run_returns_diff_without_write() {
|
||||
let response = app()
|
||||
@@ -2264,6 +2501,74 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_markdown_edit_shared_read_is_forbidden() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-markdown-edit-shared-read-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join(".mnote")).expect("metadata");
|
||||
fs::write(
|
||||
root.join(".mnote").join("workspace.json"),
|
||||
r#"{"workspaceId":"local-ws-user-1","ownerId":"user_1","createdAt":"2026-05-18T00:00:00Z","capabilities":["local_files","ai_sessions"]}"#,
|
||||
)
|
||||
.expect("manifest");
|
||||
fs::write(root.join("README.md"), "原文\n").expect("write markdown");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.doc.markdown_edit",
|
||||
"workspaceId": "local-ws-user-1",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"actorId": "user_1",
|
||||
"sessionId": "sess_shared_read_md",
|
||||
"runId": "run_shared_read_md",
|
||||
"toolCallId": "call_shared_read_md",
|
||||
"traceId": "trace_shared_read_md",
|
||||
"idempotencyKey": "idem_shared_read_md",
|
||||
"dryRun": false,
|
||||
"args": {
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "shared_read",
|
||||
"shareContext": {"shareId": "share_read_1"}
|
||||
},
|
||||
"operations": [{"search": "原文", "replace": "不应写入"}]
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-error-code")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote_tool_shared_read_write_forbidden")
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("README.md")).expect("read"),
|
||||
"原文\n"
|
||||
);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_markdown_edit_reports_empty_block_mapping_before_apply() {
|
||||
// 7-27 修复后:即使 search 吃掉了块注释,也不会崩溃或泄露 apply_block_ops 错误。
|
||||
@@ -2406,6 +2711,165 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_markdown_edit_online_page_body_save_carries_revision_conflict_key() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.doc.markdown_edit",
|
||||
"workspaceId": "ws_demo",
|
||||
"documentId": "doc_1",
|
||||
"sessionId": "sess_markdown_precondition",
|
||||
"runId": "run_markdown_precondition",
|
||||
"toolCallId": "call_markdown_precondition",
|
||||
"traceId": "trace_markdown_precondition",
|
||||
"idempotencyKey": "idem_markdown_precondition",
|
||||
"dryRun": false,
|
||||
"args": {
|
||||
"operations": [{"search": "第二段", "replace": "测试123"}]
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
let command_payload =
|
||||
&payload["result"]["applyResult"]["artifacts"]["commandLog"]["payload"];
|
||||
assert_eq!(command_payload["revision"], 7);
|
||||
assert_eq!(command_payload["conflictDetectionKey"], "doc_1:7");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_markdown_edit_local_folder_writes_same_markdown_file() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-markdown-edit-local-folder-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join("README.assets")).expect("create asset dir");
|
||||
fs::write(
|
||||
root.join("README.md"),
|
||||
"---\ntitle: AI Local\n---\n# AI Local\n\n第一段\n\n\n",
|
||||
)
|
||||
.expect("write markdown");
|
||||
fs::write(root.join("README.assets").join("photo.png"), b"png").expect("write asset");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
crate::routes::local_folder_source::initialize_local_workspace_for_actor(
|
||||
"user_1", &root_uri,
|
||||
)
|
||||
.expect("initialize workspace");
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.doc.markdown_edit",
|
||||
"workspaceId": "local-ws-test",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"actorId": "user_1",
|
||||
"sessionId": "sess_local_folder_md",
|
||||
"runId": "run_local_folder_md",
|
||||
"toolCallId": "call_local_folder_md",
|
||||
"traceId": "trace_local_folder_md",
|
||||
"idempotencyKey": "idem_local_folder_md",
|
||||
"dryRun": false,
|
||||
"args": {
|
||||
"operations": [{"search": "第一段", "replace": "第一段已由 AI 修改"}]
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let text = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
if status != StatusCode::OK {
|
||||
panic!("status={status} headers={headers:?} body={text}");
|
||||
}
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["result"]["source"], "local_folder");
|
||||
assert_eq!(
|
||||
payload["result"]["applyResult"]["commandName"],
|
||||
"page.body.write"
|
||||
);
|
||||
let saved = fs::read_to_string(root.join("README.md")).expect("read markdown");
|
||||
assert!(saved.contains("第一段已由 AI 修改"));
|
||||
assert!(saved.contains("README.assets/photo.png"));
|
||||
assert!(!saved.contains("/api/media"));
|
||||
assert!(!saved.contains("assetId"));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_markdown_edit_rejects_no_applied_operations() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.doc.markdown_edit",
|
||||
"workspaceId": "ws_demo",
|
||||
"documentId": "doc_1",
|
||||
"sessionId": "sess_markdown_noop",
|
||||
"runId": "run_markdown_noop",
|
||||
"toolCallId": "call_markdown_noop",
|
||||
"traceId": "trace_markdown_noop",
|
||||
"idempotencyKey": "idem_markdown_noop",
|
||||
"dryRun": false,
|
||||
"args": {
|
||||
"operations": [{"search": "不存在的段落", "replace": "测试123"}]
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-error-code")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote_markdown_edit_no_operations_applied")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_markdown_edit_rejects_selection_out_of_scope() {
|
||||
let response = app()
|
||||
@@ -2576,4 +3040,67 @@ mod tests {
|
||||
assert_eq!(payload["result"]["dryRun"], true);
|
||||
assert_eq!(payload["result"]["artifactType"], "summary");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_artifact_summary_local_folder_writes_sidecar_file() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-artifact-local-folder-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join(".mnote")).expect("metadata");
|
||||
fs::write(
|
||||
root.join(".mnote").join("workspace.json"),
|
||||
r#"{"workspaceId":"local-ws-user-1","ownerId":"user_1","createdAt":"2026-05-18T00:00:00Z","capabilities":["local_files"]}"#,
|
||||
)
|
||||
.expect("manifest");
|
||||
fs::write(root.join("README.md"), "# Local\n").expect("markdown");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.artifact.create_summary",
|
||||
"workspaceId": "local-ws-user-1",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"sessionId": "sess_artifact_local",
|
||||
"runId": "run_artifact_local",
|
||||
"toolCallId": "call_artifact_local",
|
||||
"traceId": "trace_artifact_local",
|
||||
"idempotencyKey": "idem_artifact_local",
|
||||
"dryRun": false,
|
||||
"args": {"summary": "本地摘要"}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
let status = response.status();
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let text = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert_eq!(status, StatusCode::OK, "{text}");
|
||||
let payload: Value = serde_json::from_str(&text).expect("json");
|
||||
assert_eq!(payload["result"]["source"], "local_folder");
|
||||
let artifact_path = root
|
||||
.join(".mnote")
|
||||
.join("artifacts")
|
||||
.join("summary_local-md_README.md.json");
|
||||
let artifact = fs::read_to_string(&artifact_path).expect("artifact");
|
||||
assert!(artifact.contains("本地摘要"), "{artifact}");
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user