feat: add evidence search and stabilize pdf previews
- add document evidence parsing/search/open routes, Hermes tool wiring, local index settings/status, and the document-evidence skill plus design notes - fix PDF resource tabs by rendering PDFs inline with pdf.js canvases instead of iframe preview pages, release PDF documents on close, and document the fourth-PDF stall bug - keep PDF preview at 2x rendering while removing the previous lazy-load/placeholder direction, and make dev:hot bind loopback defaults externally reachable Verification: - node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js - node scripts/task-dev-hot-plan-test.js - cargo test -p mnote-web --manifest-path rust/Cargo.toml pdf_preview_page_does_not_render_visible_toolbar - cargo test -p mnote-web --manifest-path rust/Cargo.toml document_shell_returns_page_aggregate_snapshot - cargo build -p mnote-web --manifest-path rust/Cargo.toml - browser smoke: sequentially opened the four tea_seed_oil_cosmetic PDFs; fourth PDF rendered 15/15 canvases, iframeCount=0, browser errors=0
This commit is contained in:
@@ -1,18 +1,20 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::gateway::current_actor_id;
|
||||
use crate::routes::query_support::{
|
||||
execute_runtime_query_against_data, execute_runtime_query_via_legacy_cloud,
|
||||
resolve_effective_workspace_id,
|
||||
};
|
||||
use crate::routes::web_shell::load_sidebar_tree_html;
|
||||
use crate::routes::{local_folder_source, local_search_index};
|
||||
use crate::routes::{evidence, local_folder_source, local_search_index};
|
||||
use crate::ssr::pages::search::SearchPage;
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use bridge_runtime::RuntimeQueryEnvelopeWire;
|
||||
use core_protocol::{EvidenceSearchMode, EvidenceSearchResult};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
@@ -64,6 +66,18 @@ pub struct LocalSearchIndexRefreshRequest {
|
||||
pub root_uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocalSearchIndexSettingsRequest {
|
||||
pub workspace_id: Option<String>,
|
||||
pub root_uri: String,
|
||||
pub include_paths: Vec<String>,
|
||||
pub schedule_mode: Option<String>,
|
||||
pub schedule_time: Option<String>,
|
||||
pub schedule_date: Option<String>,
|
||||
pub run_on_change: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocalSearchIndexQuery {
|
||||
@@ -169,43 +183,71 @@ pub async fn documents(
|
||||
None
|
||||
};
|
||||
|
||||
let result = if body.source_kind.as_deref().map(str::trim) == Some("local_folder") {
|
||||
let root_uri = body
|
||||
.root_uri
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("local_search_root_required", "本地搜索缺少 rootUri")
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
local_search_index::query_local_search_index(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&normalized_query,
|
||||
page_id.as_deref(),
|
||||
body.limit.unwrap_or(30),
|
||||
filters.title_only.unwrap_or(false),
|
||||
filters.exact.unwrap_or(false),
|
||||
filters.include_ocr.unwrap_or(false),
|
||||
)?
|
||||
} else {
|
||||
load_search_results_with_filters(
|
||||
state.config(),
|
||||
&context,
|
||||
&effective_workspace_id,
|
||||
&normalized_query,
|
||||
page_id,
|
||||
body.limit.unwrap_or(30),
|
||||
filters,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
let (result, evidence_results) =
|
||||
if body.source_kind.as_deref().map(str::trim) == Some("local_folder") {
|
||||
let root_uri = body
|
||||
.root_uri
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("local_search_root_required", "本地搜索缺少 rootUri")
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let user_settings = resolve_local_index_user_settings(
|
||||
&state,
|
||||
&context,
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let effective_settings = local_search_index::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let result = local_search_index::query_local_search_index_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&effective_settings,
|
||||
&user_settings,
|
||||
&normalized_query,
|
||||
page_id.as_deref(),
|
||||
body.limit.unwrap_or(30),
|
||||
filters.title_only.unwrap_or(false),
|
||||
filters.exact.unwrap_or(false),
|
||||
filters.include_ocr.unwrap_or(false),
|
||||
)?;
|
||||
let evidence_results = evidence::evidence_results_from_local_search(
|
||||
&result,
|
||||
&root_path,
|
||||
root_uri,
|
||||
EvidenceSearchMode::Hybrid,
|
||||
&normalized_query,
|
||||
);
|
||||
(result, evidence_results)
|
||||
} else {
|
||||
let result = load_search_results_with_filters(
|
||||
state.config(),
|
||||
&context,
|
||||
&effective_workspace_id,
|
||||
&normalized_query,
|
||||
page_id,
|
||||
body.limit.unwrap_or(30),
|
||||
filters,
|
||||
)
|
||||
.await?;
|
||||
(result, Vec::new())
|
||||
};
|
||||
let results = result
|
||||
.get("results")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Array(vec![]));
|
||||
let results = attach_evidence_to_search_results(results, &evidence_results);
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
stamp_search_headers(&mut headers);
|
||||
@@ -213,7 +255,8 @@ pub async fn documents(
|
||||
StatusCode::OK,
|
||||
headers,
|
||||
Json(json!({
|
||||
"results": result.get("results").cloned().unwrap_or(Value::Array(vec![])),
|
||||
"results": results,
|
||||
"evidence": evidence_results,
|
||||
"projectionOwner": result.get("projectionOwner").cloned().unwrap_or_else(|| json!("rust-kernel")),
|
||||
"recent": result.get("recentChanges").cloned().unwrap_or_else(|| Value::Array(vec![])),
|
||||
"meta": {
|
||||
@@ -249,10 +292,16 @@ pub async fn refresh_local_index(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let refreshed = local_search_index::refresh_local_search_index(
|
||||
let effective_settings = local_search_index::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let refreshed = local_search_index::refresh_local_search_index_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&effective_settings,
|
||||
)?;
|
||||
let mut headers = HeaderMap::new();
|
||||
stamp_search_headers(&mut headers);
|
||||
@@ -273,6 +322,180 @@ pub async fn refresh_local_index(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn local_index_status(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<LocalSearchIndexQuery>,
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
let effective_workspace_id =
|
||||
resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)?
|
||||
.expect("workspace_required 已确保存在");
|
||||
let root_uri = query.root_uri.trim();
|
||||
if root_uri.is_empty() {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_search_root_required",
|
||||
"本地索引状态缺少 rootUri",
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let user_settings =
|
||||
resolve_local_index_user_settings(&state, &context, &effective_workspace_id, &root_path)?;
|
||||
let effective_settings = local_search_index::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let status = local_search_index::local_index_status_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&user_settings,
|
||||
&effective_settings,
|
||||
)?;
|
||||
let mut headers = HeaderMap::new();
|
||||
stamp_search_headers(&mut headers);
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
headers,
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"result": status,
|
||||
"meta": {
|
||||
"owner": "mnote-web",
|
||||
"projectionOwner": "rust-kernel",
|
||||
"queryName": "search.local_index.status",
|
||||
"requestId": context.trace.request_id,
|
||||
"traceId": context.trace.trace_id,
|
||||
}
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn update_local_index_settings(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(body): Json<LocalSearchIndexSettingsRequest>,
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
let effective_workspace_id =
|
||||
resolve_effective_workspace_id(&context, body.workspace_id.as_deref(), true)?
|
||||
.expect("workspace_required 已确保存在");
|
||||
let root_uri = body.root_uri.trim();
|
||||
if root_uri.is_empty() {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_search_root_required",
|
||||
"本地索引设置缺少 rootUri",
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let root_path = local_folder_source::ensure_local_workspace_write_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let actor_id = current_actor_id(&state, &context).ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"local_index_settings_auth_required",
|
||||
"本地索引设置需要登录用户",
|
||||
)
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let settings = local_search_index::write_user_local_index_settings(
|
||||
state.control_plane(),
|
||||
&actor_id,
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
&body.include_paths,
|
||||
body.schedule_mode.as_deref(),
|
||||
body.schedule_time.as_deref(),
|
||||
body.schedule_date.as_deref(),
|
||||
body.run_on_change,
|
||||
)?;
|
||||
let effective_settings = local_search_index::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let status = local_search_index::local_index_status_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&settings,
|
||||
&effective_settings,
|
||||
)?;
|
||||
let mut headers = HeaderMap::new();
|
||||
stamp_search_headers(&mut headers);
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
headers,
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"settings": settings,
|
||||
"result": status,
|
||||
"meta": {
|
||||
"owner": "mnote-web",
|
||||
"projectionOwner": "rust-kernel",
|
||||
"queryName": "search.local_index.settings.update",
|
||||
"requestId": context.trace.request_id,
|
||||
"traceId": context.trace.trace_id,
|
||||
}
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
||||
fn attach_evidence_to_search_results(
|
||||
results: Value,
|
||||
evidence_results: &[EvidenceSearchResult],
|
||||
) -> Value {
|
||||
let Value::Array(items) = results else {
|
||||
return results;
|
||||
};
|
||||
Value::Array(
|
||||
items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| {
|
||||
let Some(evidence) = evidence_results.get(index) else {
|
||||
return item;
|
||||
};
|
||||
let mut item = item;
|
||||
if let Some(map) = item.as_object_mut() {
|
||||
let evidence_value = serde_json::to_value(evidence).unwrap_or(Value::Null);
|
||||
map.insert("evidence".into(), evidence_value);
|
||||
let source = map.entry("source").or_insert_with(|| json!({}));
|
||||
if let Some(source_map) = source.as_object_mut() {
|
||||
source_map.insert(
|
||||
"locator".into(),
|
||||
serde_json::to_value(&evidence.source).unwrap_or(Value::Null),
|
||||
);
|
||||
}
|
||||
}
|
||||
item
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_local_index_user_settings(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
workspace_id: &str,
|
||||
root_path: &std::path::Path,
|
||||
) -> Result<local_search_index::LocalIndexSettings, WebError> {
|
||||
if let Some(actor_id) = current_actor_id(state, context) {
|
||||
return local_search_index::read_user_local_index_settings(
|
||||
state.control_plane(),
|
||||
&actor_id,
|
||||
workspace_id,
|
||||
root_path,
|
||||
);
|
||||
}
|
||||
local_search_index::read_local_index_settings_or_default(root_path)
|
||||
}
|
||||
|
||||
pub async fn local_index_backlinks(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
@@ -305,10 +528,19 @@ pub async fn local_index_backlinks(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let backlinks = local_search_index::query_local_backlinks(
|
||||
let user_settings =
|
||||
resolve_local_index_user_settings(&state, &context, &effective_workspace_id, &root_path)?;
|
||||
let effective_settings = local_search_index::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let backlinks = local_search_index::query_local_backlinks_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&effective_settings,
|
||||
&user_settings,
|
||||
document_id,
|
||||
)?;
|
||||
let mut headers = HeaderMap::new();
|
||||
@@ -350,7 +582,20 @@ pub async fn local_index_tags(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let tags = local_search_index::query_local_tags(&root_path, root_uri, &effective_workspace_id)?;
|
||||
let user_settings =
|
||||
resolve_local_index_user_settings(&state, &context, &effective_workspace_id, &root_path)?;
|
||||
let effective_settings = local_search_index::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let tags = local_search_index::query_local_tags_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&effective_settings,
|
||||
&user_settings,
|
||||
)?;
|
||||
let mut headers = HeaderMap::new();
|
||||
stamp_search_headers(&mut headers);
|
||||
Ok((
|
||||
@@ -550,6 +795,7 @@ mod tests {
|
||||
use crate::app::{build_app, AppConfig, AppState};
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput};
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use tower::util::ServiceExt;
|
||||
@@ -789,6 +1035,15 @@ mod tests {
|
||||
.expect("home result");
|
||||
assert_eq!(home["sourceKind"].as_str(), Some("local_folder"));
|
||||
assert_eq!(home["resourceType"].as_str(), Some("markdown"));
|
||||
assert_eq!(
|
||||
home["source"]["locator"]["schema"].as_str(),
|
||||
Some("mnote.evidence_locator.v1")
|
||||
);
|
||||
assert_eq!(
|
||||
home["evidence"]["source"]["ownerDocumentPath"].as_str(),
|
||||
Some("README.md")
|
||||
);
|
||||
assert!(!payload["evidence"].as_array().expect("evidence").is_empty());
|
||||
assert!(home["tags"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
@@ -809,6 +1064,35 @@ mod tests {
|
||||
.join("index")
|
||||
.join("search-index.json")
|
||||
.exists());
|
||||
let evidence_db = root.join(".mnote").join("index").join("evidence.sqlite");
|
||||
assert!(evidence_db.exists(), "evidence sqlite should be built");
|
||||
let connection = rusqlite::Connection::open(&evidence_db).expect("open evidence sqlite");
|
||||
let resource_count: i64 = connection
|
||||
.query_row("SELECT COUNT(*) FROM evidence_resource", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.expect("resource count");
|
||||
let block_count: i64 = connection
|
||||
.query_row("SELECT COUNT(*) FROM evidence_block", [], |row| row.get(0))
|
||||
.expect("block count");
|
||||
let fts_count: i64 = connection
|
||||
.query_row("SELECT COUNT(*) FROM evidence_fts", [], |row| row.get(0))
|
||||
.expect("fts count");
|
||||
assert!(resource_count >= 1);
|
||||
assert!(block_count >= 1);
|
||||
assert!(fts_count >= 1);
|
||||
let locator_json: String = connection
|
||||
.query_row(
|
||||
"SELECT locator_json FROM evidence_block LIMIT 1",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("locator json");
|
||||
let locator: Value = serde_json::from_str(&locator_json).expect("locator");
|
||||
assert_eq!(
|
||||
locator["schema"].as_str(),
|
||||
Some("mnote.evidence_locator.v1")
|
||||
);
|
||||
assert!(payload["recent"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
@@ -955,4 +1239,249 @@ mod tests {
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_index_settings_route_keeps_user_settings_and_shared_effective_scope() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-search-settings-route-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join(".mnote")).expect("metadata");
|
||||
fs::create_dir_all(root.join("docs").join("alice")).expect("alice dir");
|
||||
fs::create_dir_all(root.join("docs").join("bob")).expect("bob dir");
|
||||
fs::write(
|
||||
root.join(".mnote").join("workspace.json"),
|
||||
r#"{"workspaceId":"local-ws-settings","ownerId":"alice","createdAt":"2026-05-19T00:00:00Z","capabilities":["local_files"]}"#,
|
||||
)
|
||||
.expect("manifest");
|
||||
fs::write(
|
||||
root.join("docs").join("alice").join("keep.md"),
|
||||
"# Alice\nAliceRouteToken\n",
|
||||
)
|
||||
.expect("alice doc");
|
||||
fs::write(
|
||||
root.join("docs").join("bob").join("keep.md"),
|
||||
"# Bob\nBobRouteToken\n",
|
||||
)
|
||||
.expect("bob doc");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
let encoded_root = query_escape(&root_uri);
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: false,
|
||||
enable_editor_actor: true,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
});
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("alice".into()),
|
||||
email: None,
|
||||
username: "alice".into(),
|
||||
display_name: "alice".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert alice");
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("bob".into()),
|
||||
email: None,
|
||||
username: "bob".into(),
|
||||
display_name: "bob".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert bob");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "bob".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root.display().to_string(),
|
||||
permission: "write".into(),
|
||||
recursive: true,
|
||||
capabilities: vec!["ai".into()],
|
||||
source: "test".into(),
|
||||
created_by: Some("alice".into()),
|
||||
})
|
||||
.expect("grant bob local folder access");
|
||||
let app = build_app(state);
|
||||
|
||||
let alice_settings_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("PUT")
|
||||
.uri("/api/search/local-index/settings")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "alice")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"workspaceId": "local-ws-settings",
|
||||
"rootUri": root_uri,
|
||||
"includePaths": ["docs/alice"],
|
||||
"scheduleMode": "manual",
|
||||
"scheduleTime": "02:00",
|
||||
"runOnChange": false
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("alice settings request"),
|
||||
)
|
||||
.await
|
||||
.expect("alice settings response");
|
||||
assert_eq!(alice_settings_response.status(), StatusCode::OK);
|
||||
|
||||
let bob_settings_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("PUT")
|
||||
.uri("/api/search/local-index/settings")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "bob")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"workspaceId": "local-ws-settings",
|
||||
"rootUri": root_uri,
|
||||
"includePaths": ["docs/bob"],
|
||||
"scheduleMode": "manual",
|
||||
"scheduleTime": "02:00",
|
||||
"runOnChange": true
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("bob settings request"),
|
||||
)
|
||||
.await
|
||||
.expect("bob settings response");
|
||||
assert_eq!(bob_settings_response.status(), StatusCode::OK);
|
||||
|
||||
let alice_status_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!(
|
||||
"/api/search/local-index/status?workspaceId=local-ws-settings&rootUri={encoded_root}"
|
||||
))
|
||||
.header("x-mnote-actor-id", "alice")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("alice status request"),
|
||||
)
|
||||
.await
|
||||
.expect("alice status response");
|
||||
assert_eq!(alice_status_response.status(), StatusCode::OK);
|
||||
let alice_status_body = to_bytes(alice_status_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("alice status body");
|
||||
let alice_status_payload: Value =
|
||||
serde_json::from_slice(&alice_status_body).expect("alice status json");
|
||||
assert_eq!(
|
||||
alice_status_payload["result"]["settings"]["includePaths"]
|
||||
.as_array()
|
||||
.map(Vec::len),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
alice_status_payload["result"]["effectiveSettings"]["includePaths"]
|
||||
.as_array()
|
||||
.map(Vec::len),
|
||||
Some(2)
|
||||
);
|
||||
assert_eq!(
|
||||
alice_status_payload["result"]["effectiveSettings"]["runOnChange"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
|
||||
let alice_search_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/search/documents")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "alice")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"workspaceId": "local-ws-settings",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"query": "BobRouteToken",
|
||||
"limit": 10
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("alice search request"),
|
||||
)
|
||||
.await
|
||||
.expect("alice search response");
|
||||
assert_eq!(alice_search_response.status(), StatusCode::OK);
|
||||
let alice_search_body = to_bytes(alice_search_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("alice search body");
|
||||
let alice_search_payload: Value =
|
||||
serde_json::from_slice(&alice_search_body).expect("alice search json");
|
||||
assert_eq!(
|
||||
alice_search_payload["results"].as_array().map(Vec::len),
|
||||
Some(0)
|
||||
);
|
||||
|
||||
let bob_search_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/search/documents")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "bob")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"workspaceId": "local-ws-settings",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"query": "BobRouteToken",
|
||||
"limit": 10
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("bob search request"),
|
||||
)
|
||||
.await
|
||||
.expect("bob search response");
|
||||
assert_eq!(bob_search_response.status(), StatusCode::OK);
|
||||
let bob_search_body = to_bytes(bob_search_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("bob search body");
|
||||
let bob_search_payload: Value =
|
||||
serde_json::from_slice(&bob_search_body).expect("bob search json");
|
||||
assert_eq!(
|
||||
bob_search_payload["results"].as_array().map(Vec::len),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user