#![cfg(feature = "desktop")] //! Real Python Core + Host pipes + isolated Workspace; no personal data or Provider. use notesagent_host::{core::CoreSupervisor, workspace::Workspace, workspace_broker}; use serde_json::{json, Value}; use std::{ path::Path, sync::{Arc, Mutex}, }; async fn request( core: &mut CoreSupervisor, method: &str, path: &str, vault: &str, operation: &str, body: Option, ) -> (u16, Value) { let session = core.request_session(path).unwrap(); let mut request = reqwest::Client::new() .request(method.parse::().unwrap(), session.url) .header("Authorization", session.authorization.as_str()) .header("X-Core-Generation", session.generation) .header("X-OpenNexus-Vault", vault) .header("X-Request-Id", operation); if let Some(body) = body { request = request.json(&body); } let response = request.send().await.unwrap(); let status = response.status().as_u16(); (status, response.json().await.unwrap()) } #[tokio::test] async fn real_core_notes_roundtrip_only_through_bound_host_and_confirm_commits() { let backend = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../backend") .canonicalize() .unwrap(); let python = backend.join(if cfg!(windows) { ".venv/Scripts/python.exe" } else { ".venv/bin/python" }); let temp = tempfile::tempdir().unwrap(); let root = temp.path().join("vault"); std::fs::create_dir(&root).unwrap(); let workspace = Arc::new(Mutex::new(Workspace::open(&root).unwrap())); let vault = workspace.lock().unwrap().vault_id.clone(); let handler = workspace.clone(); let mut core = CoreSupervisor::new( python, vec!["-m".into(), "app.sidecar".into()], backend, temp.path().join("core"), ) .with_broker(Arc::new(move |value| { workspace_broker::dispatch(&mut handler.lock().unwrap(), value) })); let operation = uuid::Uuid::new_v4().to_string(); let (status, created) = request( &mut core, "POST", "/api/notes", &vault, &operation, Some(json!({"title":"Core fixture","markdown":"# 中文\noriginal","tags":["fixture"]})), ) .await; assert_eq!(status, 200, "{created}"); let file_id = created["note_id"].as_str().unwrap(); let original = workspace.lock().unwrap().read("Core fixture.md").unwrap(); assert_eq!(original.entry.file_id, file_id); assert!(original.content.contains("original")); let (status, search) = request( &mut core, "POST", "/api/search", &vault, &uuid::Uuid::new_v4().to_string(), Some(json!({"query":"original","mode":"fts"})), ) .await; assert_eq!(status, 200, "{search}"); assert!(search.to_string().contains(file_id)); assert_eq!( workspace .lock() .unwrap() .operation(&operation) .unwrap() .unwrap()["state"], "committed" ); let (status, listed) = request( &mut core, "GET", "/api/notes", &vault, &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 200, "{listed}"); assert_eq!(listed["items"][0]["note_id"], file_id); let next = uuid::Uuid::new_v4().to_string(); let (status, updated) = request( &mut core, "PATCH", &format!("/api/notes/{file_id}"), &vault, &next, Some(json!({"markdown":"# New\nchanged","expected_content_hash":original.entry.hash})), ) .await; assert_eq!(status, 200, "{updated}"); let (status, conflict) = request( &mut core, "PATCH", &format!("/api/notes/{file_id}"), &vault, &uuid::Uuid::new_v4().to_string(), Some(json!({"markdown":"must-not-overwrite","expected_content_hash":original.entry.hash})), ) .await; assert_eq!(status, 409, "{conflict}"); let (status, moved) = request( &mut core, "POST", &format!("/api/notes/{file_id}/move"), &vault, &uuid::Uuid::new_v4().to_string(), Some(json!({"folder":"nested"})), ) .await; assert_eq!(status, 200, "{moved}"); assert_eq!(moved["note_id"], file_id); assert!(root.join("nested/Core fixture.md").is_file()); let (status, denied) = request( &mut core, "GET", &format!("/api/notes/{file_id}"), &uuid::Uuid::new_v4().to_string(), &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 409, "{denied}"); assert_eq!(denied["error"]["code"], "VAULT_PERMISSION_CHANGED"); let legacy_database = rusqlite::Connection::open( temp.path() .join("core/vault-state") .join(&vault) .join("core.sqlite3"), ) .unwrap(); legacy_database.execute("INSERT INTO tasks VALUES ('task_00000000000000000000000000000002','Legacy task','','todo',?1,NULL,'2026-09-08T00:00:00+00:00','2026-09-08T00:00:00+00:00')",[file_id]).unwrap(); let task_operation = uuid::Uuid::new_v4().to_string(); let task_body = json!({"title":"Host task","note_id":file_id}); let (status, task) = request( &mut core, "POST", "/api/tasks", &vault, &task_operation, Some(task_body.clone()), ) .await; assert_eq!(status, 200, "{task}"); let task_id = task["task_id"].as_str().unwrap(); for _ in 0..20 { let (status, replay) = request( &mut core, "POST", "/api/tasks", &vault, &task_operation, Some(task_body.clone()), ) .await; assert_eq!(status, 200, "{replay}"); assert_eq!(replay, task); } let (status, changed_request) = request( &mut core, "POST", "/api/tasks", &vault, &task_operation, Some(json!({"title":"changed","note_id":file_id})), ) .await; assert_eq!(status, 409, "{changed_request}"); let record_path = root.join(format!("opennexus-records/v1/tasks/{task_id}.json")); assert!(record_path.is_file()); let (status, task_updated) = request( &mut core, "PATCH", &format!("/api/tasks/{task_id}"), &vault, &uuid::Uuid::new_v4().to_string(), Some(json!({"status":"done"})), ) .await; assert_eq!(status, 200, "{task_updated}"); assert_eq!(task_updated["status"], "done"); let (status, tasks) = request( &mut core, "GET", "/api/tasks", &vault, &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 200, "{tasks}"); assert_eq!(tasks["items"].as_array().unwrap().len(), 2); let (status, task_denied) = request( &mut core, "GET", "/api/tasks", &uuid::Uuid::new_v4().to_string(), &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 409, "{task_denied}"); let deletion = uuid::Uuid::new_v4().to_string(); for _ in 0..2 { let (status, result) = request( &mut core, "DELETE", &format!("/api/tasks/{task_id}"), &vault, &deletion, None, ) .await; assert_eq!(status, 200, "{result}"); } assert!(!record_path.exists()); let source: String = legacy_database .query_row( "SELECT title FROM tasks WHERE task_id='task_00000000000000000000000000000002'", [], |r| r.get(0), ) .unwrap(); assert_eq!(source, "Legacy task"); legacy_database .execute("DELETE FROM index_meta WHERE key='tasks_host_owned_v1'", []) .unwrap(); let (status, after_migration) = request( &mut core, "GET", "/api/tasks", &vault, &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 200, "{after_migration}"); assert_eq!(after_migration["items"].as_array().unwrap().len(), 1); let (status, deleted) = request( &mut core, "DELETE", &format!("/api/notes/{file_id}"), &vault, &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 200, "{deleted}"); assert!(!root.join("nested/Core fixture.md").exists()); let (status, search) = request( &mut core, "POST", "/api/search", &vault, &uuid::Uuid::new_v4().to_string(), Some(json!({"query":"original","mode":"fts"})), ) .await; assert_eq!(status, 200, "{search}"); assert!(!search.to_string().contains(file_id)); assert_eq!(workspace.lock().unwrap().pending_count().unwrap(), 8); assert!(!temp .path() .join("core/unbound-vault/Core fixture.md") .exists()); // The actual Python HTTP handler must round-trip revision through Host pipes, // including repeat requests after a successful commit. let path = "/api/settings/persona"; let (status, empty) = request( &mut core, "GET", path, &vault, &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 200, "{empty}"); assert_eq!(empty["revision"], ""); let operation = uuid::Uuid::new_v4().to_string(); let mut body = empty.clone(); body["system_prompt"] = json!("Host persona fixture"); let (status, first) = request( &mut core, "PUT", path, &vault, &operation, Some(body.clone()), ) .await; assert_eq!(status, 200, "{first}"); assert_eq!(first["revision"].as_str().unwrap().len(), 64); for _ in 0..20 { let (status, replay) = request( &mut core, "PUT", path, &vault, &operation, Some(body.clone()), ) .await; assert_eq!(status, 200, "{replay}"); assert_eq!(replay, first); } let (status, stale) = request( &mut core, "PUT", path, &vault, &uuid::Uuid::new_v4().to_string(), Some(body.clone()), ) .await; assert_eq!(status, 409, "{stale}"); let (status, wrong_vault) = request( &mut core, "GET", path, &uuid::Uuid::new_v4().to_string(), &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 409, "{wrong_vault}"); let (status, loaded) = request( &mut core, "GET", path, &vault, &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 200, "{loaded}"); assert_eq!(loaded, first); let stored = workspace .lock() .unwrap() .record_get_kind("persona", "default") .unwrap() .unwrap(); assert_eq!(stored["hash"], first["revision"]); // User-created Skills are Vault records, use Host CAS/idempotency, and are // resolved by the actual Agent route without copying package paths or grants. let create_skill_operation = uuid::Uuid::new_v4(); let skill_body = json!({ "revision":"", "name":"Vault reviewer", "description":"portable", "prompt":"Answer with the exact phrase user-skill-active.", "tools":[], "permissions":[], "retrieval":{"top_k":10,"rerank":true,"citation":true}, "required_capabilities":["chat"] }); let (status, user_skill) = request( &mut core, "POST", "/api/user-skills", &vault, &create_skill_operation.to_string(), Some(skill_body.clone()), ) .await; assert_eq!(status, 201, "{user_skill}"); let skill_id = user_skill["skill_id"].as_str().unwrap(); assert_eq!( skill_id, format!("user_skill_{}", create_skill_operation.simple()) ); assert_eq!(user_skill["status"], "ready"); for _ in 0..20 { let (status, replay) = request( &mut core, "POST", "/api/user-skills", &vault, &create_skill_operation.to_string(), Some(skill_body.clone()), ) .await; assert_eq!(status, 201, "{replay}"); assert_eq!(replay, user_skill); } let mut changed = skill_body.clone(); changed["name"] = json!("Changed replay"); let (status, conflict) = request( &mut core, "POST", "/api/user-skills", &vault, &create_skill_operation.to_string(), Some(changed), ) .await; assert_eq!(status, 409, "{conflict}"); assert_eq!(conflict["error"]["code"], "USER_SKILL_OPERATION_CONFLICT"); let (status, listed) = request( &mut core, "GET", "/api/user-skills?limit=100&offset=0", &vault, &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 200, "{listed}"); assert_eq!(listed["items"][0]["skill_id"], skill_id); let (status, denied) = request( &mut core, "GET", "/api/user-skills?limit=100&offset=0", &uuid::Uuid::new_v4().to_string(), &uuid::Uuid::new_v4().to_string(), None, ) .await; assert_eq!(status, 409, "{denied}"); assert_eq!(denied["error"]["code"], "VAULT_PERMISSION_CHANGED"); let mut update_body = skill_body.clone(); update_body["revision"] = user_skill["revision"].clone(); update_body["name"] = json!("Updated reviewer"); let update_operation = uuid::Uuid::new_v4().to_string(); let (status, updated_skill) = request( &mut core, "PUT", &format!("/api/user-skills/{skill_id}"), &vault, &update_operation, Some(update_body.clone()), ) .await; assert_eq!(status, 200, "{updated_skill}"); assert_eq!(updated_skill["data"]["version"], 2); let (status, stale) = request( &mut core, "PUT", &format!("/api/user-skills/{skill_id}"), &vault, &uuid::Uuid::new_v4().to_string(), Some(update_body.clone()), ) .await; assert_eq!(status, 409, "{stale}"); assert_eq!(stale["error"]["code"], "USER_SKILL_REVISION_CONFLICT"); for _ in 0..2 { let (status, replay) = request( &mut core, "PUT", &format!("/api/user-skills/{skill_id}"), &vault, &update_operation, Some(update_body.clone()), ) .await; assert_eq!(status, 200, "{replay}"); assert_eq!(replay, updated_skill); } let (status, run) = request( &mut core, "POST", "/api/agent/runs", &vault, &uuid::Uuid::new_v4().to_string(), Some(json!({"input":"Confirm the Skill configuration","provider_id":"mock","model":"mock-1","skill_id":skill_id})), ) .await; assert_eq!(status, 202, "{run}"); assert_eq!(run["skill_id"], skill_id); let delete_operation = uuid::Uuid::new_v4().to_string(); let delete_path = format!( "/api/user-skills/{skill_id}?revision={}", updated_skill["revision"].as_str().unwrap() ); for _ in 0..2 { let (status, deleted) = request( &mut core, "DELETE", &delete_path, &vault, &delete_operation, None, ) .await; assert_eq!(status, 200, "{deleted}"); } assert!(!root .join(format!("opennexus-records/v1/user-skills/{skill_id}.json")) .exists()); assert_eq!(workspace.lock().unwrap().pending_count().unwrap(), 12); }