//! Round16 raw integration coverage for tools, agent delegation, credentials, app state, and config. //! //! These tests stay on loopback services and temp workspaces. They exercise //! public Rust surfaces only, so they cover the same paths used by the core RPC //! and agent runtime without launching a real browser, hitting the network, or //! touching the OS keychain. use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, OnceLock}; use anyhow::Result; use async_trait::async_trait; use axum::extract::State; use axum::response::IntoResponse; use axum::routing::post; use axum::{Json, Router}; use chrono::{Duration as ChronoDuration, Utc}; use openhuman_core::openhuman::agent::dispatcher::NativeToolDispatcher; use openhuman_core::openhuman::agent::harness::session::Agent; use openhuman_core::openhuman::agent::harness::{ run_subagent, with_parent_context, AgentDefinition, ParentExecutionContext, PromptSource, SandboxMode, SubagentRunOptions, ToolScope, }; use openhuman_core::openhuman::app_state::{ snapshot, update_local_state, StoredAppStatePatch, StoredOnboardingTasks, }; use openhuman_core::openhuman::config::rpc as config_rpc; use openhuman_core::openhuman::config::{ BrowserConfig, Config, HttpRequestConfig, McpAuthConfig, McpServerConfig, }; use openhuman_core::openhuman::context::prompt::ToolCallFormat; use openhuman_core::openhuman::credentials::profiles::{ AuthProfile, AuthProfileKind, AuthProfilesStore, TokenSet, }; use openhuman_core::openhuman::credentials::{ AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME, }; use openhuman_core::openhuman::inference::provider::traits::ProviderCapabilities; use openhuman_core::openhuman::inference::provider::{ ChatMessage, ChatRequest, ChatResponse, Provider, ToolCall, UsageInfo, }; use openhuman_core::openhuman::memory::{Memory, MemoryCategory, MemoryEntry, NamespaceSummary}; use openhuman_core::openhuman::security::{AuditLogger, SecurityPolicy}; use openhuman_core::openhuman::tokenjuice::AgentTokenjuiceCompression; use openhuman_core::openhuman::tools::{ all_tools, BrowserTool, ComputerUseConfig, SpawnSubagentTool, Tool, ToolResult, }; use parking_lot::Mutex as ParkingMutex; use serde_json::{json, Value}; use tempfile::{Builder, TempDir}; static ROUND16_ENV_LOCK: OnceLock> = OnceLock::new(); struct EnvGuard { key: &'static str, old: Option, } impl EnvGuard { fn set(key: &'static str, value: &str) -> Self { let old = std::env::var(key).ok(); std::env::set_var(key, value); Self { key, old } } fn set_to_path(key: &'static str, path: &Path) -> Self { let old = std::env::var(key).ok(); std::env::set_var(key, path.as_os_str()); Self { key, old } } fn unset(key: &'static str) -> Self { let old = std::env::var(key).ok(); std::env::remove_var(key); Self { key, old } } } impl Drop for EnvGuard { fn drop(&mut self) { match &self.old { Some(value) => std::env::set_var(self.key, value), None => std::env::remove_var(self.key), } } } struct Harness { _tmp: TempDir, root: PathBuf, workspace: PathBuf, _guards: Vec, } impl Harness { async fn config(&self) -> Config { config_rpc::load_config_with_timeout() .await .expect("isolated config should load") } fn app_state_file(&self) -> PathBuf { self.workspace.join("state/app-state.json") } } struct ScriptedProvider { responses: ParkingMutex>, requests: ParkingMutex>>, } impl ScriptedProvider { fn new(responses: Vec) -> Self { Self { responses: ParkingMutex::new(responses), requests: ParkingMutex::new(Vec::new()), } } fn requests(&self) -> Vec> { self.requests.lock().clone() } } #[async_trait] impl Provider for ScriptedProvider { fn capabilities(&self) -> ProviderCapabilities { ProviderCapabilities { native_tool_calling: true, vision: false, } } async fn chat_with_system( &self, _system_prompt: Option<&str>, message: &str, _model: &str, _temperature: f64, ) -> Result { Ok(format!("extract:{message}")) } async fn chat( &self, request: ChatRequest<'_>, _model: &str, _temperature: f64, ) -> Result { self.requests.lock().push(request.messages.to_vec()); Ok(self.responses.lock().remove(0)) } } struct StubMemory; #[async_trait] impl Memory for StubMemory { async fn store( &self, _namespace: &str, _key: &str, _content: &str, _category: MemoryCategory, _session_id: Option<&str>, ) -> Result<()> { Ok(()) } async fn recall( &self, _query: &str, _limit: usize, _opts: openhuman_core::openhuman::memory::RecallOpts<'_>, ) -> Result> { Ok(Vec::new()) } async fn get(&self, _namespace: &str, _key: &str) -> Result> { Ok(None) } async fn list( &self, _namespace: Option<&str>, _category: Option<&MemoryCategory>, _session_id: Option<&str>, ) -> Result> { Ok(Vec::new()) } async fn forget(&self, _namespace: &str, _key: &str) -> Result { Ok(false) } async fn namespace_summaries(&self) -> Result> { Ok(Vec::new()) } async fn count(&self) -> Result { Ok(0) } async fn health_check(&self) -> bool { true } fn name(&self) -> &str { "round16-memory" } } struct EchoTool; #[async_trait] impl Tool for EchoTool { fn name(&self) -> &str { "echo" } fn description(&self) -> &str { "Echo deterministic test content" } fn parameters_schema(&self) -> Value { json!({ "type": "object", "properties": { "message": { "type": "string" } } }) } async fn execute(&self, args: Value) -> Result { Ok(ToolResult::success(format!( "echo:{}", args.get("message") .and_then(Value::as_str) .unwrap_or("missing") ))) } } #[derive(Clone, Default)] struct SidecarState { requests: Arc>>, } fn env_lock() -> std::sync::MutexGuard<'static, ()> { ROUND16_ENV_LOCK .get_or_init(|| Mutex::new(())) .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) } fn tempdir() -> TempDir { std::fs::create_dir_all("target").expect("create target"); Builder::new() .prefix("tools-agent-credentials-state-round16-") .tempdir_in("target") .expect("round16 tempdir") } fn write_min_config(root: &Path, api_url: &str) { std::fs::create_dir_all(root).expect("create openhuman root"); let cfg = format!( r#"api_url = "{api_url}" default_model = "round16-coverage-model" default_temperature = 0.0 onboarding_completed = true chat_onboarding_completed = true [observability] analytics_enabled = false [secrets] encrypt = false [meet] auto_orchestrator_handoff = true [local_ai] enabled = false runtime_enabled = false opt_in_confirmed = false [memory] provider = "none" embedding_provider = "none" embedding_model = "none" embedding_dimensions = 0 auto_save = false [memory_tree] embedding_strict = false "# ); std::fs::write(root.join("config.toml"), &cfg).expect("write config.toml"); let _: Config = toml::from_str(&cfg).expect("round16 config must match schema"); } fn setup(api_url: &str) -> Harness { let tmp = tempdir(); let root = tmp.path().join("openhuman"); write_min_config(&root, api_url); let workspace = root.join("workspace"); std::fs::create_dir_all(&workspace).expect("workspace dir"); let guards = vec![ EnvGuard::set_to_path("OPENHUMAN_WORKSPACE", &root), EnvGuard::set_to_path("HOME", tmp.path()), EnvGuard::unset("BACKEND_URL"), EnvGuard::unset("VITE_BACKEND_URL"), EnvGuard::unset("OPENHUMAN_API_URL"), EnvGuard::unset("OPENHUMAN_CORE_RPC_URL"), EnvGuard::unset("OPENHUMAN_CORE_PORT"), EnvGuard::set("OPENHUMAN_KEYRING_BACKEND", "file"), EnvGuard::set("OPENHUMAN_MEMORY_EMBED_STRICT", "false"), EnvGuard::set("OPENHUMAN_MEMORY_EMBED_ENDPOINT", ""), EnvGuard::set("OPENHUMAN_MEMORY_EMBED_MODEL", ""), EnvGuard::unset("OPENHUMAN_BROWSER_ALLOW_ALL"), EnvGuard::unset("OPENHUMAN_LSP_ENABLED"), ]; Harness { _tmp: tmp, root, workspace, _guards: guards, } } fn usage(input_tokens: u64, output_tokens: u64) -> UsageInfo { UsageInfo { input_tokens, output_tokens, context_window: 8_192, cached_input_tokens: input_tokens / 2, cache_creation_tokens: 0, reasoning_tokens: 0, charged_amount_usd: 0.001, } } fn response(text: Option<&str>, tool_calls: Vec) -> ChatResponse { ChatResponse { text: text.map(str::to_string), tool_calls, usage: Some(usage(50, 7)), reasoning_content: None, } } fn parent_context(workspace: PathBuf, provider: Arc) -> ParentExecutionContext { let tools: Vec> = vec![Box::new(EchoTool)]; let tool_specs = tools.iter().map(|tool| tool.spec()).collect(); ParentExecutionContext { agent_definition_id: "orchestrator".into(), allowed_subagent_ids: [ "test".to_string(), "tools_agent".to_string(), "integrations_agent".to_string(), ] .into_iter() .collect(), provider, all_tools: Arc::new(tools), all_tool_specs: Arc::new(tool_specs), visible_tool_names: std::collections::HashSet::new(), model_name: "round16-model".to_string(), temperature: 0.0, workspace_dir: workspace, workspace_descriptor: None, memory: Arc::new(StubMemory), agent_config: openhuman_core::openhuman::config::AgentConfig { max_tool_iterations: 3, ..Default::default() }, workflows: Arc::new(Vec::new()), memory_context: Arc::new(Some("parent memory".to_string())), session_id: "round16-parent".to_string(), channel: "round16-channel".to_string(), connected_integrations: Vec::new(), tool_call_format: ToolCallFormat::Native, session_key: "1710000000_parent".to_string(), session_parent_prefix: Some("root".to_string()), on_progress: None, run_queue: None, } } fn agent_definition(id: &str, max_result_chars: Option) -> AgentDefinition { AgentDefinition { id: id.to_string(), when_to_use: "Raw coverage test agent".to_string(), display_name: Some("Round16 Agent".to_string()), system_prompt: PromptSource::Inline("Use the visible tools and answer tersely.".into()), omit_identity: true, omit_memory_context: false, omit_safety_preamble: true, omit_skills_catalog: true, omit_profile: true, omit_memory_md: true, model: Default::default(), temperature: 0.0, tools: ToolScope::Named(vec!["echo".to_string()]), disallowed_tools: Vec::new(), skill_filter: None, extra_tools: Vec::new(), max_iterations: 2, iteration_policy: Default::default(), max_result_chars, max_turn_output_tokens: None, timeout_secs: None, sandbox_mode: SandboxMode::ReadOnly, background: false, trigger_memory_agent: Default::default(), tokenjuice_compression: AgentTokenjuiceCompression::Auto, subagents: Vec::new(), delegate_name: None, agent_tier: Default::default(), source: Default::default(), graph: Default::default(), } } fn tool_names(tools: &[Box]) -> Vec { let mut names = tools .iter() .map(|tool| tool.name().to_string()) .collect::>(); names.sort(); names } async fn start_computer_sidecar(state: SidecarState) -> String { async fn handler( State(state): State, Json(body): Json, ) -> impl IntoResponse { state.requests.lock().expect("requests").push(body.clone()); Json(json!({ "success": true, "data": { "backend": "computer_use", "echo_action": body["action"].clone(), "x": body["params"]["x"].clone() } })) } let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) .await .expect("bind computer sidecar"); let addr = listener.local_addr().expect("sidecar addr"); tokio::spawn(async move { axum::serve( listener, Router::new().route("/", post(handler)).with_state(state), ) .await .expect("serve computer sidecar"); }); format!("http://{addr}/") } fn browser_tool(endpoint: String, workspace: &Path) -> BrowserTool { let security = Arc::new(SecurityPolicy::from_config( &Config::default().autonomy, workspace, workspace, )); BrowserTool::new_with_backend( security, vec!["example.com".into(), "*.example.org".into()], Some("round16-browser".into()), "computer_use".into(), true, "http://127.0.0.1:9515".into(), None, ComputerUseConfig { endpoint, api_key: Some("round16-sidecar-token".into()), timeout_ms: 1_000, allow_remote_endpoint: false, window_allowlist: vec!["OpenHuman".into()], max_coordinate_x: Some(100), max_coordinate_y: Some(100), }, ) } #[tokio::test] async fn round16_browser_computer_use_validation_and_sidecar_paths() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let state = SidecarState::default(); let endpoint = start_computer_sidecar(state.clone()).await; let tool = browser_tool(endpoint, &harness.workspace); let ok = tool .execute(json!({ "action": "mouse_move", "x": 9, "y": 10 })) .await .expect("computer-use mouse_move"); assert!(!ok.is_error, "{}", ok.output()); assert!(ok.output().contains("\"echo_action\": \"mouse_move\"")); let open = tool .execute(json!({ "action": "open", "url": "https://docs.example.org/path" })) .await .expect("computer-use open"); assert!(!open.is_error, "{}", open.output()); assert_eq!(state.requests.lock().expect("requests").len(), 2); for (args, expected) in [ ( json!({ "action": "mouse_click", "x": -1, "y": 2 }), "'x' must be >= 0", ), ( json!({ "action": "mouse_drag", "from_x": 0, "from_y": 0, "to_x": 101, "to_y": 2 }), "exceeds configured limit", ), ( json!({ "action": "open", "url": "file:///tmp/secret" }), "file:// URLs", ), ( json!({ "action": "open", "url": "https://evil.test" }), "not in browser.allowed_domains", ), (json!({ "action": "definitely_missing" }), "Unknown action"), ] { let observed = match tool.execute(args).await { Ok(result) => { assert!(result.is_error); result.output().to_string() } Err(error) => error.to_string(), }; assert!( observed.contains(expected), "expected {expected:?} in {observed}" ); } let bad_endpoint = browser_tool("https://public.example.test/".into(), &harness.workspace) .execute(json!({ "action": "screen_capture" })) .await .expect("public endpoint is rejected as a tool result"); assert!(bad_endpoint.is_error); assert!(bad_endpoint .output() .contains("host 'public.example.test' is public")); } #[test] fn round16_all_tools_registry_branches_and_browser_allowlist() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let mut cfg = Config { workspace_dir: harness.workspace.clone(), config_path: harness.root.join("config.toml"), ..Config::default() }; cfg.node.enabled = false; cfg.gitbooks.enabled = true; cfg.computer_control.enabled = true; cfg.learning.enabled = true; cfg.learning.tool_tracking_enabled = true; cfg.browser.enabled = true; cfg.http_request.allowed_domains = vec![ "*".to_string(), "example.com".to_string(), "*.example.org".to_string(), ]; cfg.mcp_client.servers.push(McpServerConfig { name: "round16-docs".into(), endpoint: "https://example.com/mcp".into(), command: String::new(), args: Vec::new(), env: HashMap::new(), cwd: None, description: Some("Round16 MCP".into()), enabled: true, allowed_tools: Vec::new(), disallowed_tools: Vec::new(), timeout_secs: 10, auth: McpAuthConfig::None, }); let tools = all_tools( Arc::new(cfg.clone()), &Arc::new(SecurityPolicy::from_config( &cfg.autonomy, &harness.workspace, &harness.workspace, )), AuditLogger::disabled(), Arc::new(StubMemory), &BrowserConfig { enabled: true, session_name: Some("round16-session".into()), backend: "computer_use".into(), ..cfg.browser.clone() }, &HttpRequestConfig { allowed_domains: cfg.http_request.allowed_domains.clone(), ..cfg.http_request.clone() }, &harness.workspace, &HashMap::from([( "researcher".to_string(), openhuman_core::openhuman::config::DelegateAgentConfig { model: "round16-delegate-model".to_string(), system_prompt: Some("Delegate test prompt".to_string()), temperature: Some(0.0), max_depth: 1, }, )]), &cfg, ); let names = tool_names(&tools); for expected in [ "spawn_subagent", "spawn_parallel_agents", "browser", "browser_open", "http_request", "web_fetch", "curl", "gitbooks_search", "gitbooks_get_page", "mcp_list_servers", "mcp_list_tools", "mcp_call_tool", "mouse", "keyboard", "tool_stats", "delegate", "mcp_setup_search", "mcp_setup_install_and_connect", ] { assert!( names.iter().any(|name| name == expected), "expected {expected} in {names:?}" ); } assert!(!names.iter().any(|name| name == "node_exec")); assert!(!names.iter().any(|name| name == "npm_exec")); assert!( names.iter().any(|name| name == "browser"), "browser registration covers http_request allowlist normalization" ); } #[tokio::test] async fn round16_spawn_subagent_tool_and_runner_error_success_paths() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let tool = SpawnSubagentTool::new(); let missing_agent = tool .execute(json!({ "prompt": "do work" })) .await .expect("missing agent returns tool result"); assert!(missing_agent.is_error); assert!(missing_agent.output().contains("agent_id")); let disabled_thread = tool .execute(json!({ "agent_id": "researcher", "prompt": "do work", "dedicated_thread": true })) .await .expect("dedicated thread returns tool result"); assert!( disabled_thread.is_error, "dedicated_thread should error: {}", disabled_thread.output() ); // #3049 superseded #1624: dedicated_thread is no longer "temporarily // disabled" — it's accepted but the tool may still error for other // reasons (e.g. no provider configured). Just verify it errors. assert!( !disabled_thread.output().is_empty(), "error output should not be empty" ); let provider = Arc::new(ScriptedProvider::new(vec![response( Some("subagent final answer that will be clipped"), Vec::new(), )])); let parent = parent_context(harness.workspace.clone(), provider.clone()); let definition = agent_definition("round16_worker", Some(18)); let outcome = with_parent_context(parent, async { run_subagent( &definition, "Summarize with no tools.", SubagentRunOptions { context: Some("caller context".into()), task_id: Some("round16-task".into()), ..Default::default() }, ) .await }) .await .expect("run subagent with parent context"); assert_eq!(outcome.agent_id, "round16_worker"); assert_eq!(outcome.output, "subagent final ans\n[...truncated]"); assert!(provider.requests()[0].iter().any(|message| { message.role == "user" && message.content.contains("parent memory") && message.content.contains("caller context") })); let no_parent = run_subagent(&definition, "no parent", SubagentRunOptions::default()) .await .expect_err("subagent outside parent context fails") .to_string(); assert!(no_parent.contains("no parent context")); } #[tokio::test] async fn round16_agent_builder_turn_uses_public_harness_paths() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let provider = Arc::new(ScriptedProvider::new(vec![ response( Some("need echo"), vec![ToolCall { id: "call-round16".into(), name: "echo".into(), arguments: json!({ "message": "builder" }).to_string(), extra_content: None, }], ), response(Some("builder final"), Vec::new()), ])); let mut agent = Agent::builder() .provider_arc(provider) .tools(vec![Box::new(EchoTool)]) .memory(Arc::new(StubMemory)) .tool_dispatcher(Box::new(NativeToolDispatcher)) .config(openhuman_core::openhuman::config::AgentConfig { max_tool_iterations: 3, ..Default::default() }) .model_name("round16-model".to_string()) .temperature(0.0) .workspace_dir(harness.workspace.clone()) .workflows(Vec::new()) .auto_save(false) .event_context("round16-session", "round16-channel") .agent_definition_name("round16_builder") .omit_profile(true) .omit_memory_md(true) .build() .expect("agent builder"); let answer = agent.turn("use echo once").await.expect("agent turn"); assert_eq!(answer, "builder final"); assert!(agent.history().iter().any(|message| matches!( message, openhuman_core::openhuman::inference::provider::ConversationMessage::ToolResults(results) if results.iter().any(|result| result.content.contains("echo:builder")) ))); } #[test] fn round16_auth_profiles_selection_migration_and_drop_edges() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let state_dir = harness.root.join("profile-store"); let store = AuthProfilesStore::new(&state_dir, false); let token = AuthProfile::new_token("channel:slack:bot", "primary", "xoxb-round16".into()); store .upsert_profile(token.clone(), true) .expect("upsert token"); let mut oauth = AuthProfile::new_oauth( "github", "work", TokenSet { access_token: "gh-round16".into(), refresh_token: Some("refresh-round16".into()), id_token: Some("id-round16".into()), expires_at: Some(Utc::now() + ChronoDuration::minutes(10)), token_type: Some("Bearer".into()), scope: Some("repo".into()), }, ); oauth.metadata = BTreeMap::from([("team".to_string(), "coverage".to_string())]); store .upsert_profile(oauth.clone(), false) .expect("upsert oauth"); store .set_active_profile("github", &oauth.id) .expect("activate github"); let loaded = store.load().expect("load auth profiles"); assert_eq!(loaded.profiles[&oauth.id].kind, AuthProfileKind::OAuth); assert!(loaded.profiles[&oauth.id] .token_set .as_ref() .expect("token set") .is_expiring_within(std::time::Duration::from_secs(900))); let service = AuthService::new(&state_dir, false); assert_eq!( service .get_provider_bearer_token("github", None) .expect("github bearer") .as_deref(), Some("gh-round16") ); assert!(service .set_active_profile("github", &token.id) .expect_err("wrong provider activation") .to_string() .contains("belongs to provider")); assert!(store .set_active_profile("github", "missing") .expect_err("missing active profile") .to_string() .contains("Auth profile not found")); let path = store.path().to_path_buf(); let mut raw: Value = serde_json::from_str(&std::fs::read_to_string(&path).expect("profile json")) .expect("valid profile json"); raw["schema_version"] = json!(0); raw["profiles"]["legacy-bad-kind"] = json!({ "provider": "legacy", "profile_name": "bad", "kind": "api_key", "token": "legacy-token", "created_at": "not-a-date", "updated_at": "also-not-a-date", "metadata": {} }); raw["active_profiles"]["legacy"] = json!("legacy-bad-kind"); std::fs::write( &path, serde_json::to_string_pretty(&raw).expect("serialize"), ) .expect("write bad kind"); let migrated = store.load().expect("bad profile kind is dropped"); assert_eq!(migrated.schema_version, 1); assert!(!migrated.profiles.contains_key("legacy-bad-kind")); assert!(!migrated.active_profiles.contains_key("legacy")); raw["schema_version"] = json!(999); std::fs::write( &path, serde_json::to_string_pretty(&raw).expect("serialize"), ) .expect("write future schema"); assert!(store .load() .expect_err("future schema rejected") .to_string() .contains("Unsupported auth profile schema version 999")); } #[tokio::test] async fn round16_app_state_config_and_session_snapshot_edges() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let config = harness.config().await; assert_eq!( config.default_model.as_deref(), Some("round16-coverage-model") ); assert!(config.onboarding_completed); std::fs::create_dir_all(harness.app_state_file().parent().expect("state parent")) .expect("state dir"); std::fs::write(harness.app_state_file(), "{broken").expect("write corrupt app state"); let stored = update_local_state(StoredAppStatePatch { keyring_consent: None, encryption_key: Some(Some(" round16-key ".to_string())), onboarding_tasks: Some(Some(StoredOnboardingTasks { accessibility_permission_granted: true, local_model_consent_given: true, local_model_download_started: false, enabled_tools: vec!["gmail".to_string(), "slack".to_string()], connected_sources: vec!["github".to_string()], updated_at_ms: Some(16), })), }) .await .expect("update app state") .value; assert_eq!(stored.encryption_key.as_deref(), Some("round16-key")); assert_eq!( stored .onboarding_tasks .as_ref() .expect("tasks") .connected_sources, vec!["github"] ); let quarantined = std::fs::read_dir(harness.app_state_file().parent().expect("state parent")) .expect("state entries") .filter_map(Result::ok) .any(|entry| { entry .file_name() .to_string_lossy() .contains("app-state.json.corrupted") }); assert!(quarantined, "corrupt app-state.json should be quarantined"); let mut metadata = HashMap::new(); metadata.insert("user_id".to_string(), "round16-user".to_string()); metadata.insert( "user_json".to_string(), json!({ "id": "round16-user", "displayName": "Round16 User", "email": "round16@example.test" }) .to_string(), ); AuthService::from_config(&config) .store_provider_token( APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME, "round16.header.payload", metadata, true, ) .expect("store app session"); let snap = snapshot().await.expect("snapshot").value; assert!(snap.auth.is_authenticated); assert_eq!( snap.session_token.as_deref(), Some("round16.header.payload") ); assert_eq!(snap.auth.user_id.as_deref(), Some("round16-user")); assert_eq!( snap.local_state.encryption_key.as_deref(), Some("round16-key") ); let cleared = update_local_state(StoredAppStatePatch { keyring_consent: None, encryption_key: Some(None), onboarding_tasks: Some(None), }) .await .expect("clear local app state") .value; assert!(cleared.encryption_key.is_none()); assert!(cleared.onboarding_tasks.is_none()); }