Improve local evidence search and AI capabilities
This commit is contained in:
@@ -1318,6 +1318,123 @@ pub async fn toggle_skill(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_capabilities(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<HashMap<String, String>>,
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
ensure_authenticated(&context)?;
|
||||
let runtime = query.get("runtime").map(String::as_str).unwrap_or("mnote");
|
||||
if runtime != "mnote" {
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"runtime": runtime,
|
||||
"categories": [],
|
||||
"archived": []
|
||||
})),
|
||||
));
|
||||
}
|
||||
let fallback_profile = active_profile_name().unwrap_or_else(|| "default".into());
|
||||
let profile = query
|
||||
.get("profile")
|
||||
.map(String::as_str)
|
||||
.unwrap_or(fallback_profile.as_str());
|
||||
let payload = mnote_capabilities_payload(
|
||||
&state,
|
||||
&context,
|
||||
query.get("agentId").map(String::as_str),
|
||||
profile,
|
||||
)?;
|
||||
Ok((StatusCode::OK, stamp_client_headers(), Json(payload)))
|
||||
}
|
||||
|
||||
pub async fn toggle_capability(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(payload): Json<Value>,
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
ensure_authenticated(&context)?;
|
||||
let capability_id = payload
|
||||
.get("id")
|
||||
.or_else(|| payload.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("hermes_client_bad_request", "缺少 capability id")
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.with_header(HEADER_HERMES_CLIENT_OWNER, "mnote-web-hermes-client")
|
||||
})?;
|
||||
let enabled = payload
|
||||
.get("enabled")
|
||||
.and_then(Value::as_bool)
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("hermes_client_bad_request", "缺少 enabled")
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.with_header(HEADER_HERMES_CLIENT_OWNER, "mnote-web-hermes-client")
|
||||
})?;
|
||||
let runtime = payload
|
||||
.get("runtime")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.unwrap_or("mnote");
|
||||
if runtime != "mnote" {
|
||||
return Err(WebError::bad_request_code(
|
||||
"hermes_client_capability_runtime_unsupported",
|
||||
"当前只支持 MNote 内置能力开关",
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let fallback_profile = active_profile_name().unwrap_or_else(|| "default".into());
|
||||
let profile = payload
|
||||
.get("profile")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(fallback_profile.as_str());
|
||||
let skill = crate::hermes_tools::skill::find_skill(capability_id, None).ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
"mnote_capability_not_found",
|
||||
"未知 MNote AI 能力",
|
||||
)
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let actor_id = page_ai_actor_id(&state, &context)?;
|
||||
ensure_page_ai_actor_user(&state, &actor_id, page_ai_actor_is_admin(&context))?;
|
||||
set_mnote_builtin_capability_enabled(&state, &actor_id, capability_id, enabled, &context)?;
|
||||
for tool_name in skill.tool_names {
|
||||
if mnote_capability_tool_toggleable(tool_name) {
|
||||
set_mnote_tool_enabled(profile, tool_name, enabled).map_err(|error| {
|
||||
WebError::bad_gateway_code(
|
||||
"hermes_client_capability_tool_toggle_failed",
|
||||
format!("更新 MNote 能力工具设置失败: {error}"),
|
||||
)
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.with_header(HEADER_HERMES_CLIENT_OWNER, "mnote-web-hermes-client")
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"runtime": "mnote",
|
||||
"id": capability_id,
|
||||
"enabled": enabled,
|
||||
"profile": profile,
|
||||
"configScope": "user_sqlite+profile_tool_policy"
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn toggle_tool(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(payload): Json<Value>,
|
||||
@@ -4068,6 +4185,217 @@ fn mnote_tool_entry(tool: &Value, disabled: &[String]) -> Option<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
fn mnote_capabilities_payload(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
agent_id: Option<&str>,
|
||||
profile: &str,
|
||||
) -> Result<Value, WebError> {
|
||||
let mut skills_payload = mnote_builtin_skills_payload(agent_id);
|
||||
stamp_mnote_builtin_skill_payload_policy(state, context, &mut skills_payload)?;
|
||||
let tools_by_name = mnote_tools_payload(profile)
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
let name = tool.get("name").and_then(Value::as_str)?.to_string();
|
||||
Some((name, tool))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let mut capability_categories: BTreeMap<String, Vec<Value>> = BTreeMap::new();
|
||||
for category in skills_payload
|
||||
.get("categories")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
let mut capabilities = Vec::new();
|
||||
for skill in category
|
||||
.get("skills")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
let Some(id) = skill.get("id").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
if id == "mnote-chat-only" {
|
||||
continue;
|
||||
}
|
||||
let tool_names = skill
|
||||
.get("toolNames")
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.filter(|name| !name.trim().is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let tools = tool_names
|
||||
.iter()
|
||||
.filter_map(|name| tools_by_name.get(name).cloned())
|
||||
.collect::<Vec<_>>();
|
||||
let disabled_tool_count = tools
|
||||
.iter()
|
||||
.filter(|tool| tool.get("enabled").and_then(Value::as_bool) == Some(false))
|
||||
.count();
|
||||
let enabled = skill
|
||||
.get("enabled")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true);
|
||||
let status = if !enabled {
|
||||
"disabled"
|
||||
} else if disabled_tool_count > 0 {
|
||||
"partial"
|
||||
} else {
|
||||
"available"
|
||||
};
|
||||
let capability_category = skill
|
||||
.get("category")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("mnote");
|
||||
capabilities.push(json!({
|
||||
"id": id,
|
||||
"name": id,
|
||||
"title": skill.get("title").cloned().unwrap_or_else(|| json!(id)),
|
||||
"description": skill.get("description").cloned().unwrap_or(Value::Null),
|
||||
"enabled": enabled,
|
||||
"toggleable": skill.get("toggleable").cloned().unwrap_or_else(|| json!(true)),
|
||||
"builtin": true,
|
||||
"configurable": true,
|
||||
"configScope": "user_sqlite+profile_tool_policy",
|
||||
"skillKind": "mnote_capability",
|
||||
"source": "mnote",
|
||||
"origin": "builtin",
|
||||
"category": capability_category,
|
||||
"categoryTitle": mnote_capability_category_title(capability_category),
|
||||
"capabilityId": id,
|
||||
"capabilityKind": "mnote_builtin",
|
||||
"uiKind": mnote_capability_ui_kind(capability_category),
|
||||
"skillId": id,
|
||||
"readOnly": skill.get("readOnly").cloned().unwrap_or(Value::Bool(false)),
|
||||
"agentIds": skill.get("agentIds").cloned().unwrap_or(Value::Null),
|
||||
"requiresContextRefs": skill.get("requiresContextRefs").cloned().unwrap_or(Value::Null),
|
||||
"toolNames": tool_names,
|
||||
"tools": tools,
|
||||
"toolCount": tools.len(),
|
||||
"disabledToolCount": disabled_tool_count,
|
||||
"status": status,
|
||||
}));
|
||||
}
|
||||
for capability in capabilities {
|
||||
let category_name = capability
|
||||
.get("category")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("mnote")
|
||||
.to_string();
|
||||
capability_categories
|
||||
.entry(category_name)
|
||||
.or_default()
|
||||
.push(capability);
|
||||
}
|
||||
}
|
||||
let categories = ordered_mnote_capability_categories(capability_categories)
|
||||
.into_iter()
|
||||
.map(|(name, capabilities)| {
|
||||
json!({
|
||||
"name": name,
|
||||
"title": mnote_capability_category_title(&name),
|
||||
"description": mnote_capability_category_description(&name),
|
||||
"capabilities": capabilities.clone(),
|
||||
"skills": capabilities
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"runtime": "mnote",
|
||||
"profile": profile,
|
||||
"categories": categories,
|
||||
"archived": []
|
||||
}))
|
||||
}
|
||||
|
||||
fn ordered_mnote_capability_categories(
|
||||
mut categories: BTreeMap<String, Vec<Value>>,
|
||||
) -> Vec<(String, Vec<Value>)> {
|
||||
let mut ordered = Vec::new();
|
||||
for name in ["mnote", "knowledge", "file", "resource", "office", "chat"] {
|
||||
if let Some(capabilities) = categories.remove(name) {
|
||||
ordered.push((name.to_string(), capabilities));
|
||||
}
|
||||
}
|
||||
ordered.extend(categories);
|
||||
ordered
|
||||
}
|
||||
|
||||
fn mnote_capability_category_title(category: &str) -> &'static str {
|
||||
match category {
|
||||
"knowledge" => "知识库与索引",
|
||||
"file" => "本地文件",
|
||||
"resource" => "资源编辑",
|
||||
"office" => "Office / ONLYOFFICE",
|
||||
"chat" => "聊天",
|
||||
_ => "MNote",
|
||||
}
|
||||
}
|
||||
|
||||
fn mnote_capability_category_description(category: &str) -> &'static str {
|
||||
match category {
|
||||
"knowledge" => "本地索引、证据检索和资料范围管理。",
|
||||
"file" => "授权目录内的本地 Markdown 文件读写。",
|
||||
"resource" => "MNote 资源型编辑器能力,例如思维导图。",
|
||||
"office" => "Office 摘要、建议和 ONLYOFFICE 实时编辑桥。",
|
||||
"chat" => "不读取文档上下文的普通对话能力。",
|
||||
_ => "MNote 页面上下文与基础能力。",
|
||||
}
|
||||
}
|
||||
|
||||
fn mnote_capability_ui_kind(category: &str) -> &'static str {
|
||||
if category == "chat" {
|
||||
"chat"
|
||||
} else {
|
||||
"ai_capability"
|
||||
}
|
||||
}
|
||||
|
||||
fn set_mnote_builtin_capability_enabled(
|
||||
state: &AppState,
|
||||
actor_id: &str,
|
||||
capability_id: &str,
|
||||
enabled: bool,
|
||||
context: &RequestContext,
|
||||
) -> Result<(), WebError> {
|
||||
let key = format!("ai.agent.mnote_builtin.skill.{capability_id}.enabled");
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user_ui_preference(control_plane::UpsertUserUiPreferenceInput {
|
||||
id: None,
|
||||
user_id: actor_id.to_string(),
|
||||
workspace_id: None,
|
||||
source_kind: None,
|
||||
scope_kind: "page_ai_capability".to_string(),
|
||||
scope_id: "mnote_builtin".to_string(),
|
||||
key,
|
||||
value_json: Value::Bool(enabled).to_string(),
|
||||
})
|
||||
.map(|_| ())
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("SQLite MNote AI 能力偏好写入失败: {error}"))
|
||||
.with_context(context)
|
||||
})
|
||||
}
|
||||
|
||||
fn mnote_capability_tool_toggleable(tool_name: &str) -> bool {
|
||||
!matches!(
|
||||
tool_name,
|
||||
"mnote.skill.read" | "mnote.context.snapshot" | "mnote.context.resolve_target"
|
||||
)
|
||||
}
|
||||
|
||||
fn extract_skill_description(markdown: &str) -> String {
|
||||
markdown
|
||||
.lines()
|
||||
@@ -4451,6 +4779,7 @@ fn mnote_builtin_skills_payload(agent_id: Option<&str>) -> Value {
|
||||
"skillKind": "mnote_builtin",
|
||||
"source": "mnote",
|
||||
"origin": "builtin",
|
||||
"category": skill.get("category").cloned().unwrap_or_else(|| json!("mnote")),
|
||||
"agentIds": skill.get("agentIds").cloned().unwrap_or(Value::Null),
|
||||
"toolNames": skill.get("toolNames").cloned().unwrap_or(Value::Null),
|
||||
"requiresContextRefs": skill.get("requiresContextRefs").cloned().unwrap_or(Value::Null)
|
||||
@@ -10144,6 +10473,223 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(&hermes_home);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn page_ai_capabilities_expose_local_index_and_toggle_tools() {
|
||||
let _env_guard = env_lock().lock().expect("env lock");
|
||||
let hermes_home = std::env::temp_dir().join(format!(
|
||||
"mnote-web-ai-capability-policy-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&hermes_home);
|
||||
fs::create_dir_all(&hermes_home).expect("hermes home");
|
||||
std::env::set_var("HERMES_HOME", &hermes_home);
|
||||
let app = build_app(test_state());
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/hermes/client/capabilities?runtime=mnote&agentId=reasonix&profile=chemist")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("capabilities");
|
||||
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("capabilities json");
|
||||
let all_capabilities = payload["categories"]
|
||||
.as_array()
|
||||
.expect("categories")
|
||||
.iter()
|
||||
.flat_map(|category| category["capabilities"].as_array().into_iter().flatten())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
all_capabilities
|
||||
.iter()
|
||||
.all(|capability| capability["id"] != "mnote-chat-only"),
|
||||
"纯聊天是 agent 模式,不应作为 MNote 公共能力展示"
|
||||
);
|
||||
let local_index = payload["categories"]
|
||||
.as_array()
|
||||
.expect("categories")
|
||||
.iter()
|
||||
.flat_map(|category| category["capabilities"].as_array().into_iter().flatten())
|
||||
.find(|capability| capability["id"] == "mnote-local-index")
|
||||
.expect("local index capability");
|
||||
assert_eq!(local_index["enabled"], true);
|
||||
assert_eq!(local_index["uiKind"], "ai_capability");
|
||||
assert!(local_index["tools"]
|
||||
.as_array()
|
||||
.expect("local index tools")
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.index.status"));
|
||||
assert!(local_index["tools"]
|
||||
.as_array()
|
||||
.expect("local index tools")
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.index.update_settings"));
|
||||
|
||||
let toggle_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("PUT")
|
||||
.uri("/api/hermes/client/capabilities/toggle")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"runtime": "mnote",
|
||||
"profile": "chemist",
|
||||
"id": "mnote-local-index",
|
||||
"enabled": false
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("toggle capability");
|
||||
assert_eq!(toggle_response.status(), StatusCode::OK);
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/hermes/client/capabilities?runtime=mnote&agentId=reasonix&profile=chemist")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("capabilities after toggle");
|
||||
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("capabilities json");
|
||||
let local_index = payload["categories"]
|
||||
.as_array()
|
||||
.expect("categories")
|
||||
.iter()
|
||||
.flat_map(|category| category["capabilities"].as_array().into_iter().flatten())
|
||||
.find(|capability| capability["id"] == "mnote-local-index")
|
||||
.expect("local index capability");
|
||||
assert_eq!(local_index["enabled"], false);
|
||||
assert_eq!(local_index["status"], "disabled");
|
||||
assert!(local_index["tools"]
|
||||
.as_array()
|
||||
.expect("local index tools")
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.index.status" && tool["enabled"] == false));
|
||||
|
||||
let tools_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/hermes/client/tools?scope=mnote&profile=chemist")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("tools after toggle");
|
||||
assert_eq!(tools_response.status(), StatusCode::OK);
|
||||
let body = to_bytes(tools_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("tools json");
|
||||
let tools = payload["tools"]
|
||||
.as_array()
|
||||
.expect("tools")
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
(
|
||||
tool["name"].as_str().unwrap_or_default().to_string(),
|
||||
tool.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
assert_eq!(tools["mnote.index.status"]["enabled"], false);
|
||||
assert_eq!(tools["mnote.index.update_settings"]["enabled"], false);
|
||||
|
||||
std::env::remove_var("HERMES_HOME");
|
||||
let _ = fs::remove_dir_all(&hermes_home);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn page_ai_capabilities_group_onlyoffice_live_bridge() {
|
||||
let _env_guard = env_lock().lock().expect("env lock");
|
||||
let hermes_home = std::env::temp_dir().join(format!(
|
||||
"mnote-web-ai-onlyoffice-capability-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&hermes_home);
|
||||
fs::create_dir_all(&hermes_home).expect("hermes home");
|
||||
std::env::set_var("HERMES_HOME", &hermes_home);
|
||||
|
||||
let response = build_app(test_state())
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/hermes/client/capabilities?runtime=mnote&agentId=reasonix&profile=chemist")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("capabilities");
|
||||
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("capabilities json");
|
||||
let office_category = payload["categories"]
|
||||
.as_array()
|
||||
.expect("categories")
|
||||
.iter()
|
||||
.find(|category| category["name"] == "office")
|
||||
.expect("office category");
|
||||
assert_eq!(office_category["title"], "Office / ONLYOFFICE");
|
||||
let onlyoffice = office_category["capabilities"]
|
||||
.as_array()
|
||||
.expect("office capabilities")
|
||||
.iter()
|
||||
.find(|capability| capability["id"] == "mnote-onlyoffice-live")
|
||||
.expect("onlyoffice capability");
|
||||
assert_eq!(onlyoffice["title"], "ONLYOFFICE 实时编辑");
|
||||
assert_eq!(onlyoffice["categoryTitle"], "Office / ONLYOFFICE");
|
||||
assert_eq!(onlyoffice["enabled"], true);
|
||||
assert!(onlyoffice["tools"]
|
||||
.as_array()
|
||||
.expect("onlyoffice tools")
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.onlyoffice.session.current"));
|
||||
assert!(onlyoffice["tools"]
|
||||
.as_array()
|
||||
.expect("onlyoffice tools")
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.onlyoffice.sheet.batch_set_values"));
|
||||
assert!(onlyoffice["tools"]
|
||||
.as_array()
|
||||
.expect("onlyoffice tools")
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.onlyoffice.presentation.add_shape"));
|
||||
|
||||
std::env::remove_var("HERMES_HOME");
|
||||
let _ = fs::remove_dir_all(&hermes_home);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasonix_memory_policy_defaults_off_and_reads_user_preference() {
|
||||
let state = test_state();
|
||||
|
||||
Reference in New Issue
Block a user