diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index c8654c42b4..2e41810235 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -218,6 +218,11 @@ pub(crate) enum LocalStartIntent { Automatic, } +// The argument tuple is the launch-inputs ABI this seam forwards verbatim to +// `start_local_agent_with_preflight_using` (which carries the same allow): the +// capture → preflight → locked-revalidate → spawn inputs. Collapsing it into +// a params struct would fork the shared `_using` boundary shape for a lint. +#[allow(clippy::too_many_arguments)] async fn start_local_agent_with_preflight( app: &AppHandle, state: &AppState, diff --git a/desktop/src-tauri/src/commands/desktop_lifecycle.rs b/desktop/src-tauri/src/commands/desktop_lifecycle.rs index 80bd30b479..9fe4132a54 100644 --- a/desktop/src-tauri/src/commands/desktop_lifecycle.rs +++ b/desktop/src-tauri/src/commands/desktop_lifecycle.rs @@ -17,6 +17,12 @@ use tauri::{AppHandle, Manager}; mod configurations; +// Tauri IPC command boundary: the nine parameters are the existing frontend +// invoke contract (each named argument is passed individually from +// `desktopLifecycle.ts`), so the shape is ABI-fixed here — a params struct +// would change the wire format. Same command-boundary allow as the other +// wide `#[tauri::command]` (`send_managed_agent_channel_message`). +#[allow(clippy::too_many_arguments)] #[tauri::command] pub fn prepare_desktop_lifecycle( app: AppHandle, diff --git a/desktop/src-tauri/src/managed_agents/effective_config/mod.rs b/desktop/src-tauri/src/managed_agents/effective_config/mod.rs index 3ac743de6b..6f2e60748c 100644 --- a/desktop/src-tauri/src/managed_agents/effective_config/mod.rs +++ b/desktop/src-tauri/src/managed_agents/effective_config/mod.rs @@ -286,6 +286,11 @@ pub fn resolve_effective_model_provider_pair( /// never spawns (see `require_resolved`), so it never needs a mesh preflight /// either; the caller's own orphan handling downstream is unaffected, this /// just avoids tripping mesh bootstrap for a start that will be refused. +/// +/// Bound only by `effective_config/tests.rs` now — production mesh preflights +/// resolve through `PreparedLaunch`'s captured effective config — so the +/// resolver stays as a `cfg(test)` seam for the mesh-model regression tests. +#[cfg(test)] pub fn resolve_effective_relay_mesh_model_id( record: &ManagedAgentRecord, definitions: &[AgentDefinition], diff --git a/desktop/src-tauri/src/managed_agents/mod.rs b/desktop/src-tauri/src/managed_agents/mod.rs index 23e6661a32..070c23a735 100644 --- a/desktop/src-tauri/src/managed_agents/mod.rs +++ b/desktop/src-tauri/src/managed_agents/mod.rs @@ -52,27 +52,45 @@ pub(crate) use team_repair::team_persona_key; mod teams; mod types; -// Shared lock for tests that call `lock_path_mutex` or `lock_env_mutex`. -// Both helpers delegate here so any two tests using either helper are mutually -// exclusive with each other. Tests in other modules that maintain their own -// independent locks (app_state_tests, agent_config_tests, reader_tests) are -// NOT in this domain and are not covered by this mutex. +// Shared lock for tests that call `lock_path_mutex`, `lock_path_mutex_async`, +// or `lock_env_mutex`. All helpers delegate here so any two tests using any +// of them are mutually exclusive with each other. Tests in other modules that +// maintain their own independent locks (app_state_tests, agent_config_tests, +// reader_tests) are NOT in this domain and are not covered by this mutex. +// +// The lock is a `tokio::sync::Mutex` (same convention as buzz-db's +// `POOL_METRICS_TEST_LOCK`) so async tests can hold the serialization guard +// across `.await` points without tripping `clippy::await_holding_lock`, while +// sync tests acquire it with `blocking_lock()`. One lock domain keeps sync +// and async env-mutating tests mutually exclusive; the guard scope (the whole +// test body) is unchanged, so shared-state isolation is preserved exactly. #[cfg(test)] -static PROCESS_ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); +static PROCESS_ENV_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); -// Acquires the shared process-env lock. Call from any test in this module that -// reads, writes, or removes a process-global environment variable (including PATH). +// Acquires the shared process-env lock from a synchronous test. Call from any +// sync test that reads, writes, or removes a process-global environment +// variable (including PATH). Panics if called from inside an async context — +// async tests must use `lock_path_mutex_async` instead. #[cfg(test)] -pub(crate) fn lock_path_mutex() -> std::sync::MutexGuard<'static, ()> { - PROCESS_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()) +pub(crate) fn lock_path_mutex() -> tokio::sync::MutexGuard<'static, ()> { + PROCESS_ENV_MUTEX.blocking_lock() +} + +// Async twin of `lock_path_mutex`: the same single lock domain, acquired in +// `async fn` tests. Hold the returned guard for the whole test body, across +// `.await` points, exactly where the sync guard was previously held — the +// guard keeps every env-mutating test (sync or async) mutually exclusive. +#[cfg(test)] +pub(crate) async fn lock_path_mutex_async() -> tokio::sync::MutexGuard<'static, ()> { + PROCESS_ENV_MUTEX.lock().await } // Delegates to the same lock as `lock_path_mutex`. Tests using either helper // are mutually exclusive with each other; PATH and env-key mutations that go // through these helpers cannot race. #[cfg(test)] -pub(crate) fn lock_env_mutex() -> std::sync::MutexGuard<'static, ()> { - PROCESS_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()) +pub(crate) fn lock_env_mutex() -> tokio::sync::MutexGuard<'static, ()> { + PROCESS_ENV_MUTEX.blocking_lock() } pub use backend::*; diff --git a/desktop/src-tauri/src/managed_agents/placement.rs b/desktop/src-tauri/src/managed_agents/placement.rs index e6a8615cfa..53b81ca6ee 100644 --- a/desktop/src-tauri/src/managed_agents/placement.rs +++ b/desktop/src-tauri/src/managed_agents/placement.rs @@ -202,10 +202,8 @@ pub(crate) fn receive( || (request.action == Action::Start && desired(conn, &request.target.agent)?.map(|(_, id)| id) != Some(event.id.to_hex())))) - { - (Outcome::Unknown, None) - } else if request.action != Action::Status - && nostr::Timestamp::now().as_secs() >= event.created_at.as_secs().saturating_add(30) + || (request.action != Action::Status + && nostr::Timestamp::now().as_secs() >= event.created_at.as_secs().saturating_add(30)) { (Outcome::Unknown, None) } else { diff --git a/desktop/src-tauri/src/managed_agents/restore.rs b/desktop/src-tauri/src/managed_agents/restore.rs index 9614bb315c..16c173fc55 100644 --- a/desktop/src-tauri/src/managed_agents/restore.rs +++ b/desktop/src-tauri/src/managed_agents/restore.rs @@ -598,22 +598,6 @@ pub(crate) fn spawn_pending_profile_reconciliations(app: &tauri::AppHandle, work } } -#[cfg(test)] -mod profile_reconcile_tests { - use super::profile_reconcile_completed; - use crate::commands::ProfileReconcileOutcome; - - #[test] - fn skipped_reconciliation_never_retires_pending_work() { - assert!(profile_reconcile_completed( - ProfileReconcileOutcome::Reconciled - )); - assert!(!profile_reconcile_completed( - ProfileReconcileOutcome::SkippedDisabled - )); - } -} - fn persist_restore_error( app: &tauri::AppHandle, state: &AppState, @@ -630,3 +614,19 @@ fn persist_restore_error( record.last_error = Some(error); save_managed_agents(app, &records) } + +#[cfg(test)] +mod profile_reconcile_tests { + use super::profile_reconcile_completed; + use crate::commands::ProfileReconcileOutcome; + + #[test] + fn skipped_reconciliation_never_retires_pending_work() { + assert!(profile_reconcile_completed( + ProfileReconcileOutcome::Reconciled + )); + assert!(!profile_reconcile_completed( + ProfileReconcileOutcome::SkippedDisabled + )); + } +} diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 2b9c806f4a..23e1ee47f6 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -333,8 +333,8 @@ pub fn build_managed_agent_summary( last_error_code: record.last_error_code, start_on_app_launch: record.start_on_app_launch, auto_restart_on_config_change: record.auto_restart_on_config_change - && !pair_runtime - .is_some_and(|runtime| runtime.spawn_config.runtime_configuration.is_some()) + && pair_runtime + .is_none_or(|runtime| runtime.spawn_config.runtime_configuration.is_none()) && app .state::() .signing_keys() @@ -422,11 +422,12 @@ pub(crate) fn configure_runtime_cli( #[must_use] pub(crate) struct EffortApplied(()); -/// Apply effort env to an agent spawn command. Called by `spawn_agent_child` -/// (production) and `effort_cmd_tests` (test seam). Inner-seam: removing -/// `apply_spawn_effort_env` below turns the production-sequence tests RED. -/// Outer-seam: the returned token is consumed by `spawn_with_effort_proof`; -/// deleting this call leaves `effort` undefined at the spawn site. +/// Apply effort env to an agent spawn command. Called by +/// `spawn_agent_child_prepared` (production) and `effort_cmd_tests` (test +/// seam). Inner-seam: removing `apply_spawn_effort_env` below turns the +/// production-sequence tests RED. Outer-seam: the returned token is consumed +/// by `spawn_with_effort_proof`; deleting this call leaves `effort` undefined +/// at the spawn site. pub(crate) fn apply_effort_to_spawn_command( cmd: &mut std::process::Command, record: &crate::managed_agents::types::ManagedAgentRecord, @@ -443,8 +444,9 @@ pub(crate) fn apply_effort_to_spawn_command( } /// Spawn the agent command, consuming the `EffortApplied` proof token. -/// Deleting `apply_effort_to_spawn_command` from `spawn_agent_child` leaves -/// `effort` undefined here — a compile error CI catches before any test runs. +/// Deleting `apply_effort_to_spawn_command` from `spawn_agent_child_prepared` +/// leaves `effort` undefined here — a compile error CI catches before any +/// test runs. pub(crate) fn spawn_with_effort_proof( cmd: &mut std::process::Command, _effort: EffortApplied, @@ -464,27 +466,10 @@ pub(crate) fn spawn_with_effort_proof( /// publishes the triggering message before this spawn and passes its send /// timestamp here so the harness's first REQ replays past that message no /// matter how long the spawn takes. buzz-acp clamps stale floors to ~15 min. -pub fn spawn_agent_child( - app: &AppHandle, - record: &ManagedAgentRecord, - relay_url: &str, - lazy: bool, - owner_hex: Option<&str>, - replay_floor_unix: Option, - resume: Option<&super::remote_stop::ResumeTicket>, -) -> Result { - spawn_agent_child_prepared( - app, - record, - relay_url, - lazy, - owner_hex, - replay_floor_unix, - resume, - None, - ) -} - +/// +/// `prepared`: the captured, preflighted launch plan. Production callers pass +/// a `PreparedLaunch`; tests may pass `None` to exercise the refusal paths +/// that reject an un-preflighted spawn. #[allow(clippy::too_many_arguments)] pub(crate) fn spawn_agent_child_prepared( app: &AppHandle, @@ -646,7 +631,7 @@ pub(crate) fn spawn_agent_child_prepared( // ── Readiness check: set setup-payload if agent is not ready ───────────── // `spawned_setup_mode` is stamped on `ManagedAgentProcess` below. let spawned_setup_mode = - apply_setup_payload_env(&mut command, record, &descriptor, runtime_meta); + apply_setup_payload_env(&mut command, record, descriptor, runtime_meta); // Emit BUZZ_ACP_IDLE_TIMEOUT only when explicitly set; the harness // DEFAULT_IDLE_TIMEOUT_SECS is the single source of truth. The deprecated // BUZZ_ACP_TURN_TIMEOUT pinned agents to a stale default (320s). @@ -883,7 +868,7 @@ pub(crate) fn spawn_agent_child_prepared( .transpose()?; super::runtime_configurations::apply_required_model_env(&mut command, required_model)?; if let Some(model) = required_model { - if !runtime_meta.is_some_and(|runtime| runtime.id == "claude") { + if runtime_meta.is_none_or(|runtime| runtime.id != "claude") { command.env("BUZZ_ACP_MODEL", model); } } @@ -935,12 +920,13 @@ pub(crate) fn spawn_agent_child_prepared( }) } -/// Spawn (or adopt) the runtime pair for `record` on the caller's bound -/// workspace relay. `workspace_relay` can only be produced by -/// `bind_expected_relay_scope`, so this spawn consumes — by construction — the -/// exact workspace-relay read the caller's scope assertion passed on; it never -/// re-reads the mutable override (see `relay::scope`). The key comes from -/// [`bound_runtime_key`] — the seam the spawn-key regressions exercise. +/// Test-only seam over [`start_managed_agent_process_prepared`] with no +/// captured launch: the runtime-authority regressions in +/// `runtime/authority_tests.rs` use it to prove a spawn without a preflighted +/// plan is refused before any side effect. Production callers resolve a +/// `PreparedLaunch` and go through `start_managed_agent_process_prepared` +/// directly. +#[cfg(test)] pub fn start_managed_agent_process( app: &AppHandle, record: &mut ManagedAgentRecord, diff --git a/desktop/src-tauri/src/managed_agents/runtime_configurations.rs b/desktop/src-tauri/src/managed_agents/runtime_configurations.rs index ae75279fee..2d505e752c 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_configurations.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_configurations.rs @@ -88,9 +88,6 @@ impl PreparedLaunch { Ok(()) } - pub(crate) fn record(&self) -> &ManagedAgentRecord { - &self.record - } pub(crate) fn configuration(&self) -> Option { selected(&self.record) .ok() @@ -407,7 +404,7 @@ fn verify_owner(record: &ManagedAgentRecord, owner: &str) -> Result<(), String> buzz_sdk_pkg::nip_oa::verify_auth_tag(tag, &agent).ok() }); if record.backend != super::BackendKind::Local - || !verified.is_some_and(|key| key.to_hex() == owner) + || verified.is_none_or(|key| key.to_hex() != owner) { return Err("Agent ownership is unavailable in this scope".into()); } @@ -460,6 +457,17 @@ fn local_host( relay_url: community.into(), owner_keys: keys, }; + // `managed_agents_base_dir` only creates `agents/`; the scoped + // `retention/` parent is normally ensured lazily by + // `active_retention_scope` during event retention, which a first + // lifecycle message can precede on a fresh install (and callers that + // bypass app startup, like tests, never trigger). Same on-demand parent + // ensure as `remote_stop::connection` so opening the scoped identity db + // is safe for every fresh-state caller. + if let Some(parent) = scope.db_path.parent() { + std::fs::create_dir_all(parent) + .map_err(|error| format!("failed to create retention scope directory: {error}"))?; + } crate::commands::desktop_stop::local_id( &mut super::retention::open_retention_db(&scope.db_path)?, &scope, diff --git a/desktop/src-tauri/src/managed_agents/runtime_configurations/orchestration_tests.rs b/desktop/src-tauri/src/managed_agents/runtime_configurations/orchestration_tests.rs index 65b5cc8d8b..3bf0160df4 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_configurations/orchestration_tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_configurations/orchestration_tests.rs @@ -67,7 +67,7 @@ impl Fixture { save(&mut self.record, &self.owner, relay, entry) } fn persist(&self) { - agents::save_managed_agents(self.app.handle(), &[self.record.clone()]).unwrap(); + agents::save_managed_agents(self.app.handle(), std::slice::from_ref(&self.record)).unwrap(); } fn finish_children(&self) -> Vec<(String, Option)> { let state = self.app.state::(); @@ -132,7 +132,7 @@ impl Drop for Fixture { #[tokio::test] async fn ordinary_default_start_cannot_launch_new_selection_after_preflight() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let mut fixture = Fixture::new(); fixture.persist(); let app = fixture.app.handle().clone(); @@ -168,7 +168,7 @@ async fn ordinary_default_start_cannot_launch_new_selection_after_preflight() { #[tokio::test] async fn restore_preflights_selected_provider_in_both_directions() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; for (default_provider, named_provider, expected) in [ ("relay-mesh", "openai", None), ("openai", "relay-mesh", Some("selected-model")), @@ -215,7 +215,7 @@ async fn restore_preflights_selected_provider_in_both_directions() { #[tokio::test] async fn restore_refuses_provider_preflight_failure_before_spawn() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let mut fixture = Fixture::new(); fixture.named(ONE, "relay-mesh", "offline-model"); fixture.persist(); @@ -242,7 +242,7 @@ async fn restore_refuses_provider_preflight_failure_before_spawn() { #[tokio::test] async fn bulk_restart_preflights_every_captured_community_and_revalidates_before_spawn() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; // The unchanged case proves a real lazy pair spawn and receipts. The other // cases edit while the first provider awaits, after BOTH plans were captured. for mutation in ["unchanged", "revision", "workspace", "selection"] { @@ -340,7 +340,7 @@ async fn bulk_restart_preflights_every_captured_community_and_revalidates_before #[tokio::test] async fn restore_selection_fence_survives_suspension_in_both_directions() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; for starts_named in [false, true] { let mut fixture = Fixture::new(); let named = fixture.named(ONE, "relay-mesh", "named-model"); @@ -407,7 +407,7 @@ async fn restore_selection_fence_survives_suspension_in_both_directions() { #[tokio::test] async fn ordinary_default_revalidates_effective_inputs_not_only_selection() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let mut fixture = Fixture::new(); fixture.persist(); let app = fixture.app.handle().clone(); @@ -466,7 +466,7 @@ impl Fixture { #[tokio::test] async fn direct_restart_preflights_target_and_preserves_old_child_on_refusal() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; for relay in [ONE, TWO] { for mutation in [ "unchanged", @@ -565,7 +565,7 @@ async fn direct_restart_preflights_target_and_preserves_old_child_on_refusal() { #[tokio::test] async fn direct_start_and_reconcile_continuation_refuse_selection_change_and_stop() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; for explicit in [true, false] { for mutation in ["selection", "stop"] { let mut fixture = Fixture::new(); @@ -609,7 +609,7 @@ async fn direct_start_and_reconcile_continuation_refuse_selection_change_and_sto #[cfg(feature = "mesh-llm")] #[tokio::test] async fn recovery_uses_running_pair_snapshots_not_default_or_next_selection() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; for default_mesh in [false, true] { let mut fixture = Fixture::new(); fixture.hold_children(); @@ -664,7 +664,7 @@ async fn recovery_uses_running_pair_snapshots_not_default_or_next_selection() { #[tokio::test] async fn reconcile_keeps_authorization_inputs_without_cross_pair_timestamp_failure() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let mut fixture = Fixture::new(); fixture.hold_children(); fixture.named(ONE, "relay-mesh", "mesh-one"); @@ -693,7 +693,7 @@ async fn reconcile_keeps_authorization_inputs_without_cross_pair_timestamp_failu #[tokio::test] async fn bulk_stop_during_preflight_cannot_be_resumed_by_automatic_start() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let mut fixture = Fixture::new(); fixture.named(ONE, "relay-mesh", "mesh-one"); fixture.named(TWO, "relay-mesh", "mesh-two"); diff --git a/desktop/src-tauri/src/managed_agents/runtime_configurations/tests.rs b/desktop/src-tauri/src/managed_agents/runtime_configurations/tests.rs index 6023c36afc..d783e19f0a 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_configurations/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_configurations/tests.rs @@ -227,7 +227,7 @@ fn malformed_or_stale_reference_is_rejected_before_launch_resolution() { async fn shared_spawn_registers_exact_plan_and_rejects_edits_or_lost_identity() { use std::os::unix::fs::PermissionsExt; use tauri::Manager; - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let temp = tempfile::tempdir().unwrap(); struct Restore(Vec<(&'static str, Option)>); impl Drop for Restore { diff --git a/desktop/src-tauri/src/managed_agents/runtime_configurations/tests/remote_credentials.rs b/desktop/src-tauri/src/managed_agents/runtime_configurations/tests/remote_credentials.rs index 5dfd4b8730..150a90c926 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_configurations/tests/remote_credentials.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_configurations/tests/remote_credentials.rs @@ -220,7 +220,7 @@ impl Drop for Fixture { #[tokio::test] async fn provisioned_destination_catalog_start_and_same_host_switch_use_shared_launch() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let mut fixture = Fixture::new(); let catalog = fixture.action(Action::Catalog, None).await; assert_eq!(catalog.outcome, Outcome::Ready); @@ -285,7 +285,7 @@ async fn provisioned_destination_catalog_start_and_same_host_switch_use_shared_l #[tokio::test] async fn unavailable_identity_or_runtime_excludes_catalog_and_refuses_start() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; for missing in ["absent", "wrong", "runtime"] { let mut fixture = Fixture::new(); match missing { @@ -324,7 +324,7 @@ async fn unavailable_identity_or_runtime_excludes_catalog_and_refuses_start() { #[tokio::test] async fn revoked_key_keeps_existing_process_visible_and_restart_refuses_without_teardown() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let mut fixture = Fixture::new(); assert_eq!( fixture.action(Action::Start, None).await.outcome, @@ -350,7 +350,7 @@ async fn revoked_key_keeps_existing_process_visible_and_restart_refuses_without_ #[tokio::test] async fn key_loss_during_preflight_is_rechecked_for_start_and_catalog() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; for action in [Action::Start, Action::Catalog] { let fixture = Fixture::new(); let result = fixture @@ -382,7 +382,7 @@ async fn key_loss_during_preflight_is_rechecked_for_start_and_catalog() { #[tokio::test] async fn failed_destination_preflight_never_tears_down_existing_source() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let fixture = Fixture::new(); assert_eq!( fixture.action(Action::Start, None).await.outcome, @@ -436,7 +436,7 @@ fn assert_failed_without_child(fixture: &Fixture) -> ManagedAgentRecord { #[tokio::test] async fn post_stop_key_loss_or_executable_loss_persists_failed_without_restoring_credentials() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; for revoke_key in [true, false] { let fixture = Fixture::new(); let store_path = agents::storage::managed_agents_store_path(fixture.app.handle()).unwrap(); @@ -480,7 +480,7 @@ async fn post_stop_key_loss_or_executable_loss_persists_failed_without_restoring #[tokio::test] async fn post_stop_expiry_persists_truthful_failed_without_second_spawn() { - let _guard = agents::lock_path_mutex(); + let _guard = agents::lock_path_mutex_async().await; let fixture = Fixture::new(); // TERM is ignored only by this bounded synthetic process; ordinary Stop // takes its one-second grace then SIGKILL, crossing the captured deadline. diff --git a/desktop/src-tauri/src/managed_agents/spawn_snapshot.rs b/desktop/src-tauri/src/managed_agents/spawn_snapshot.rs index 4a77b0d014..8192bd1e4f 100644 --- a/desktop/src-tauri/src/managed_agents/spawn_snapshot.rs +++ b/desktop/src-tauri/src/managed_agents/spawn_snapshot.rs @@ -181,6 +181,10 @@ pub(crate) fn effective_effort(descriptor: &EffectiveHarnessDescriptor) -> Optio impl SpawnConfigSnapshot { /// Assemble the snapshot from values a spawn has already resolved. + /// Read by `mesh_llm::recovery` to collect the mesh consumer model ids of + /// live pairs; dead only in default-feature builds, where `mesh-llm` is + /// off and that consumer is compiled out. + #[cfg_attr(not(feature = "mesh-llm"), allow(dead_code))] pub(crate) fn relay_mesh_model_id(&self) -> Option { super::resolved_relay_mesh_model_id(self.provider.as_deref(), self.model.as_deref()) }