From 930dd42d9a6d0f8ea0b1505b1320e6e00794ac5a Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Mar 2026 22:37:02 -0700 Subject: [PATCH] codex: preserve search filtering on db fallback Co-authored-by: Codex --- .../app-server/tests/suite/v2/thread_list.rs | 65 +++++++++++++++++++ codex-rs/rollout/src/recorder.rs | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/codex-rs/app-server/tests/suite/v2/thread_list.rs b/codex-rs/app-server/tests/suite/v2/thread_list.rs index 5b5d0ee99b..93895ffa43 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_list.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_list.rs @@ -605,6 +605,71 @@ sqlite = true Ok(()) } +#[tokio::test] +async fn thread_list_keeps_empty_search_results_when_db_page_is_empty() -> Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("config.toml"), + r#" +model = "mock-model" +approval_policy = "never" +suppress_unstable_features_warning = true + +[features] +sqlite = true +"#, + )?; + + create_fake_rollout( + codex_home.path(), + "2025-01-02T10-00-00", + "2025-01-02T10:00:00Z", + "match: needle", + Some("mock_provider"), + None, + )?; + create_fake_rollout( + codex_home.path(), + "2025-01-02T11-00-00", + "2025-01-02T11:00:00Z", + "no hit here", + Some("mock_provider"), + None, + )?; + + let state_db = + codex_state::StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()) + .await?; + state_db.mark_backfill_complete(None).await?; + + let mut mcp = init_mcp(codex_home.path()).await?; + let request_id = mcp + .send_thread_list_request(codex_app_server_protocol::ThreadListParams { + cursor: None, + limit: Some(10), + sort_key: None, + model_providers: Some(vec!["mock_provider".to_string()]), + source_kinds: None, + archived: None, + cwd: None, + search_term: Some("absent".to_string()), + }) + .await?; + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let ThreadListResponse { + data, next_cursor, .. + } = to_response::(resp)?; + + assert_eq!(next_cursor, None); + assert!(data.is_empty()); + + Ok(()) +} + #[tokio::test] async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/rollout/src/recorder.rs b/codex-rs/rollout/src/recorder.rs index 0c281c337b..0cedc0f6f1 100644 --- a/codex-rs/rollout/src/recorder.rs +++ b/codex-rs/rollout/src/recorder.rs @@ -287,7 +287,7 @@ impl RolloutRecorder { ) .await { - if db_page.items.is_empty() && !fs_page.items.is_empty() { + if search_term.is_none() && db_page.items.is_empty() && !fs_page.items.is_empty() { tracing::warn!( "state db discrepancy during list_threads_with_db_fallback: empty_db_page" );