use super::*; use crate::transport::Listener; #[test] fn auth_doctor_provider_focus_uses_global_provider_when_positional_is_absent() { assert_eq!( auth_doctor_provider_arg(None, &ProviderChoice::Cerebras), Some("cerebras") ); assert_eq!( auth_doctor_provider_arg(None, &ProviderChoice::Auto), None, "auto should keep the default doctor behavior of checking configured providers" ); } #[test] fn auth_doctor_positional_provider_wins_over_global_provider() { assert_eq!( auth_doctor_provider_arg(Some("openai"), &ProviderChoice::Cerebras), Some("openai"), "`jcode --provider cerebras auth doctor openai` should diagnose the explicit positional provider" ); } struct ReloadTestEnv { prev_socket: Option, prev_runtime: Option, socket_path: std::path::PathBuf, } impl ReloadTestEnv { fn new() -> Self { let temp = tempfile::tempdir().expect("tempdir"); let socket_path = temp.path().join("jcode.sock"); let prev_socket = std::env::var_os("JCODE_SOCKET"); let prev_runtime = std::env::var_os("JCODE_RUNTIME_DIR"); crate::server::set_socket_path(socket_path.to_str().expect("utf8 socket path")); crate::env::set_var("JCODE_RUNTIME_DIR", temp.path()); // Keep tempdir alive for the duration of the test helper. let _ = temp.keep(); Self { prev_socket, prev_runtime, socket_path, } } } impl Drop for ReloadTestEnv { fn drop(&mut self) { crate::server::clear_reload_marker(); let _ = std::fs::remove_file(&self.socket_path); if let Some(prev_socket) = &self.prev_socket { crate::env::set_var("JCODE_SOCKET", prev_socket); } else { crate::env::remove_var("JCODE_SOCKET"); } if let Some(prev_runtime) = &self.prev_runtime { crate::env::set_var("JCODE_RUNTIME_DIR", prev_runtime); } else { crate::env::remove_var("JCODE_RUNTIME_DIR"); } } } #[cfg(unix)] #[test] fn spawn_lock_serializes_shared_server_bootstrap() { let temp = tempfile::tempdir().expect("tempdir"); let socket_path = temp.path().join("jcode.sock"); let lock_path = spawn_lock_path(&socket_path); let first = try_acquire_spawn_lock(&lock_path) .expect("acquire first lock") .expect("first lock should succeed"); let second = try_acquire_spawn_lock(&lock_path).expect("acquire second lock"); assert!( second.is_none(), "second lock should be held by first guard" ); drop(first); let third = try_acquire_spawn_lock(&lock_path) .expect("acquire third lock") .expect("third lock should succeed after release"); drop(third); assert!( !lock_path.exists(), "lock file should be cleaned up when the guard drops" ); } #[test] fn resolve_resume_id_imports_raw_codex_session_ids() { let _guard = crate::storage::lock_test_env(); let temp = tempfile::tempdir().expect("tempdir"); crate::env::set_var("JCODE_HOME", temp.path()); let codex_dir = temp.path().join("external/.codex/sessions/2026/04/16"); std::fs::create_dir_all(&codex_dir).expect("create codex dir"); std::fs::write( codex_dir.join("rollout.jsonl"), concat!( "{\"timestamp\":\"2026-04-16T10:00:00Z\",\"type\":\"session_meta\",\"payload\":{\"id\":\"codex-cli-resume-test\",\"timestamp\":\"2026-04-16T09:59:00Z\",\"cwd\":\"/tmp/codex-cli-resume\"}}\n", "{\"timestamp\":\"2026-04-16T10:00:01Z\",\"type\":\"response_item\",\"payload\":{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Resume this Codex session\"}]}}\n", "{\"timestamp\":\"2026-04-16T10:00:02Z\",\"type\":\"response_item\",\"payload\":{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Imported\"}]}}\n" ), ) .expect("write codex transcript"); let resolved = resolve_resume_id("codex-cli-resume-test").expect("resolve codex id"); let imported_id = crate::import::imported_codex_session_id("codex-cli-resume-test"); assert_eq!(resolved, imported_id); let session = crate::session::Session::load(&resolved).expect("load imported session"); assert_eq!(session.messages.len(), 2); crate::env::remove_var("JCODE_HOME"); } #[test] fn resume_failure_defers_to_server_during_reload_handoff() { // Issue #328: when `--resume ` cannot be resolved locally but a reload/ // update/restart handoff is in progress (JCODE_RESUMING set), we must defer // to the server instead of exiting with "No session found matching ...". let with_resuming = |key: &str| { if key == "JCODE_RESUMING" { Some(std::ffi::OsString::from("1")) } else { None } }; assert_eq!( resume_resolution_failure_action("ses_1780655839703_174710856", with_resuming), ResumeResolutionFailureAction::DeferToServer, ); } #[test] fn resume_failure_exits_when_no_handoff_in_progress() { // Without a reload handoff, an unresolved id is a genuine user error and // should still surface the actionable "use --resume to list" message + exit. let no_env = |_key: &str| Option::::None; assert_eq!( resume_resolution_failure_action("totally-bogus-id", no_env), ResumeResolutionFailureAction::Exit, ); } #[tokio::test] async fn wait_for_existing_reload_server_uses_reloading_server_instead_of_spawning() { let _guard = crate::storage::lock_test_env(); let env = ReloadTestEnv::new(); crate::server::write_reload_state( "reload-test", "hash", crate::server::ReloadPhase::Starting, None, ); let bind_path = env.socket_path.clone(); let (release_tx, release_rx) = tokio::sync::oneshot::channel(); let bind_task = tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(100)).await; let listener = Listener::bind(&bind_path).expect("bind replacement listener"); crate::server::write_reload_state( "reload-test", "hash", crate::server::ReloadPhase::SocketReady, None, ); let _listener = listener; let _ = release_rx.await; }); let result = tokio::time::timeout( std::time::Duration::from_secs(2), wait_for_existing_reload_server("test"), ) .await .expect("reload wait should not hang"); let _ = release_tx.send(()); bind_task.await.expect("bind task"); assert!(result); } #[tokio::test] async fn wait_for_existing_reload_server_returns_false_for_failed_reload() { let _guard = crate::storage::lock_test_env(); let _env = ReloadTestEnv::new(); crate::server::write_reload_state( "reload-test", "hash", crate::server::ReloadPhase::Failed, Some("boom".to_string()), ); assert!(!wait_for_existing_reload_server("test").await); } #[tokio::test] async fn wait_for_resuming_server_detects_delayed_listener_without_marker() { let _guard = crate::storage::lock_test_env(); let env = ReloadTestEnv::new(); let bind_path = env.socket_path.clone(); let (release_tx, release_rx) = tokio::sync::oneshot::channel(); let bind_task = tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(100)).await; let listener = Listener::bind(&bind_path).expect("bind delayed listener"); let _listener = listener; let _ = release_rx.await; }); let result = tokio::time::timeout( std::time::Duration::from_secs(2), wait_for_resuming_server("test", std::time::Duration::from_secs(1)), ) .await .expect("resume wait should not hang"); let _ = release_tx.send(()); bind_task.await.expect("bind task"); assert!( result, "resume wait should detect a delayed server without requiring a reload marker" ); } #[tokio::test] async fn wait_for_reloading_server_returns_false_when_idle() { let _guard = crate::storage::lock_test_env(); let _env = ReloadTestEnv::new(); assert!(!wait_for_reloading_server().await); } #[tokio::test] async fn wait_for_reloading_server_returns_false_when_reload_failed() { let _guard = crate::storage::lock_test_env(); let _env = ReloadTestEnv::new(); crate::server::write_reload_state( "reload-test", "hash", crate::server::ReloadPhase::Failed, Some("boom".to_string()), ); assert!(!wait_for_reloading_server().await); } #[tokio::test] async fn wait_for_reloading_server_returns_true_for_live_listener() { let _guard = crate::storage::lock_test_env(); let env = ReloadTestEnv::new(); let _listener = Listener::bind(&env.socket_path).expect("bind listener"); assert!(wait_for_reloading_server().await); } #[tokio::test] async fn server_is_running_at_treats_live_listener_as_running_without_pong() { let temp = tempfile::tempdir().expect("tempdir"); let socket_path = temp.path().join("jcode.sock"); let _listener = Listener::bind(&socket_path).expect("bind listener"); assert!( server_is_running_at(&socket_path).await, "a live listener should prevent duplicate server spawns even if ping is slow or absent" ); }