use crate::test_support::*; /// Test that multi-turn conversation works with session resume #[tokio::test] async fn test_multi_turn_conversation() -> Result<()> { let _env = setup_test_env()?; let provider = MockProvider::new(); // First turn response provider.queue_response(vec![ StreamEvent::TextDelta("I'll remember that.".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, StreamEvent::SessionId("session-abc".to_string()), ]); // Second turn response provider.queue_response(vec![ StreamEvent::TextDelta("You said hello earlier.".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, StreamEvent::SessionId("session-abc".to_string()), ]); let provider: Arc = Arc::new(provider); let registry = Registry::new(provider.clone()).await; let mut agent = Agent::new(provider, registry); // First turn let response1 = agent.run_once_capture("Hello").await?; assert_eq!(response1, "I'll remember that."); // Second turn - should use session resume let response2 = agent.run_once_capture("What did I say?").await?; assert_eq!(response2, "You said hello earlier."); Ok(()) } /// Test that token usage is tracked #[tokio::test] async fn test_token_usage() -> Result<()> { let _env = setup_test_env()?; let provider = MockProvider::new(); provider.queue_response(vec![ StreamEvent::TokenUsage { input_tokens: Some(10), output_tokens: Some(20), cache_read_input_tokens: None, cache_creation_input_tokens: None, }, StreamEvent::TextDelta("Response".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, StreamEvent::SessionId("session-123".to_string()), ]); let provider: Arc = Arc::new(provider); let registry = Registry::new(provider.clone()).await; let mut agent = Agent::new(provider, registry); let response = agent.run_once_capture("Test").await?; assert_eq!(response, "Response"); Ok(()) } /// Test error handling #[tokio::test] async fn test_stream_error() -> Result<()> { let _env = setup_test_env()?; let provider = MockProvider::new(); provider.queue_response(vec![ StreamEvent::TextDelta("Starting...".to_string()), StreamEvent::Error { message: "Something went wrong".to_string(), retry_after_secs: None, }, ]); let provider: Arc = Arc::new(provider); let registry = Registry::new(provider.clone()).await; let mut agent = Agent::new(provider, registry); let result = agent.run_once_capture("Test").await; assert!(result.is_err()); assert!( result .unwrap_err() .to_string() .contains("Something went wrong") ); Ok(()) } /// Test model cycling over the socket interface (server + client) #[tokio::test] async fn test_socket_model_cycle_supported_models() -> Result<()> { let _env = setup_test_env()?; let runtime_dir = short_runtime_dir(format!( "jcode-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&runtime_dir)?; let socket_path = runtime_dir.join("jcode.sock"); let debug_socket_path = runtime_dir.join("jcode-debug.sock"); let provider = MockProvider::with_models(vec!["gpt-5.2-codex", "claude-opus-4-5-20251101"]); let provider: Arc = Arc::new(provider); let server_instance = server::Server::new_with_paths(provider, socket_path.clone(), debug_socket_path.clone()); let server_handle = tokio::spawn(async move { server_instance.run().await }); let mut client = wait_for_server_client(&socket_path).await?; let request_id = client.cycle_model(1).await?; let mut saw_model_changed = false; let deadline = Instant::now() + Duration::from_secs(2); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await??; match event { ServerEvent::Ack { .. } => continue, ServerEvent::ModelChanged { id, model, error, .. } if id == request_id => { assert!(error.is_none(), "Expected successful model change"); assert_eq!(model, "claude-opus-4-5-20251101"); saw_model_changed = true; break; } _ => {} } } abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path); assert!(saw_model_changed, "Did not receive model_changed event"); Ok(()) } /// Test that resume restores model selection and tool output in history #[tokio::test] async fn test_resume_restores_model_and_tool_history() -> Result<()> { let _env = setup_test_env()?; let runtime_dir = short_runtime_dir(format!( "jcode-resume-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&runtime_dir)?; let mut session = Session::create(None, Some("Resume Test".to_string())); session.model = Some("gpt-5.2-codex".to_string()); session.add_message( jcode::message::Role::User, vec![jcode::message::ContentBlock::Text { text: "Run a tool".to_string(), cache_control: None, }], ); session.add_message( jcode::message::Role::Assistant, vec![ jcode::message::ContentBlock::Text { text: "Running...".to_string(), cache_control: None, }, jcode::message::ContentBlock::ToolUse { id: "tool-1".to_string(), name: "bash".to_string(), input: serde_json::json!({"cmd": "echo hi"}), thought_signature: None, }, ], ); session.add_message( jcode::message::Role::User, vec![jcode::message::ContentBlock::ToolResult { tool_use_id: "tool-1".to_string(), content: "hi\n".to_string(), is_error: None, }], ); session.save()?; let socket_path = runtime_dir.join("jcode.sock"); let debug_socket_path = runtime_dir.join("jcode-debug.sock"); // Default model = claude, resume should switch to gpt-5.2-codex let provider = MockProvider::with_models(vec!["claude-opus-4-5-20251101", "gpt-5.2-codex"]); let provider: Arc = Arc::new(provider); let server_instance = server::Server::new_with_paths(provider, socket_path.clone(), debug_socket_path.clone()); let server_handle = tokio::spawn(async move { server_instance.run().await }); let mut client = wait_for_server_client(&socket_path).await?; let resume_id = client.resume_session(&session.id).await?; let mut history_event = None; let deadline = Instant::now() + Duration::from_secs(2); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await??; match event { ServerEvent::History { id, messages, provider_model, .. } if id == resume_id => { history_event = Some((messages, provider_model)); break; } _ => {} } } abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path); let (messages, provider_model) = history_event.ok_or_else(|| anyhow::anyhow!("Did not receive history event"))?; assert_eq!(provider_model, Some("gpt-5.2-codex".to_string())); let tool_msg = messages .iter() .find(|m| m.role == "tool") .ok_or_else(|| anyhow::anyhow!("Tool message missing in history"))?; assert!(tool_msg.content.contains("hi")); let tool_data = tool_msg .tool_data .as_ref() .ok_or_else(|| anyhow::anyhow!("Tool metadata missing in history"))?; assert_eq!(tool_data.name, "bash"); Ok(()) } /// Test that subscribe selfdev hint marks the session as canary #[tokio::test] async fn test_resume_session_with_local_history_uses_metadata_only_history() -> Result<()> { let _env = setup_test_env()?; let runtime_dir = short_runtime_dir(format!( "jcode-target-subscribe-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&runtime_dir)?; let mut session = Session::create(None, Some("Target Subscribe Test".to_string())); session.model = Some("model-a".to_string()); session.provider_session_id = Some("provider-resume-123".to_string()); session.add_message( jcode::message::Role::User, vec![jcode::message::ContentBlock::Text { text: "Existing local history".to_string(), cache_control: None, }], ); session.add_message( jcode::message::Role::Assistant, vec![jcode::message::ContentBlock::Text { text: "Existing assistant response".to_string(), cache_control: None, }], ); session.save()?; let socket_path = runtime_dir.join("jcode.sock"); let debug_socket_path = runtime_dir.join("jcode-debug.sock"); let provider = Arc::new(MockProvider::with_models(vec!["model-a"])); provider.queue_response(vec![ StreamEvent::TextDelta("resumed ok".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, ]); let provider_dyn: Arc = provider.clone(); let server_instance = server::Server::new_with_paths( provider_dyn, socket_path.clone(), debug_socket_path.clone(), ); let server_handle = tokio::spawn(async move { server_instance.run().await }); let mut client = wait_for_server_client(&socket_path).await?; let subscribe_id = client.subscribe().await?; let mut saw_done = false; let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await??; match event { ServerEvent::Done { id } if id == subscribe_id => { saw_done = true; break; } _ => {} } } assert!(saw_done, "Did not receive subscribe done event"); let resume_id = client .resume_session_with_options(&session.id, true, false) .await?; let mut history_checked = false; let mut resume_done = false; let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline && !(history_checked && resume_done) { let event = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await??; match event { ServerEvent::Ack { .. } => continue, ServerEvent::History { id, session_id, messages, provider_model, .. } if id == resume_id => { assert_eq!(session_id, session.id); assert_eq!( messages.len(), 2, "resume history should include persisted local messages" ); assert_eq!(messages[0].role, "user"); assert!(messages[0].content.contains("Existing local history")); assert_eq!(messages[1].role, "assistant"); assert!( messages[1].content.contains("Existing assistant response"), "resume history should include persisted assistant response" ); assert_eq!(provider_model, Some("model-a".to_string())); history_checked = true; } ServerEvent::Done { id } if id == resume_id => { resume_done = true; } ServerEvent::Error { id, message, .. } if id == resume_id => { anyhow::bail!("resume_session failed: {}", message); } _ => {} } } assert!(history_checked, "Did not receive resume history event"); assert!(resume_done, "Did not receive resume done event"); let msg_id = client.send_message("continue resumed session").await?; let deadline = Instant::now() + Duration::from_secs(5); let mut saw_message_done = false; let mut seen_events = Vec::new(); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await??; seen_events.push(format!("{event:?}")); if matches!(event, ServerEvent::Done { id } if id == msg_id) { saw_message_done = true; break; } } assert!( saw_message_done, "Did not receive Done for resumed message. Seen events: {}\nstate={}\nhistory={}\nlogs={}", seen_events.join(" | "), debug_run_command(debug_socket_path.clone(), "state", Some(&session.id)) .await .unwrap_or_else(|err| format!("")), debug_run_command(debug_socket_path.clone(), "history", Some(&session.id)) .await .unwrap_or_else(|err| format!("")), std::env::var_os("JCODE_HOME") .and_then(|home| latest_log_excerpt(std::path::Path::new(&home))) .unwrap_or_else(|| "".to_string()) ); let resume_ids = provider.captured_resume_session_ids.lock().unwrap().clone(); assert_eq!( resume_ids.last().cloned(), Some(Some("provider-resume-123".to_string())) ); abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path); Ok(()) } /// End-to-end: resume_all_sessions continues a live session whose last /// visible turn is an unanswered (interrupted) user message, and reports a /// summary describing the resumed session. #[tokio::test] async fn test_resume_all_sessions_continues_interrupted_live_session() -> Result<()> { let _env = setup_test_env()?; let runtime_dir = short_runtime_dir(format!( "jcode-resume-all-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&runtime_dir)?; // A session left with a pending user turn the assistant never answered. let mut session = Session::create(None, Some("Interrupted Session".to_string())); session.model = Some("model-a".to_string()); session.add_message( jcode::message::Role::User, vec![jcode::message::ContentBlock::Text { text: "keep going on the migration".to_string(), cache_control: None, }], ); session.save()?; let socket_path = runtime_dir.join("jcode.sock"); let debug_socket_path = runtime_dir.join("jcode-debug.sock"); let provider = MockProvider::with_models(vec!["model-a"]); provider.queue_response(vec![ StreamEvent::TextDelta("Continuing the migration now.".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, ]); let provider: Arc = Arc::new(provider); let server_instance = server::Server::new_with_paths(provider, socket_path.clone(), debug_socket_path.clone()); let server_handle = tokio::spawn(async move { server_instance.run().await }); let mut client = wait_for_server_client(&socket_path).await?; let subscribe_id = client.subscribe().await?; let _ = collect_until_done_unix(&mut client, subscribe_id).await?; // Make the interrupted session live by attaching this client to it. let resume_id = client.resume_session(&session.id).await?; let _ = collect_until_done_unix(&mut client, resume_id).await?; // Ask the server to continue every interrupted live session. let resume_all_id = client.resume_all_sessions().await?; let mut saw_continuation = false; let mut resume_all_result: Option<(usize, usize)> = None; let deadline = Instant::now() + Duration::from_secs(10); while Instant::now() < deadline && (!saw_continuation || resume_all_result.is_none()) { let event = tokio::time::timeout(Duration::from_secs(2), client.read_event()).await??; match event { ServerEvent::TextDelta { text } if text.contains("Continuing the migration now.") => { saw_continuation = true; } ServerEvent::ResumeAllResult { id, resumed, skipped, .. } if id == resume_all_id => { resume_all_result = Some((resumed, skipped)); } _ => {} } } abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path); assert!( saw_continuation, "interrupted live session should stream a continuation after resume_all_sessions" ); let (resumed, skipped) = resume_all_result.ok_or_else(|| anyhow::anyhow!("did not receive ResumeAllResult"))?; assert_eq!(resumed, 1, "exactly one interrupted session should resume"); assert_eq!(skipped, 0, "no live session should be skipped"); Ok(()) } #[tokio::test] async fn test_resume_session_reports_reload_interruption_for_peer_sessions() -> Result<()> { let _env = setup_test_env()?; let runtime_dir = short_runtime_dir(format!( "jcode-reload-interruption-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&runtime_dir)?; let mut session = Session::create(None, Some("Reload Interrupted Session".to_string())); session.model = Some("model-a".to_string()); session.add_message( jcode::message::Role::User, vec![jcode::message::ContentBlock::ToolResult { tool_use_id: "tool_bash_1".to_string(), content: "[Tool 'bash' interrupted by server reload after 0.2s]".to_string(), is_error: Some(true), }], ); session.save()?; let socket_path = runtime_dir.join("jcode.sock"); let debug_socket_path = runtime_dir.join("jcode-debug.sock"); let provider = Arc::new(MockProvider::with_models(vec!["model-a"])); let provider_dyn: Arc = provider.clone(); let server_instance = server::Server::new_with_paths( provider_dyn, socket_path.clone(), debug_socket_path.clone(), ); let server_handle = tokio::spawn(async move { server_instance.run().await }); let mut client = wait_for_server_client(&socket_path).await?; let subscribe_id = client.subscribe().await?; let subscribe_events = collect_until_done_unix(&mut client, subscribe_id).await?; assert!( subscribe_events .iter() .any(|event| matches!(event, ServerEvent::Done { id } if *id == subscribe_id)), "expected subscribe to finish, got: {subscribe_events:?}" ); let resume_id = client.resume_session(&session.id).await?; let events = collect_until_done_unix(&mut client, resume_id).await?; let history_event = events .iter() .find_map(|event| match event { ServerEvent::History { id, session_id, was_interrupted, .. } if *id == resume_id => Some((session_id.clone(), *was_interrupted)), _ => None, }) .ok_or_else(|| anyhow::anyhow!("Did not receive resume history event: {events:?}"))?; assert_eq!(history_event.0, session.id); assert_eq!( history_event.1, Some(true), "reload-interrupted peer sessions should be marked interrupted so clients auto-continue" ); abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path); Ok(()) } /// Test that subscribe selfdev hint marks the session as canary #[tokio::test] async fn test_subscribe_selfdev_hint_marks_canary() -> Result<()> { let _env = setup_test_env()?; let runtime_dir = short_runtime_dir(format!( "jcode-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&runtime_dir)?; let socket_path = runtime_dir.join("jcode.sock"); let debug_socket_path = runtime_dir.join("jcode-debug.sock"); let provider = MockProvider::new(); let provider: Arc = Arc::new(provider); let server_instance = server::Server::new_with_paths(provider, socket_path.clone(), debug_socket_path.clone()); let server_handle = tokio::spawn(async move { server_instance.run().await }); let mut client = wait_for_server_client(&socket_path).await?; let subscribe_id = client .subscribe_with_info(None, Some(true), None, false, false) .await?; let deadline = Instant::now() + Duration::from_secs(2); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await??; if matches!(event, ServerEvent::Done { id } if id == subscribe_id) { break; } } let history_event = client.get_history_event().await?; match history_event { ServerEvent::History { is_canary, .. } => { assert_eq!(is_canary, Some(true)); } _ => anyhow::bail!("Expected history event after subscribe"), } abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path); Ok(()) } /// Test that working_dir alone no longer upgrades a session to self-dev. #[tokio::test] async fn test_subscribe_working_dir_without_selfdev_hint_stays_normal() -> Result<()> { let _env = setup_test_env()?; let runtime_dir = short_runtime_dir(format!( "jcode-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&runtime_dir)?; let socket_path = runtime_dir.join("jcode.sock"); let debug_socket_path = runtime_dir.join("jcode-debug.sock"); let fake_repo = tempfile::tempdir()?; std::fs::create_dir_all(fake_repo.path().join(".git"))?; std::fs::write( fake_repo.path().join("Cargo.toml"), "[package]\nname = \"jcode\"\nversion = \"0.0.0\"\n", )?; let nested_dir = fake_repo.path().join("nested").join("worktree"); std::fs::create_dir_all(&nested_dir)?; let provider = MockProvider::new(); let provider: Arc = Arc::new(provider); let server_instance = server::Server::new_with_paths(provider, socket_path.clone(), debug_socket_path.clone()); let server_handle = tokio::spawn(async move { server_instance.run().await }); let mut client = wait_for_server_client(&socket_path).await?; let subscribe_id = client .subscribe_with_info( Some(nested_dir.display().to_string()), None, None, false, false, ) .await?; let deadline = Instant::now() + Duration::from_secs(2); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await??; if matches!(event, ServerEvent::Done { id } if id == subscribe_id) { break; } } let history_event = client.get_history_event().await?; match history_event { ServerEvent::History { is_canary, .. } => { assert_eq!(is_canary, Some(false)); } _ => anyhow::bail!("Expected history event after subscribe"), } abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path); Ok(()) } /// Test that switching models resets the provider resume session #[tokio::test] async fn test_model_switch_resets_provider_session() -> Result<()> { let _env = setup_test_env()?; let runtime_dir = short_runtime_dir(format!( "jcode-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&runtime_dir)?; let socket_path = runtime_dir.join("jcode.sock"); let debug_socket_path = runtime_dir.join("jcode-debug.sock"); let provider = Arc::new(MockProvider::with_models(vec!["model-a", "model-b"])); provider.queue_response(vec![ StreamEvent::TextDelta("hello".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, StreamEvent::SessionId("session-1".to_string()), ]); provider.queue_response(vec![ StreamEvent::TextDelta("again".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, ]); let provider_dyn: Arc = provider.clone(); let server_instance = server::Server::new_with_paths( provider_dyn, socket_path.clone(), debug_socket_path.clone(), ); let server_handle = tokio::spawn(async move { server_instance.run().await }); let mut client = wait_for_server_client(&socket_path).await?; let msg_id = client.send_message("hello").await?; let mut saw_done1 = false; // First-turn Done includes full agent-loop startup; slow Windows CI // runners intermittently exceeded the old 5s budget (CI flake). Keep the // loop tolerant of individual read timeouts and bound only the total wait. let deadline = Instant::now() + Duration::from_secs(30); while Instant::now() < deadline { let Ok(event) = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await else { continue; }; if matches!(event?, ServerEvent::Done { id } if id == msg_id) { saw_done1 = true; break; } } assert!(saw_done1, "Did not receive Done for first message"); let model_id = client.cycle_model(1).await?; let mut saw_model = false; let deadline = Instant::now() + Duration::from_secs(30); while Instant::now() < deadline { let Ok(event) = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await else { continue; }; if matches!(event?, ServerEvent::ModelChanged { id, error: None, .. } if id == model_id) { saw_model = true; break; } } assert!(saw_model, "Did not receive ModelChanged after cycle"); let msg2_id = client.send_message("second").await?; let mut saw_done2 = false; let deadline = Instant::now() + Duration::from_secs(30); while Instant::now() < deadline { let Ok(event) = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await else { continue; }; if matches!(event?, ServerEvent::Done { id } if id == msg2_id) { saw_done2 = true; break; } } assert!(saw_done2, "Did not receive Done for second message"); let resume_ids = provider.captured_resume_session_ids.lock().unwrap().clone(); assert_eq!(resume_ids.len(), 2); assert_eq!(resume_ids[0], None); assert_eq!(resume_ids[1], None); abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path); Ok(()) } /// Test that switching models only affects the active session #[tokio::test] async fn test_model_switch_is_per_session() -> Result<()> { let _env = setup_test_env()?; let runtime_dir = short_runtime_dir(format!( "jcode-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&runtime_dir)?; let socket_path = runtime_dir.join("jcode.sock"); let debug_socket_path = runtime_dir.join("jcode-debug.sock"); let provider = Arc::new(MockProvider::with_models(vec!["model-a", "model-b"])); provider.queue_response(vec![ StreamEvent::TextDelta("one".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, StreamEvent::SessionId("session-1".to_string()), ]); provider.queue_response(vec![ StreamEvent::TextDelta("two".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, StreamEvent::SessionId("session-2".to_string()), ]); provider.queue_response(vec![ StreamEvent::TextDelta("three".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, ]); let provider_dyn: Arc = provider.clone(); let server_instance = server::Server::new_with_paths( provider_dyn, socket_path.clone(), debug_socket_path.clone(), ); let server_handle = tokio::spawn(async move { server_instance.run().await }); let mut client1 = wait_for_server_client(&socket_path).await?; let mut client2 = server::Client::connect_with_path(socket_path.clone()).await?; // Give server time to set up both client sessions tokio::time::sleep(Duration::from_millis(100)).await; let msg1 = client1.send_message("hello").await?; let mut done1 = false; let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client1.read_event()).await??; if matches!(event, ServerEvent::Done { id } if id == msg1) { done1 = true; break; } } assert!(done1, "Did not receive Done for client1 message"); let msg2 = client2.send_message("hello").await?; let mut done2 = false; let deadline = Instant::now() + Duration::from_secs(2); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client2.read_event()).await??; if matches!(event, ServerEvent::Done { id } if id == msg2) { done2 = true; break; } } assert!(done2, "Did not receive Done for client2 message"); let model_id = client1.cycle_model(1).await?; let mut saw_model = false; let deadline = Instant::now() + Duration::from_secs(2); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client1.read_event()).await??; if matches!(event, ServerEvent::ModelChanged { id, error: None, .. } if id == model_id) { saw_model = true; break; } } assert!(saw_model, "Did not receive ModelChanged after cycle"); let msg3 = client2.send_message("after").await?; let mut done3 = false; let deadline = Instant::now() + Duration::from_secs(2); while Instant::now() < deadline { let event = tokio::time::timeout(Duration::from_secs(1), client2.read_event()).await??; if matches!(event, ServerEvent::Done { id } if id == msg3) { done3 = true; break; } } assert!(done3, "Did not receive Done for client2 after switch"); let models = provider.captured_models.lock().unwrap().clone(); assert!(models.len() >= 3, "Expected at least 3 model captures"); assert_eq!(models[2], "model-a"); abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path); Ok(()) } /// Test that the system prompt does NOT identify the agent as "Claude Code" /// The agent should identify as "jcode" or just a generic "coding assistant powered by Claude" #[tokio::test] async fn test_system_prompt_no_claude_code_identity() -> Result<()> { let _env = setup_test_env()?; let provider = Arc::new(MockProvider::new()); // Queue a simple response provider.queue_response(vec![ StreamEvent::TextDelta("I'm a coding assistant.".to_string()), StreamEvent::MessageEnd { stop_reason: Some("end_turn".to_string()), }, StreamEvent::SessionId("test-identity-123".to_string()), ]); // Keep a clone of Arc before converting to Arc let provider_for_check = provider.clone(); let provider_dyn: Arc = provider; let registry = Registry::new(provider_dyn.clone()).await; let mut agent = Agent::new(provider_dyn, registry); // Run a simple query - we just need to trigger a complete() call let _ = agent.run_once_capture("Who are you?").await?; // Get the captured system prompt from our Arc let captured_prompts = provider_for_check.captured_system_prompts.lock().unwrap(); assert!( !captured_prompts.is_empty(), "No system prompts were captured" ); let system_prompt = &captured_prompts[0]; // Check only the identity portion at the start of the system prompt. // User-provided instruction files may legitimately mention Claude Code CLI. // The first ~500 chars contain the identity statement let identity_portion = if system_prompt.len() > 500 { &system_prompt[..500] } else { system_prompt }; let lower_identity = identity_portion.to_lowercase(); // The identity portion should NOT say "you are claude code" or similar assert!( !lower_identity.contains("you are claude code"), "System prompt should NOT identify as 'You are Claude Code'. Found: {}", identity_portion ); // Should identify as jcode assert!( lower_identity.contains("jcode"), "System prompt should identify as jcode. Found: {}", identity_portion ); // It's OK if it says "powered by Claude" or just "Claude" (the model name) // It's OK if project context references "Claude Code CLI" as a tool Ok(()) }