Skip to content

Commit 9871ccf

Browse files
authored
fix(tests): stop TEST_ENV_LOCK poison cascade turning 1 panic into 38 (tinyhumansai#1604)
1 parent 6abf4a6 commit 9871ccf

13 files changed

Lines changed: 136 additions & 48 deletions

File tree

‎src/core/jsonrpc_tests.rs‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -735,10 +735,19 @@ async fn thread_not_found_rpc_error_does_not_report_to_sentry() {
735735
let _sentry_guard = sentry::HubSwitchGuard::new(sentry_hub);
736736

737737
let subscriber = tracing_subscriber::registry().with(
738-
sentry::integrations::tracing::layer().event_filter(|metadata| match *metadata.level() {
739-
Level::ERROR => sentry::integrations::tracing::EventFilter::Event,
740-
Level::WARN | Level::INFO => sentry::integrations::tracing::EventFilter::Breadcrumb,
741-
_ => sentry::integrations::tracing::EventFilter::Ignore,
738+
sentry::integrations::tracing::layer().event_filter(|metadata| {
739+
// Mirror the production sentry-tracing layer: events emitted from
740+
// `report_error_message` are captured directly via
741+
// `sentry::capture_message` and must not be picked up here too
742+
// (otherwise this test sees double events).
743+
if metadata.target() == crate::core::observability::REPORT_ERROR_TRACING_TARGET {
744+
return sentry::integrations::tracing::EventFilter::Ignore;
745+
}
746+
match *metadata.level() {
747+
Level::ERROR => sentry::integrations::tracing::EventFilter::Event,
748+
Level::WARN | Level::INFO => sentry::integrations::tracing::EventFilter::Breadcrumb,
749+
_ => sentry::integrations::tracing::EventFilter::Ignore,
750+
}
742751
}),
743752
);
744753
let _subscriber_guard = tracing::subscriber::set_default(subscriber);

‎src/core/logging.rs‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,13 @@ where
354354
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
355355
{
356356
sentry::integrations::tracing::layer().event_filter(|md: &tracing::Metadata<'_>| {
357+
// Events emitted from `report_error_message` are captured directly via
358+
// `sentry::capture_message` at the call site (see
359+
// `core::observability::REPORT_ERROR_TRACING_TARGET` for rationale).
360+
// Skip them here so we don't double-report.
361+
if md.target() == crate::core::observability::REPORT_ERROR_TRACING_TARGET {
362+
return sentry::integrations::tracing::EventFilter::Ignore;
363+
}
357364
match *md.level() {
358365
Level::ERROR => sentry::integrations::tracing::EventFilter::Event,
359366
Level::WARN | Level::INFO => sentry::integrations::tracing::EventFilter::Breadcrumb,
@@ -372,7 +379,7 @@ mod tests {
372379
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
373380

374381
fn with_clean_rust_log<R>(f: impl FnOnce() -> R) -> R {
375-
let _guard = ENV_LOCK.lock().unwrap();
382+
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
376383
let prior = std::env::var("RUST_LOG").ok();
377384
std::env::remove_var("RUST_LOG");
378385
let result = f();
@@ -435,7 +442,7 @@ mod tests {
435442

436443
#[test]
437444
fn seed_rust_log_respects_existing_value() {
438-
let _guard = ENV_LOCK.lock().unwrap();
445+
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
439446
let prior = std::env::var("RUST_LOG").ok();
440447
std::env::set_var("RUST_LOG", "warn");
441448
seed_rust_log(true, CliLogDefault::Global);
@@ -456,7 +463,7 @@ mod tests {
456463

457464
#[test]
458465
fn parse_log_file_constraints_handles_csv_and_whitespace() {
459-
let _guard = ENV_LOCK.lock().unwrap();
466+
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
460467
let prior = std::env::var("OPENHUMAN_LOG_FILE_CONSTRAINTS").ok();
461468
std::env::set_var("OPENHUMAN_LOG_FILE_CONSTRAINTS", "rpc, , agent ,memory");
462469
let parsed = parse_log_file_constraints();

‎src/core/observability.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@ fn report_expected_message(kind: ExpectedErrorKind, message: &str, domain: &str,
119119
}
120120
}
121121

122+
/// Distinct `tracing::Metadata::target()` we set on the diagnostic
123+
/// `tracing::error!` emitted from [`report_error_message`].
124+
///
125+
/// Sentry capture for this helper happens via an explicit
126+
/// `sentry::capture_message` call below — not via the `sentry-tracing`
127+
/// layer scooping up the `tracing::error!` event. The production
128+
/// `sentry_tracing_layer()` in `core::logging` filters events with this
129+
/// target to `EventFilter::Ignore` so we never double-report (one direct
130+
/// `capture_message`, one tracing-bridge capture of the same condition).
131+
///
132+
/// Why direct capture instead of relying on the bridge: the bridge worked
133+
/// in steady-state but flaked under parallel test scheduling
134+
/// (`thread_not_found_rpc_error_does_not_report_to_sentry` repeatedly hit
135+
/// `events.len() == 0` in CI even with a thread-default subscriber wired
136+
/// up — likely a Linux-only thread-local ordering quirk in
137+
/// `sentry-tracing`'s `Hub::current()` lookup at event-emit time). Direct
138+
/// `sentry::capture_message` synchronously routes through the active hub
139+
/// and is deterministic, which keeps both production reporting and tests
140+
/// honest.
141+
pub const REPORT_ERROR_TRACING_TARGET: &str = "openhuman::observability::report_error";
142+
122143
pub(crate) fn report_error_message(
123144
message: &str,
124145
domain: &str,
@@ -134,7 +155,15 @@ pub(crate) fn report_error_message(
134155
}
135156
},
136157
|| {
158+
// Direct, synchronous Sentry capture — see
159+
// `REPORT_ERROR_TRACING_TARGET` for why we don't rely on the
160+
// `sentry-tracing` layer for this call site.
161+
sentry::capture_message(message, sentry::Level::Error);
162+
// Diagnostic log line for stderr / file appenders. Tagged with
163+
// the marker target so the production sentry-tracing layer
164+
// skips it (no double Sentry event).
137165
tracing::error!(
166+
target: REPORT_ERROR_TRACING_TARGET,
138167
domain = domain,
139168
operation = operation,
140169
error = %message,

‎src/openhuman/composio/periodic.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ mod tests {
285285
async fn run_one_tick_returns_ok_when_no_client() {
286286
// Isolate the workspace/env so config loading doesn't contend with
287287
// sibling tests mutating OPENHUMAN_WORKSPACE in parallel.
288-
let _guard = ENV_LOCK.lock().expect("env lock");
288+
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
289289
let tmp = tempdir().expect("tempdir");
290290
unsafe {
291291
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());

‎src/openhuman/config/ops_tests.rs‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use crate::openhuman::config::TEST_ENV_LOCK as ENV_LOCK;
3737

3838
#[test]
3939
fn env_flag_enabled_recognizes_truthy_forms() {
40-
let _g = ENV_LOCK.lock().unwrap();
40+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
4141
let key = "OPENHUMAN_TEST_FLAG_A";
4242
for truthy in ["1", "true", "TRUE", "yes", "YES"] {
4343
unsafe {
@@ -61,7 +61,7 @@ fn env_flag_enabled_recognizes_truthy_forms() {
6161

6262
#[test]
6363
fn core_rpc_url_from_env_returns_default_when_unset() {
64-
let _g = ENV_LOCK.lock().unwrap();
64+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
6565
unsafe {
6666
std::env::remove_var("OPENHUMAN_CORE_RPC_URL");
6767
}
@@ -70,7 +70,7 @@ fn core_rpc_url_from_env_returns_default_when_unset() {
7070

7171
#[test]
7272
fn core_rpc_url_from_env_uses_override_when_set() {
73-
let _g = ENV_LOCK.lock().unwrap();
73+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
7474
unsafe {
7575
std::env::set_var("OPENHUMAN_CORE_RPC_URL", "http://1.2.3.4:9999/rpc");
7676
}
@@ -116,7 +116,7 @@ fn config_openhuman_dir_returns_config_path_parent() {
116116

117117
#[test]
118118
fn get_runtime_flags_reads_env_overrides() {
119-
let _g = ENV_LOCK.lock().unwrap();
119+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
120120
unsafe {
121121
std::env::remove_var("OPENHUMAN_BROWSER_ALLOW_ALL");
122122
}
@@ -128,7 +128,7 @@ fn get_runtime_flags_reads_env_overrides() {
128128

129129
#[test]
130130
fn set_browser_allow_all_toggles_env_var() {
131-
let _g = ENV_LOCK.lock().unwrap();
131+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
132132
let before = std::env::var("OPENHUMAN_BROWSER_ALLOW_ALL").ok();
133133

134134
let _ = set_browser_allow_all(true);
@@ -503,7 +503,7 @@ async fn get_config_snapshot_wraps_snapshot_in_rpc_outcome() {
503503

504504
#[tokio::test]
505505
async fn load_and_apply_dictation_settings_rejects_invalid_activation_mode() {
506-
let _g = ENV_LOCK.lock().unwrap();
506+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
507507
let tmp = tempdir().unwrap();
508508
unsafe {
509509
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -525,7 +525,7 @@ async fn load_and_apply_dictation_settings_rejects_invalid_activation_mode() {
525525

526526
#[tokio::test]
527527
async fn load_and_apply_voice_server_settings_rejects_invalid_activation_mode() {
528-
let _g = ENV_LOCK.lock().unwrap();
528+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
529529
let tmp = tempdir().unwrap();
530530
unsafe {
531531
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -550,7 +550,7 @@ async fn load_and_apply_voice_server_settings_rejects_invalid_activation_mode()
550550

551551
#[tokio::test]
552552
async fn load_and_apply_dictation_settings_accepts_valid_modes() {
553-
let _g = ENV_LOCK.lock().unwrap();
553+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
554554
let tmp = tempdir().unwrap();
555555
unsafe {
556556
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -576,7 +576,7 @@ async fn load_and_apply_dictation_settings_accepts_valid_modes() {
576576

577577
#[tokio::test]
578578
async fn load_and_apply_voice_server_settings_accepts_valid_modes_and_clamps() {
579-
let _g = ENV_LOCK.lock().unwrap();
579+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
580580
let tmp = tempdir().unwrap();
581581
unsafe {
582582
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -609,7 +609,7 @@ async fn load_and_apply_voice_server_settings_accepts_valid_modes_and_clamps() {
609609

610610
#[tokio::test]
611611
async fn get_dictation_settings_reads_from_loaded_config() {
612-
let _g = ENV_LOCK.lock().unwrap();
612+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
613613
let tmp = tempdir().unwrap();
614614
unsafe {
615615
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -625,7 +625,7 @@ async fn get_dictation_settings_reads_from_loaded_config() {
625625

626626
#[tokio::test]
627627
async fn get_voice_server_settings_reads_from_loaded_config() {
628-
let _g = ENV_LOCK.lock().unwrap();
628+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
629629
let tmp = tempdir().unwrap();
630630
unsafe {
631631
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -640,7 +640,7 @@ async fn get_voice_server_settings_reads_from_loaded_config() {
640640

641641
#[tokio::test]
642642
async fn get_onboarding_completed_reads_from_loaded_config() {
643-
let _g = ENV_LOCK.lock().unwrap();
643+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
644644
let tmp = tempdir().unwrap();
645645
unsafe {
646646
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -655,7 +655,7 @@ async fn get_onboarding_completed_reads_from_loaded_config() {
655655

656656
#[tokio::test]
657657
async fn load_and_resolve_api_url_returns_api_url_in_response() {
658-
let _g = ENV_LOCK.lock().unwrap();
658+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
659659
let tmp = tempdir().unwrap();
660660
unsafe {
661661
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -669,7 +669,7 @@ async fn load_and_resolve_api_url_returns_api_url_in_response() {
669669

670670
#[tokio::test]
671671
async fn workspace_onboarding_flag_resolve_rejects_invalid_and_defaults() {
672-
let _g = ENV_LOCK.lock().unwrap();
672+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
673673
let tmp = tempdir().unwrap();
674674
unsafe {
675675
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -691,7 +691,7 @@ async fn workspace_onboarding_flag_resolve_rejects_invalid_and_defaults() {
691691

692692
#[tokio::test]
693693
async fn workspace_onboarding_flag_set_rejects_invalid_names() {
694-
let _g = ENV_LOCK.lock().unwrap();
694+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
695695
let tmp = tempdir().unwrap();
696696
unsafe {
697697
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
@@ -709,7 +709,7 @@ async fn workspace_onboarding_flag_set_rejects_invalid_names() {
709709

710710
#[tokio::test]
711711
async fn workspace_onboarding_flag_set_round_trip() {
712-
let _g = ENV_LOCK.lock().unwrap();
712+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
713713
let tmp = tempdir().unwrap();
714714
unsafe {
715715
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());

‎src/openhuman/credentials/cli.rs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ mod tests {
181181

182182
#[tokio::test]
183183
async fn cli_auth_login_provider_branch_stores_credentials() {
184-
let _g = ENV_LOCK.lock().unwrap();
184+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
185185
let tmp = TempDir::new().unwrap();
186186
set_workspace(&tmp);
187187
let result = cli_auth_login(
@@ -204,7 +204,7 @@ mod tests {
204204

205205
#[tokio::test]
206206
async fn cli_auth_login_with_non_empty_fields_passes_them_through() {
207-
let _g = ENV_LOCK.lock().unwrap();
207+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
208208
let tmp = TempDir::new().unwrap();
209209
set_workspace(&tmp);
210210
let fields = serde_json::json!({ "org_id": "org-1" });
@@ -216,7 +216,7 @@ mod tests {
216216

217217
#[tokio::test]
218218
async fn cli_auth_logout_provider_branch_reports_no_op_on_empty_store() {
219-
let _g = ENV_LOCK.lock().unwrap();
219+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
220220
let tmp = TempDir::new().unwrap();
221221
set_workspace(&tmp);
222222
let result = cli_auth_logout("openai".into(), None).await;
@@ -230,7 +230,7 @@ mod tests {
230230

231231
#[tokio::test]
232232
async fn cli_auth_status_provider_branch_lists_for_provider() {
233-
let _g = ENV_LOCK.lock().unwrap();
233+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
234234
let tmp = TempDir::new().unwrap();
235235
set_workspace(&tmp);
236236
let result = cli_auth_status("openai".into(), None).await;
@@ -245,7 +245,7 @@ mod tests {
245245

246246
#[tokio::test]
247247
async fn cli_auth_list_with_empty_filter_lists_all() {
248-
let _g = ENV_LOCK.lock().unwrap();
248+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
249249
let tmp = TempDir::new().unwrap();
250250
set_workspace(&tmp);
251251
let out = cli_auth_list(None).await.expect("list ok");
@@ -256,7 +256,7 @@ mod tests {
256256

257257
#[tokio::test]
258258
async fn cli_auth_list_rejects_whitespace_only_filter_as_no_filter() {
259-
let _g = ENV_LOCK.lock().unwrap();
259+
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
260260
let tmp = TempDir::new().unwrap();
261261
set_workspace(&tmp);
262262
let out = cli_auth_list(Some(" ".into())).await.expect("list ok");

‎src/openhuman/local_ai/install.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,33 @@ mod tests {
468468
#[test]
469469
fn find_system_ollama_binary_detects_macos_app_bundle_in_applications() {
470470
let _lock = env_lock();
471+
// `find_system_ollama_binary` probes a fixed priority list on macOS:
472+
// 1. /usr/local/bin/ollama (intel homebrew, hand-installed)
473+
// 2. /opt/homebrew/bin/ollama (apple-silicon homebrew)
474+
// 3. /Applications/Ollama.app/Contents/Resources/ollama
475+
// 4. $HOME/Applications/Ollama.app/Contents/Resources/ollama
476+
// The test exercises (4) by pointing $HOME at a tempdir and clearing
477+
// PATH/OLLAMA_BIN. Paths (1)–(3) are absolute and cannot be redirected
478+
// — if a dev machine already has Ollama installed at either homebrew
479+
// location or in the system /Applications dir, the function returns
480+
// that real binary first and the assertion below fails. Skip when any
481+
// earlier candidate already resolves so this test stays a regression
482+
// gate on the ~/Applications branch and not a "is Ollama installed on
483+
// this CI runner" probe.
484+
let unmaskable_real_install = [
485+
"/usr/local/bin/ollama",
486+
"/opt/homebrew/bin/ollama",
487+
"/Applications/Ollama.app/Contents/Resources/ollama",
488+
]
489+
.iter()
490+
.any(|p| std::path::Path::new(p).is_file());
491+
if unmaskable_real_install {
492+
eprintln!(
493+
"skipping: host has a real Ollama install at a higher-priority absolute path \
494+
the test cannot mock"
495+
);
496+
return;
497+
}
471498
let tmp = tempfile::tempdir().unwrap();
472499
// Build a fake /Applications/Ollama.app/Contents/Resources/ollama tree.
473500
let bundle_bin = tmp

0 commit comments

Comments
 (0)