mirror of
https://github.com/openai/codex.git
synced 2026-04-26 15:45:02 +00:00
Exposes through the app server updated names set for a thread. This enables other surfaces to use the core as the source of truth for thread naming. `threadName` is gathered using the helper functions used to interact with `session_index.jsonl`, and is hydrated in: - `thread/list` - `thread/read` - `thread/resume` - `thread/unarchive` - `thread/rollback` We don't do this for `thread/start` and `thread/fork`.
199 lines
6.8 KiB
Rust
199 lines
6.8 KiB
Rust
use anyhow::Result;
|
|
use app_test_support::McpProcess;
|
|
use app_test_support::create_mock_responses_server_repeating_assistant;
|
|
use app_test_support::to_response;
|
|
use codex_app_server_protocol::JSONRPCResponse;
|
|
use codex_app_server_protocol::RequestId;
|
|
use codex_app_server_protocol::ThreadArchiveParams;
|
|
use codex_app_server_protocol::ThreadArchiveResponse;
|
|
use codex_app_server_protocol::ThreadStartParams;
|
|
use codex_app_server_protocol::ThreadStartResponse;
|
|
use codex_app_server_protocol::ThreadStatus;
|
|
use codex_app_server_protocol::ThreadUnarchiveParams;
|
|
use codex_app_server_protocol::ThreadUnarchiveResponse;
|
|
use codex_app_server_protocol::ThreadUnarchivedNotification;
|
|
use codex_app_server_protocol::TurnStartParams;
|
|
use codex_app_server_protocol::TurnStartResponse;
|
|
use codex_app_server_protocol::UserInput;
|
|
use codex_core::find_archived_thread_path_by_id_str;
|
|
use codex_core::find_thread_path_by_id_str;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::Value;
|
|
use std::fs::FileTimes;
|
|
use std::fs::OpenOptions;
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
use std::time::SystemTime;
|
|
use tempfile::TempDir;
|
|
use tokio::time::timeout;
|
|
|
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
|
|
|
#[tokio::test]
|
|
async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> 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 mut mcp = McpProcess::new(codex_home.path()).await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let start_id = mcp
|
|
.send_thread_start_request(ThreadStartParams {
|
|
model: Some("mock-model".to_string()),
|
|
..Default::default()
|
|
})
|
|
.await?;
|
|
let start_resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
|
)
|
|
.await??;
|
|
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
|
|
|
let rollout_path = thread.path.clone().expect("thread path");
|
|
|
|
let turn_start_id = mcp
|
|
.send_turn_start_request(TurnStartParams {
|
|
thread_id: thread.id.clone(),
|
|
input: vec![UserInput::Text {
|
|
text: "materialize".to_string(),
|
|
text_elements: Vec::new(),
|
|
}],
|
|
..Default::default()
|
|
})
|
|
.await?;
|
|
let turn_start_response: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(turn_start_id)),
|
|
)
|
|
.await??;
|
|
let _: TurnStartResponse = to_response::<TurnStartResponse>(turn_start_response)?;
|
|
timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_notification_message("turn/completed"),
|
|
)
|
|
.await??;
|
|
|
|
let found_rollout_path = find_thread_path_by_id_str(codex_home.path(), &thread.id)
|
|
.await?
|
|
.expect("expected rollout path for thread id to exist");
|
|
assert_paths_match_on_disk(&found_rollout_path, &rollout_path)?;
|
|
|
|
let archive_id = mcp
|
|
.send_thread_archive_request(ThreadArchiveParams {
|
|
thread_id: thread.id.clone(),
|
|
})
|
|
.await?;
|
|
let archive_resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(archive_id)),
|
|
)
|
|
.await??;
|
|
let _: ThreadArchiveResponse = to_response::<ThreadArchiveResponse>(archive_resp)?;
|
|
|
|
let archived_path = find_archived_thread_path_by_id_str(codex_home.path(), &thread.id)
|
|
.await?
|
|
.expect("expected archived rollout path for thread id to exist");
|
|
let archived_path_display = archived_path.display();
|
|
assert!(
|
|
archived_path.exists(),
|
|
"expected {archived_path_display} to exist"
|
|
);
|
|
let old_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
|
|
let old_timestamp = old_time
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
.expect("old timestamp")
|
|
.as_secs() as i64;
|
|
let times = FileTimes::new().set_modified(old_time);
|
|
OpenOptions::new()
|
|
.append(true)
|
|
.open(&archived_path)?
|
|
.set_times(times)?;
|
|
|
|
let unarchive_id = mcp
|
|
.send_thread_unarchive_request(ThreadUnarchiveParams {
|
|
thread_id: thread.id.clone(),
|
|
})
|
|
.await?;
|
|
let unarchive_resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(unarchive_id)),
|
|
)
|
|
.await??;
|
|
let unarchive_result = unarchive_resp.result.clone();
|
|
let ThreadUnarchiveResponse {
|
|
thread: unarchived_thread,
|
|
} = to_response::<ThreadUnarchiveResponse>(unarchive_resp)?;
|
|
let unarchive_notification = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_notification_message("thread/unarchived"),
|
|
)
|
|
.await??;
|
|
let unarchived_notification: ThreadUnarchivedNotification = serde_json::from_value(
|
|
unarchive_notification
|
|
.params
|
|
.expect("thread/unarchived notification params"),
|
|
)?;
|
|
assert_eq!(unarchived_notification.thread_id, thread.id);
|
|
assert!(
|
|
unarchived_thread.updated_at > old_timestamp,
|
|
"expected updated_at to be bumped on unarchive"
|
|
);
|
|
assert_eq!(unarchived_thread.status, ThreadStatus::NotLoaded);
|
|
|
|
// Wire contract: thread title field is `name`, serialized as null when unset.
|
|
let thread_json = unarchive_result
|
|
.get("thread")
|
|
.and_then(Value::as_object)
|
|
.expect("thread/unarchive result.thread must be an object");
|
|
assert_eq!(unarchived_thread.name, None);
|
|
assert_eq!(
|
|
thread_json.get("name"),
|
|
Some(&Value::Null),
|
|
"thread/unarchive must serialize `name: null` when unset"
|
|
);
|
|
|
|
let rollout_path_display = rollout_path.display();
|
|
assert!(
|
|
rollout_path.exists(),
|
|
"expected rollout path {rollout_path_display} to be restored"
|
|
);
|
|
assert!(
|
|
!archived_path.exists(),
|
|
"expected archived rollout path {archived_path_display} to be moved"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
|
let config_toml = codex_home.join("config.toml");
|
|
std::fs::write(config_toml, config_contents(server_uri))
|
|
}
|
|
|
|
fn config_contents(server_uri: &str) -> String {
|
|
format!(
|
|
r#"model = "mock-model"
|
|
approval_policy = "never"
|
|
sandbox_mode = "read-only"
|
|
|
|
model_provider = "mock_provider"
|
|
|
|
[model_providers.mock_provider]
|
|
name = "Mock provider for test"
|
|
base_url = "{server_uri}/v1"
|
|
wire_api = "responses"
|
|
request_max_retries = 0
|
|
stream_max_retries = 0
|
|
"#
|
|
)
|
|
}
|
|
|
|
fn assert_paths_match_on_disk(actual: &Path, expected: &Path) -> std::io::Result<()> {
|
|
let actual = actual.canonicalize()?;
|
|
let expected = expected.canonicalize()?;
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|