feat(rag): harden post-LightRAG runtime

Retire legacy OCR/media/evidence fallbacks, add local-folder event bus and Page Aggregate guards, and archive completed design checklists.

Validation: cargo test -p mnote-web -- --test-threads=1; cargo test --workspace -- --test-threads=1; git diff --check; codegraph sync .; codegraph_status.
This commit is contained in:
lix-2026
2026-06-07 10:35:21 +08:00
parent 22a92edcda
commit 9551d4c1dc
59 changed files with 4053 additions and 5249 deletions
@@ -10608,7 +10608,7 @@ mod tests {
}
#[tokio::test]
async fn page_ai_capabilities_expose_local_index_and_toggle_tools() {
async fn page_ai_capabilities_expose_knowledge_rag_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-{}",
@@ -13223,20 +13223,16 @@ mod tests {
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["ok"], true);
assert_eq!(payload["sessions"][0]["sessionId"], "sess_1");
assert_eq!(payload["persistence"], "convex_acp_runtime_store");
assert_eq!(payload["ok"], false);
assert_eq!(payload["code"], "convex_retired");
let query_body = captured_body.lock().expect("captured convex body").clone();
assert_eq!(query_body["path"], "aiSessions:listRuntimeRuns");
assert_eq!(query_body["args"]["userId"], "user_1");
assert_eq!(query_body["args"]["workspaceId"], "ws_1");
assert_eq!(query_body["args"]["documentId"], "doc_1");
assert_eq!(query_body, Value::Null);
}
#[tokio::test]
@@ -13317,28 +13313,16 @@ mod tests {
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["ok"], true);
assert_eq!(payload["persistence"], "convex_acp_runtime_store");
assert_eq!(payload["session"]["sessionId"], "sess_1");
assert_eq!(payload["session"]["runs"][0]["runId"], "run_1");
assert_eq!(payload["runtime"]["runId"], "run_1");
assert_eq!(payload["events"][0]["eventType"], "message.delta");
assert_eq!(
payload["session"]["messages"].as_array().map(Vec::len),
Some(0)
);
assert_eq!(payload["ok"], false);
assert_eq!(payload["code"], "convex_retired");
let bodies = captured_bodies.lock().expect("captured convex bodies");
assert_eq!(bodies[0]["path"], "aiSessions:listRuntimeRuns");
assert_eq!(bodies[0]["args"]["userId"], "user_1");
assert_eq!(bodies[0]["args"]["sessionId"], "sess_1");
assert_eq!(bodies[1]["path"], "aiSessions:listRuntimeEvents");
assert_eq!(bodies[1]["args"]["runId"], "run_1");
assert!(bodies.is_empty());
}
#[tokio::test]
@@ -13403,15 +13387,13 @@ mod tests {
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["ok"], true);
assert_eq!(payload["resumed"], true);
assert_eq!(payload["resumeSource"], "convex_acp_runtime_store");
assert_eq!(payload["session"]["runs"][0]["runId"], "run_1");
assert_eq!(payload["ok"], false);
assert_eq!(payload["code"], "convex_retired");
}
#[tokio::test]
@@ -13486,7 +13468,7 @@ mod tests {
}
#[tokio::test]
async fn hermes_client_acp_session_search_returns_convex_snippets() {
async fn hermes_client_acp_session_search_legacy_convex_returns_retired_guard() {
let captured_body = Arc::new(Mutex::new(Value::Null));
let captured_for_route = Arc::clone(&captured_body);
let mock = axum::Router::new().route(
@@ -13548,21 +13530,16 @@ mod tests {
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["ok"], true);
assert_eq!(payload["results"][0]["sessionId"], "sess_1");
assert_eq!(payload["results"][0]["snippet"], "帮我总结化学页面");
assert_eq!(payload["ok"], false);
assert_eq!(payload["code"], "convex_retired");
let query_body = captured_body.lock().expect("captured convex body").clone();
assert_eq!(query_body["path"], "aiSessions:searchRuntimeSessions");
assert_eq!(query_body["args"]["userId"], "user_1");
assert_eq!(query_body["args"]["workspaceId"], "ws_1");
assert_eq!(query_body["args"]["q"], "化学");
assert_eq!(query_body["args"]["limit"], 5);
assert_eq!(query_body, Value::Null);
}
#[tokio::test]
@@ -14520,13 +14497,40 @@ mod tests {
&"/api/hermes/client/runs".parse().expect("uri"),
&headers,
);
let root = std::env::temp_dir().join(format!(
"mnote-local-agent-run-receipt-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("create root");
std::fs::write(root.join("README.md"), "# Readme\nold\n").expect("write readme");
std::fs::write(root.join("Other.md"), "# Other\nold\n").expect("write other");
let root_uri = format!("file://{}", root.display());
let payload = json!({
"workspaceId": "local-workspace-1",
"documentId": "local-md:README.md",
"sessionId": "sess_local_1",
"rootUri": "file:///tmp/mnote-agent-run-receipt",
"actorId": "user_1"
"rootUri": root_uri,
"actorId": "user_1",
"contextRefs": [{
"kind": "folder",
"rootUri": root_uri,
"relativePath": ""
}],
"targetPackage": {
"schema": "mnote.agent_target_package.v1",
"allowedFiles": ["README.md"],
"currentFile": {
"relativePath": "README.md"
}
}
});
let before = local_agent_audit_collect_snapshot_for_payload(&payload, None)
.expect("before allowed-files snapshot");
std::fs::write(root.join("README.md"), "# Readme\nnew\n").expect("modify readme");
std::fs::write(root.join("Other.md"), "# Other\nnew\n").expect("modify other");
let after = local_agent_audit_collect_snapshot_for_payload(&payload, Some(&before))
.expect("after allowed-files snapshot");
let changed_files = json!([
{
"path": "README.md",
@@ -14541,8 +14545,8 @@ mod tests {
"reasonix",
"completed",
changed_files,
None,
None,
Some(&before),
Some(&after),
false,
);
let receipt = &event["agentRunReceipt"];
@@ -14553,6 +14557,11 @@ mod tests {
assert_eq!(receipt["status"], "completed");
assert_eq!(receipt["changedFiles"][0]["path"], "README.md");
assert_eq!(receipt["refresh"]["touchesCurrentFile"], true);
assert_eq!(event["auditScope"]["scope"], "allowed_files");
assert_eq!(event["auditScope"]["fileCount"], 1);
assert_eq!(receipt["auditScope"]["scope"], "allowed_files");
assert_eq!(receipt["auditScope"]["fileCount"], 1);
let _ = std::fs::remove_dir_all(root);
}
#[test]
@@ -15066,7 +15075,7 @@ mod tests {
"/api/hermes/client/profile-memory?profile=chemist",
None,
),
("GET", "/api/hermes/client/skills?profile=chemist", None),
("GET", "/api/hermes/client/skills", None),
(
"PUT",
"/api/hermes/client/profiles/active",
@@ -15107,7 +15116,7 @@ mod tests {
);
let request = Request::builder()
.method("GET")
.uri("/api/hermes/client/skills?profile=chemist")
.uri("/api/hermes/client/skills")
.header("x-mnote-actor-id", "user_1")
.body(Body::empty())
.expect("request");