diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index d8ca3aa696..ac3442fd3d 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -2,7 +2,9 @@ use anyhow::Result; use app_test_support::McpProcess; use app_test_support::create_fake_rollout_with_text_elements; use app_test_support::create_mock_responses_server_repeating_assistant; +use app_test_support::rollout_path; use app_test_support::to_response; +use chrono::Utc; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SessionSource; @@ -14,6 +16,7 @@ use codex_app_server_protocol::UserInput; use codex_protocol::user_input::ByteRange; use codex_protocol::user_input::TextElement; use pretty_assertions::assert_eq; +use std::fs::FileTimes; use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; @@ -134,6 +137,57 @@ async fn thread_read_can_include_turns() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_read_does_not_change_updated_at_or_mtime() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let preview = "Saved user message"; + let filename_ts = "2025-01-05T12-00-00"; + let meta_rfc3339 = "2025-01-05T12:00:00Z"; + let expected_updated_at_rfc3339 = "2025-01-07T00:00:00Z"; + let conversation_id = create_fake_rollout_with_text_elements( + codex_home.path(), + filename_ts, + meta_rfc3339, + preview, + Vec::new(), + Some("mock_provider"), + None, + )?; + let rollout_file_path = rollout_path(codex_home.path(), filename_ts, &conversation_id); + set_rollout_mtime(rollout_file_path.as_path(), expected_updated_at_rfc3339)?; + + let before_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let read_id = mcp + .send_thread_read_request(ThreadReadParams { + thread_id: conversation_id, + include_turns: false, + }) + .await?; + let read_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(read_id)), + ) + .await??; + let ThreadReadResponse { thread } = to_response::(read_resp)?; + + let expected_updated_at = chrono::DateTime::parse_from_rfc3339(expected_updated_at_rfc3339)? + .with_timezone(&Utc) + .timestamp(); + assert_eq!(thread.updated_at, expected_updated_at); + + let after_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + assert_eq!(after_modified, before_modified); + + Ok(()) +} + // Helper to create a config.toml pointing at the mock model server. fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); @@ -157,3 +211,13 @@ stream_max_retries = 0 ), ) } + +fn set_rollout_mtime(path: &Path, updated_at_rfc3339: &str) -> Result<()> { + let parsed = chrono::DateTime::parse_from_rfc3339(updated_at_rfc3339)?.with_timezone(&Utc); + let times = FileTimes::new().set_modified(parsed.into()); + std::fs::OpenOptions::new() + .append(true) + .open(path)? + .set_times(times)?; + Ok(()) +}