[app-server] feat: v2 Thread APIs (#6214)
Implements: ``` thread/list thread/start thread/resume thread/archive ``` along with their integration tests. These are relatively light wrappers around the existing core logic, and changes to core logic are minimal. However, an improvement made for developer ergonomics: - `thread/start` and `thread/resume` automatically attaches a conversation listener internally, so clients don't have to make a separate `AddConversationListener` call like they do today. For consistency, also updated `model/list` and `feedback/upload` (naming conventions, list API params).
This commit is contained in:
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -186,9 +186,11 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"codex-app-server-protocol",
|
"codex-app-server-protocol",
|
||||||
"codex-core",
|
"codex-core",
|
||||||
|
"codex-protocol",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"uuid",
|
||||||
"wiremock",
|
"wiremock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::JSONRPCNotification;
|
use crate::JSONRPCNotification;
|
||||||
@@ -101,11 +102,43 @@ macro_rules! client_request_definitions {
|
|||||||
|
|
||||||
client_request_definitions! {
|
client_request_definitions! {
|
||||||
/// NEW APIs
|
/// NEW APIs
|
||||||
|
// Thread lifecycle
|
||||||
|
#[serde(rename = "thread/start")]
|
||||||
|
#[ts(rename = "thread/start")]
|
||||||
|
ThreadStart {
|
||||||
|
params: v2::ThreadStartParams,
|
||||||
|
response: v2::ThreadStartResponse,
|
||||||
|
},
|
||||||
|
#[serde(rename = "thread/resume")]
|
||||||
|
#[ts(rename = "thread/resume")]
|
||||||
|
ThreadResume {
|
||||||
|
params: v2::ThreadResumeParams,
|
||||||
|
response: v2::ThreadResumeResponse,
|
||||||
|
},
|
||||||
|
#[serde(rename = "thread/archive")]
|
||||||
|
#[ts(rename = "thread/archive")]
|
||||||
|
ThreadArchive {
|
||||||
|
params: v2::ThreadArchiveParams,
|
||||||
|
response: v2::ThreadArchiveResponse,
|
||||||
|
},
|
||||||
|
#[serde(rename = "thread/list")]
|
||||||
|
#[ts(rename = "thread/list")]
|
||||||
|
ThreadList {
|
||||||
|
params: v2::ThreadListParams,
|
||||||
|
response: v2::ThreadListResponse,
|
||||||
|
},
|
||||||
|
#[serde(rename = "thread/compact")]
|
||||||
|
#[ts(rename = "thread/compact")]
|
||||||
|
ThreadCompact {
|
||||||
|
params: v2::ThreadCompactParams,
|
||||||
|
response: v2::ThreadCompactResponse,
|
||||||
|
},
|
||||||
|
|
||||||
#[serde(rename = "model/list")]
|
#[serde(rename = "model/list")]
|
||||||
#[ts(rename = "model/list")]
|
#[ts(rename = "model/list")]
|
||||||
ListModels {
|
ModelList {
|
||||||
params: v2::ListModelsParams,
|
params: v2::ModelListParams,
|
||||||
response: v2::ListModelsResponse,
|
response: v2::ModelListResponse,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[serde(rename = "account/login")]
|
#[serde(rename = "account/login")]
|
||||||
@@ -131,9 +164,9 @@ client_request_definitions! {
|
|||||||
|
|
||||||
#[serde(rename = "feedback/upload")]
|
#[serde(rename = "feedback/upload")]
|
||||||
#[ts(rename = "feedback/upload")]
|
#[ts(rename = "feedback/upload")]
|
||||||
UploadFeedback {
|
FeedbackUpload {
|
||||||
params: v2::UploadFeedbackParams,
|
params: v2::FeedbackUploadParams,
|
||||||
response: v2::UploadFeedbackResponse,
|
response: v2::FeedbackUploadResponse,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[serde(rename = "account/read")]
|
#[serde(rename = "account/read")]
|
||||||
@@ -292,7 +325,7 @@ macro_rules! server_request_definitions {
|
|||||||
|
|
||||||
#[allow(clippy::vec_init_then_push)]
|
#[allow(clippy::vec_init_then_push)]
|
||||||
pub fn export_server_response_schemas(
|
pub fn export_server_response_schemas(
|
||||||
out_dir: &::std::path::Path,
|
out_dir: &Path,
|
||||||
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
||||||
let mut schemas = Vec::new();
|
let mut schemas = Vec::new();
|
||||||
paste! {
|
paste! {
|
||||||
@@ -303,7 +336,7 @@ macro_rules! server_request_definitions {
|
|||||||
|
|
||||||
#[allow(clippy::vec_init_then_push)]
|
#[allow(clippy::vec_init_then_push)]
|
||||||
pub fn export_server_param_schemas(
|
pub fn export_server_param_schemas(
|
||||||
out_dir: &::std::path::Path,
|
out_dir: &Path,
|
||||||
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
) -> ::anyhow::Result<Vec<GeneratedSchema>> {
|
||||||
let mut schemas = Vec::new();
|
let mut schemas = Vec::new();
|
||||||
paste! {
|
paste! {
|
||||||
@@ -741,16 +774,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_list_models() -> Result<()> {
|
fn serialize_list_models() -> Result<()> {
|
||||||
let request = ClientRequest::ListModels {
|
let request = ClientRequest::ModelList {
|
||||||
request_id: RequestId::Integer(6),
|
request_id: RequestId::Integer(6),
|
||||||
params: v2::ListModelsParams::default(),
|
params: v2::ModelListParams::default(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
json!({
|
json!({
|
||||||
"method": "model/list",
|
"method": "model/list",
|
||||||
"id": 6,
|
"id": 6,
|
||||||
"params": {
|
"params": {
|
||||||
"pageSize": null,
|
"limit": null,
|
||||||
"cursor": null
|
"cursor": null
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::protocol::common::AuthMode;
|
use crate::protocol::common::AuthMode;
|
||||||
use codex_protocol::ConversationId;
|
use codex_protocol::ConversationId;
|
||||||
use codex_protocol::account::PlanType;
|
use codex_protocol::account::PlanType;
|
||||||
@@ -9,10 +12,109 @@ use schemars::JsonSchema;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
use std::path::PathBuf;
|
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// Macro to declare a camelCased API v2 enum mirroring a core enum which
|
||||||
|
// tends to use kebab-case.
|
||||||
|
macro_rules! v2_enum_from_core {
|
||||||
|
(
|
||||||
|
pub enum $Name:ident from $Src:path { $( $Variant:ident ),+ $(,)? }
|
||||||
|
) => {
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum $Name { $( $Variant ),+ }
|
||||||
|
|
||||||
|
impl $Name {
|
||||||
|
pub fn to_core(self) -> $Src {
|
||||||
|
match self { $( $Name::$Variant => <$Src>::$Variant ),+ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$Src> for $Name {
|
||||||
|
fn from(value: $Src) -> Self {
|
||||||
|
match value { $( <$Src>::$Variant => $Name::$Variant ),+ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
v2_enum_from_core!(
|
||||||
|
pub enum AskForApproval from codex_protocol::protocol::AskForApproval {
|
||||||
|
UnlessTrusted, OnFailure, OnRequest, Never
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
v2_enum_from_core!(
|
||||||
|
pub enum SandboxMode from codex_protocol::config_types::SandboxMode {
|
||||||
|
ReadOnly, WorkspaceWrite, DangerFullAccess
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(tag = "mode", rename_all = "camelCase")]
|
||||||
|
#[ts(tag = "mode")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum SandboxPolicy {
|
||||||
|
DangerFullAccess,
|
||||||
|
ReadOnly,
|
||||||
|
WorkspaceWrite {
|
||||||
|
#[serde(default)]
|
||||||
|
writable_roots: Vec<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
network_access: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
exclude_tmpdir_env_var: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
exclude_slash_tmp: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SandboxPolicy {
|
||||||
|
pub fn to_core(&self) -> codex_protocol::protocol::SandboxPolicy {
|
||||||
|
match self {
|
||||||
|
SandboxPolicy::DangerFullAccess => {
|
||||||
|
codex_protocol::protocol::SandboxPolicy::DangerFullAccess
|
||||||
|
}
|
||||||
|
SandboxPolicy::ReadOnly => codex_protocol::protocol::SandboxPolicy::ReadOnly,
|
||||||
|
SandboxPolicy::WorkspaceWrite {
|
||||||
|
writable_roots,
|
||||||
|
network_access,
|
||||||
|
exclude_tmpdir_env_var,
|
||||||
|
exclude_slash_tmp,
|
||||||
|
} => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
|
||||||
|
writable_roots: writable_roots.clone(),
|
||||||
|
network_access: *network_access,
|
||||||
|
exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
|
||||||
|
exclude_slash_tmp: *exclude_slash_tmp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {
|
||||||
|
fn from(value: codex_protocol::protocol::SandboxPolicy) -> Self {
|
||||||
|
match value {
|
||||||
|
codex_protocol::protocol::SandboxPolicy::DangerFullAccess => {
|
||||||
|
SandboxPolicy::DangerFullAccess
|
||||||
|
}
|
||||||
|
codex_protocol::protocol::SandboxPolicy::ReadOnly => SandboxPolicy::ReadOnly,
|
||||||
|
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
|
||||||
|
writable_roots,
|
||||||
|
network_access,
|
||||||
|
exclude_tmpdir_env_var,
|
||||||
|
exclude_slash_tmp,
|
||||||
|
} => SandboxPolicy::WorkspaceWrite {
|
||||||
|
writable_roots,
|
||||||
|
network_access,
|
||||||
|
exclude_tmpdir_env_var,
|
||||||
|
exclude_slash_tmp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
#[ts(tag = "type")]
|
#[ts(tag = "type")]
|
||||||
@@ -82,11 +184,11 @@ pub struct GetAccountResponse {
|
|||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
pub struct ListModelsParams {
|
pub struct ModelListParams {
|
||||||
/// Optional page size; defaults to a reasonable server-side value.
|
|
||||||
pub page_size: Option<usize>,
|
|
||||||
/// Opaque pagination cursor returned by a previous call.
|
/// Opaque pagination cursor returned by a previous call.
|
||||||
pub cursor: Option<String>,
|
pub cursor: Option<String>,
|
||||||
|
/// Optional page size; defaults to a reasonable server-side value.
|
||||||
|
pub limit: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
@@ -114,17 +216,17 @@ pub struct ReasoningEffortOption {
|
|||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
pub struct ListModelsResponse {
|
pub struct ModelListResponse {
|
||||||
pub items: Vec<Model>,
|
pub data: Vec<Model>,
|
||||||
/// Opaque cursor to pass to the next call to continue after the last item.
|
/// Opaque cursor to pass to the next call to continue after the last item.
|
||||||
/// if None, there are no more items to return.
|
/// If None, there are no more items to return.
|
||||||
pub next_cursor: Option<String>,
|
pub next_cursor: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
pub struct UploadFeedbackParams {
|
pub struct FeedbackUploadParams {
|
||||||
pub classification: String,
|
pub classification: String,
|
||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
pub conversation_id: Option<ConversationId>,
|
pub conversation_id: Option<ConversationId>,
|
||||||
@@ -134,10 +236,101 @@ pub struct UploadFeedbackParams {
|
|||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
pub struct UploadFeedbackResponse {
|
pub struct FeedbackUploadResponse {
|
||||||
pub thread_id: String,
|
pub thread_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Threads, Turns, and Items ===
|
||||||
|
// Thread APIs
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadStartParams {
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub model_provider: Option<String>,
|
||||||
|
pub cwd: Option<String>,
|
||||||
|
pub approval_policy: Option<AskForApproval>,
|
||||||
|
pub sandbox: Option<SandboxMode>,
|
||||||
|
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||||
|
pub base_instructions: Option<String>,
|
||||||
|
pub developer_instructions: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadStartResponse {
|
||||||
|
pub thread: Thread,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadResumeParams {
|
||||||
|
pub thread_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadResumeResponse {
|
||||||
|
pub thread: Thread,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadArchiveParams {
|
||||||
|
pub thread_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadArchiveResponse {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadListParams {
|
||||||
|
/// Opaque pagination cursor returned by a previous call.
|
||||||
|
pub cursor: Option<String>,
|
||||||
|
/// Optional page size; defaults to a reasonable server-side value.
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
/// Optional provider filter; when set, only sessions recorded under these
|
||||||
|
/// providers are returned. When present but empty, includes all providers.
|
||||||
|
pub model_providers: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadListResponse {
|
||||||
|
pub data: Vec<Thread>,
|
||||||
|
/// Opaque cursor to pass to the next call to continue after the last item.
|
||||||
|
/// if None, there are no more items to return.
|
||||||
|
pub next_cursor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadCompactParams {
|
||||||
|
pub thread_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadCompactResponse {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct Thread {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
@@ -145,13 +338,6 @@ pub struct AccountUpdatedNotification {
|
|||||||
pub auth_method: Option<AuthMode>,
|
pub auth_method: Option<AuthMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export_to = "v2/")]
|
|
||||||
pub struct Thread {
|
|
||||||
pub id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ base64 = { workspace = true }
|
|||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
codex-app-server-protocol = { workspace = true }
|
codex-app-server-protocol = { workspace = true }
|
||||||
codex-core = { workspace = true }
|
codex-core = { workspace = true }
|
||||||
|
codex-protocol = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true, features = [
|
tokio = { workspace = true, features = [
|
||||||
@@ -21,4 +22,5 @@ tokio = { workspace = true, features = [
|
|||||||
"process",
|
"process",
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
] }
|
] }
|
||||||
|
uuid = { workspace = true }
|
||||||
wiremock = { workspace = true }
|
wiremock = { workspace = true }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod auth_fixtures;
|
|||||||
mod mcp_process;
|
mod mcp_process;
|
||||||
mod mock_model_server;
|
mod mock_model_server;
|
||||||
mod responses;
|
mod responses;
|
||||||
|
mod rollout;
|
||||||
|
|
||||||
pub use auth_fixtures::ChatGptAuthFixture;
|
pub use auth_fixtures::ChatGptAuthFixture;
|
||||||
pub use auth_fixtures::ChatGptIdTokenClaims;
|
pub use auth_fixtures::ChatGptIdTokenClaims;
|
||||||
@@ -10,9 +11,11 @@ pub use auth_fixtures::write_chatgpt_auth;
|
|||||||
use codex_app_server_protocol::JSONRPCResponse;
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
pub use mcp_process::McpProcess;
|
pub use mcp_process::McpProcess;
|
||||||
pub use mock_model_server::create_mock_chat_completions_server;
|
pub use mock_model_server::create_mock_chat_completions_server;
|
||||||
|
pub use mock_model_server::create_mock_chat_completions_server_unchecked;
|
||||||
pub use responses::create_apply_patch_sse_response;
|
pub use responses::create_apply_patch_sse_response;
|
||||||
pub use responses::create_final_assistant_message_sse_response;
|
pub use responses::create_final_assistant_message_sse_response;
|
||||||
pub use responses::create_shell_sse_response;
|
pub use responses::create_shell_sse_response;
|
||||||
|
pub use rollout::create_fake_rollout;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
|
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ use codex_app_server_protocol::ArchiveConversationParams;
|
|||||||
use codex_app_server_protocol::CancelLoginChatGptParams;
|
use codex_app_server_protocol::CancelLoginChatGptParams;
|
||||||
use codex_app_server_protocol::ClientInfo;
|
use codex_app_server_protocol::ClientInfo;
|
||||||
use codex_app_server_protocol::ClientNotification;
|
use codex_app_server_protocol::ClientNotification;
|
||||||
|
use codex_app_server_protocol::FeedbackUploadParams;
|
||||||
use codex_app_server_protocol::GetAuthStatusParams;
|
use codex_app_server_protocol::GetAuthStatusParams;
|
||||||
use codex_app_server_protocol::InitializeParams;
|
use codex_app_server_protocol::InitializeParams;
|
||||||
use codex_app_server_protocol::InterruptConversationParams;
|
use codex_app_server_protocol::InterruptConversationParams;
|
||||||
use codex_app_server_protocol::ListConversationsParams;
|
use codex_app_server_protocol::ListConversationsParams;
|
||||||
use codex_app_server_protocol::ListModelsParams;
|
|
||||||
use codex_app_server_protocol::LoginApiKeyParams;
|
use codex_app_server_protocol::LoginApiKeyParams;
|
||||||
|
use codex_app_server_protocol::ModelListParams;
|
||||||
use codex_app_server_protocol::NewConversationParams;
|
use codex_app_server_protocol::NewConversationParams;
|
||||||
use codex_app_server_protocol::RemoveConversationListenerParams;
|
use codex_app_server_protocol::RemoveConversationListenerParams;
|
||||||
use codex_app_server_protocol::ResumeConversationParams;
|
use codex_app_server_protocol::ResumeConversationParams;
|
||||||
@@ -30,7 +31,10 @@ use codex_app_server_protocol::SendUserMessageParams;
|
|||||||
use codex_app_server_protocol::SendUserTurnParams;
|
use codex_app_server_protocol::SendUserTurnParams;
|
||||||
use codex_app_server_protocol::ServerRequest;
|
use codex_app_server_protocol::ServerRequest;
|
||||||
use codex_app_server_protocol::SetDefaultModelParams;
|
use codex_app_server_protocol::SetDefaultModelParams;
|
||||||
use codex_app_server_protocol::UploadFeedbackParams;
|
use codex_app_server_protocol::ThreadArchiveParams;
|
||||||
|
use codex_app_server_protocol::ThreadListParams;
|
||||||
|
use codex_app_server_protocol::ThreadResumeParams;
|
||||||
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
|
|
||||||
use codex_app_server_protocol::JSONRPCError;
|
use codex_app_server_protocol::JSONRPCError;
|
||||||
use codex_app_server_protocol::JSONRPCMessage;
|
use codex_app_server_protocol::JSONRPCMessage;
|
||||||
@@ -246,7 +250,7 @@ impl McpProcess {
|
|||||||
/// Send a `feedback/upload` JSON-RPC request.
|
/// Send a `feedback/upload` JSON-RPC request.
|
||||||
pub async fn send_upload_feedback_request(
|
pub async fn send_upload_feedback_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: UploadFeedbackParams,
|
params: FeedbackUploadParams,
|
||||||
) -> anyhow::Result<i64> {
|
) -> anyhow::Result<i64> {
|
||||||
let params = Some(serde_json::to_value(params)?);
|
let params = Some(serde_json::to_value(params)?);
|
||||||
self.send_request("feedback/upload", params).await
|
self.send_request("feedback/upload", params).await
|
||||||
@@ -275,10 +279,46 @@ impl McpProcess {
|
|||||||
self.send_request("listConversations", params).await
|
self.send_request("listConversations", params).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a `thread/start` JSON-RPC request.
|
||||||
|
pub async fn send_thread_start_request(
|
||||||
|
&mut self,
|
||||||
|
params: ThreadStartParams,
|
||||||
|
) -> anyhow::Result<i64> {
|
||||||
|
let params = Some(serde_json::to_value(params)?);
|
||||||
|
self.send_request("thread/start", params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a `thread/resume` JSON-RPC request.
|
||||||
|
pub async fn send_thread_resume_request(
|
||||||
|
&mut self,
|
||||||
|
params: ThreadResumeParams,
|
||||||
|
) -> anyhow::Result<i64> {
|
||||||
|
let params = Some(serde_json::to_value(params)?);
|
||||||
|
self.send_request("thread/resume", params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a `thread/archive` JSON-RPC request.
|
||||||
|
pub async fn send_thread_archive_request(
|
||||||
|
&mut self,
|
||||||
|
params: ThreadArchiveParams,
|
||||||
|
) -> anyhow::Result<i64> {
|
||||||
|
let params = Some(serde_json::to_value(params)?);
|
||||||
|
self.send_request("thread/archive", params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a `thread/list` JSON-RPC request.
|
||||||
|
pub async fn send_thread_list_request(
|
||||||
|
&mut self,
|
||||||
|
params: ThreadListParams,
|
||||||
|
) -> anyhow::Result<i64> {
|
||||||
|
let params = Some(serde_json::to_value(params)?);
|
||||||
|
self.send_request("thread/list", params).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Send a `model/list` JSON-RPC request.
|
/// Send a `model/list` JSON-RPC request.
|
||||||
pub async fn send_list_models_request(
|
pub async fn send_list_models_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: ListModelsParams,
|
params: ModelListParams,
|
||||||
) -> anyhow::Result<i64> {
|
) -> anyhow::Result<i64> {
|
||||||
let params = Some(serde_json::to_value(params)?);
|
let params = Some(serde_json::to_value(params)?);
|
||||||
self.send_request("model/list", params).await
|
self.send_request("model/list", params).await
|
||||||
|
|||||||
@@ -29,6 +29,25 @@ pub async fn create_mock_chat_completions_server(responses: Vec<String>) -> Mock
|
|||||||
server
|
server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Same as `create_mock_chat_completions_server` but does not enforce an
|
||||||
|
/// expectation on the number of calls.
|
||||||
|
pub async fn create_mock_chat_completions_server_unchecked(responses: Vec<String>) -> MockServer {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
|
||||||
|
let seq_responder = SeqResponder {
|
||||||
|
num_calls: AtomicUsize::new(0),
|
||||||
|
responses,
|
||||||
|
};
|
||||||
|
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/chat/completions"))
|
||||||
|
.respond_with(seq_responder)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
server
|
||||||
|
}
|
||||||
|
|
||||||
struct SeqResponder {
|
struct SeqResponder {
|
||||||
num_calls: AtomicUsize,
|
num_calls: AtomicUsize,
|
||||||
responses: Vec<String>,
|
responses: Vec<String>,
|
||||||
|
|||||||
82
codex-rs/app-server/tests/common/rollout.rs
Normal file
82
codex-rs/app-server/tests/common/rollout.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use codex_protocol::ConversationId;
|
||||||
|
use codex_protocol::protocol::SessionMeta;
|
||||||
|
use codex_protocol::protocol::SessionSource;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Create a minimal rollout file under `CODEX_HOME/sessions/YYYY/MM/DD/`.
|
||||||
|
///
|
||||||
|
/// - `filename_ts` is the filename timestamp component in `YYYY-MM-DDThh-mm-ss` format.
|
||||||
|
/// - `meta_rfc3339` is the envelope timestamp used in JSON lines.
|
||||||
|
/// - `preview` is the user message preview text.
|
||||||
|
/// - `model_provider` optionally sets the provider in the session meta payload.
|
||||||
|
///
|
||||||
|
/// Returns the generated conversation/session UUID as a string.
|
||||||
|
pub fn create_fake_rollout(
|
||||||
|
codex_home: &Path,
|
||||||
|
filename_ts: &str,
|
||||||
|
meta_rfc3339: &str,
|
||||||
|
preview: &str,
|
||||||
|
model_provider: Option<&str>,
|
||||||
|
) -> Result<String> {
|
||||||
|
let uuid = Uuid::new_v4();
|
||||||
|
let uuid_str = uuid.to_string();
|
||||||
|
let conversation_id = ConversationId::from_string(&uuid_str)?;
|
||||||
|
|
||||||
|
// sessions/YYYY/MM/DD derived from filename_ts (YYYY-MM-DDThh-mm-ss)
|
||||||
|
let year = &filename_ts[0..4];
|
||||||
|
let month = &filename_ts[5..7];
|
||||||
|
let day = &filename_ts[8..10];
|
||||||
|
let dir = codex_home.join("sessions").join(year).join(month).join(day);
|
||||||
|
fs::create_dir_all(&dir)?;
|
||||||
|
|
||||||
|
let file_path = dir.join(format!("rollout-{filename_ts}-{uuid}.jsonl"));
|
||||||
|
|
||||||
|
// Build JSONL lines
|
||||||
|
let payload = serde_json::to_value(SessionMeta {
|
||||||
|
id: conversation_id,
|
||||||
|
timestamp: meta_rfc3339.to_string(),
|
||||||
|
cwd: PathBuf::from("/"),
|
||||||
|
originator: "codex".to_string(),
|
||||||
|
cli_version: "0.0.0".to_string(),
|
||||||
|
instructions: None,
|
||||||
|
source: SessionSource::Cli,
|
||||||
|
model_provider: model_provider.map(str::to_string),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let lines = [
|
||||||
|
json!({
|
||||||
|
"timestamp": meta_rfc3339,
|
||||||
|
"type": "session_meta",
|
||||||
|
"payload": payload
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
json!({
|
||||||
|
"timestamp": meta_rfc3339,
|
||||||
|
"type":"response_item",
|
||||||
|
"payload": {
|
||||||
|
"type":"message",
|
||||||
|
"role":"user",
|
||||||
|
"content":[{"type":"input_text","text": preview}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
json!({
|
||||||
|
"timestamp": meta_rfc3339,
|
||||||
|
"type":"event_msg",
|
||||||
|
"payload": {
|
||||||
|
"type":"user_message",
|
||||||
|
"message": preview,
|
||||||
|
"kind": "plain"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
fs::write(file_path, lines.join("\n") + "\n")?;
|
||||||
|
Ok(uuid_str)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use app_test_support::McpProcess;
|
use app_test_support::McpProcess;
|
||||||
|
use app_test_support::create_fake_rollout;
|
||||||
use app_test_support::to_response;
|
use app_test_support::to_response;
|
||||||
use codex_app_server_protocol::JSONRPCNotification;
|
use codex_app_server_protocol::JSONRPCNotification;
|
||||||
use codex_app_server_protocol::JSONRPCResponse;
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
@@ -15,12 +16,8 @@ use codex_core::protocol::EventMsg;
|
|||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serde_json::json;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
|
||||||
@@ -357,70 +354,3 @@ async fn test_list_and_resume_conversations() -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_fake_rollout(
|
|
||||||
codex_home: &Path,
|
|
||||||
filename_ts: &str,
|
|
||||||
meta_rfc3339: &str,
|
|
||||||
preview: &str,
|
|
||||||
model_provider: Option<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let uuid = Uuid::new_v4();
|
|
||||||
// sessions/YYYY/MM/DD/ derived from filename_ts (YYYY-MM-DDThh-mm-ss)
|
|
||||||
let year = &filename_ts[0..4];
|
|
||||||
let month = &filename_ts[5..7];
|
|
||||||
let day = &filename_ts[8..10];
|
|
||||||
let dir = codex_home.join("sessions").join(year).join(month).join(day);
|
|
||||||
fs::create_dir_all(&dir)?;
|
|
||||||
|
|
||||||
let file_path = dir.join(format!("rollout-{filename_ts}-{uuid}.jsonl"));
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
// Meta line with timestamp (flattened meta in payload for new schema)
|
|
||||||
let mut payload = json!({
|
|
||||||
"id": uuid,
|
|
||||||
"timestamp": meta_rfc3339,
|
|
||||||
"cwd": "/",
|
|
||||||
"originator": "codex",
|
|
||||||
"cli_version": "0.0.0",
|
|
||||||
"instructions": null,
|
|
||||||
});
|
|
||||||
if let Some(provider) = model_provider {
|
|
||||||
payload["model_provider"] = json!(provider);
|
|
||||||
}
|
|
||||||
lines.push(
|
|
||||||
json!({
|
|
||||||
"timestamp": meta_rfc3339,
|
|
||||||
"type": "session_meta",
|
|
||||||
"payload": payload
|
|
||||||
})
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
// Minimal user message entry as a persisted response item (with envelope timestamp)
|
|
||||||
lines.push(
|
|
||||||
json!({
|
|
||||||
"timestamp": meta_rfc3339,
|
|
||||||
"type":"response_item",
|
|
||||||
"payload": {
|
|
||||||
"type":"message",
|
|
||||||
"role":"user",
|
|
||||||
"content":[{"type":"input_text","text": preview}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
// Add a matching user message event line to satisfy filters
|
|
||||||
lines.push(
|
|
||||||
json!({
|
|
||||||
"timestamp": meta_rfc3339,
|
|
||||||
"type":"event_msg",
|
|
||||||
"payload": {
|
|
||||||
"type":"user_message",
|
|
||||||
"message": preview,
|
|
||||||
"kind": "plain"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
fs::write(file_path, lines.join("\n") + "\n")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ use app_test_support::McpProcess;
|
|||||||
use app_test_support::to_response;
|
use app_test_support::to_response;
|
||||||
use codex_app_server_protocol::JSONRPCError;
|
use codex_app_server_protocol::JSONRPCError;
|
||||||
use codex_app_server_protocol::JSONRPCResponse;
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
use codex_app_server_protocol::ListModelsParams;
|
|
||||||
use codex_app_server_protocol::ListModelsResponse;
|
|
||||||
use codex_app_server_protocol::Model;
|
use codex_app_server_protocol::Model;
|
||||||
|
use codex_app_server_protocol::ModelListParams;
|
||||||
|
use codex_app_server_protocol::ModelListResponse;
|
||||||
use codex_app_server_protocol::ReasoningEffortOption;
|
use codex_app_server_protocol::ReasoningEffortOption;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
use codex_protocol::config_types::ReasoningEffort;
|
use codex_protocol::config_types::ReasoningEffort;
|
||||||
@@ -27,8 +27,8 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
|
|||||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_list_models_request(ListModelsParams {
|
.send_list_models_request(ModelListParams {
|
||||||
page_size: Some(100),
|
limit: Some(100),
|
||||||
cursor: None,
|
cursor: None,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -39,7 +39,10 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
let ListModelsResponse { items, next_cursor } = to_response::<ListModelsResponse>(response)?;
|
let ModelListResponse {
|
||||||
|
data: items,
|
||||||
|
next_cursor,
|
||||||
|
} = to_response::<ModelListResponse>(response)?;
|
||||||
|
|
||||||
let expected_models = vec![
|
let expected_models = vec![
|
||||||
Model {
|
Model {
|
||||||
@@ -111,8 +114,8 @@ async fn list_models_pagination_works() -> Result<()> {
|
|||||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
let first_request = mcp
|
let first_request = mcp
|
||||||
.send_list_models_request(ListModelsParams {
|
.send_list_models_request(ModelListParams {
|
||||||
page_size: Some(1),
|
limit: Some(1),
|
||||||
cursor: None,
|
cursor: None,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -123,18 +126,18 @@ async fn list_models_pagination_works() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
let ListModelsResponse {
|
let ModelListResponse {
|
||||||
items: first_items,
|
data: first_items,
|
||||||
next_cursor: first_cursor,
|
next_cursor: first_cursor,
|
||||||
} = to_response::<ListModelsResponse>(first_response)?;
|
} = to_response::<ModelListResponse>(first_response)?;
|
||||||
|
|
||||||
assert_eq!(first_items.len(), 1);
|
assert_eq!(first_items.len(), 1);
|
||||||
assert_eq!(first_items[0].id, "gpt-5-codex");
|
assert_eq!(first_items[0].id, "gpt-5-codex");
|
||||||
let next_cursor = first_cursor.ok_or_else(|| anyhow!("cursor for second page"))?;
|
let next_cursor = first_cursor.ok_or_else(|| anyhow!("cursor for second page"))?;
|
||||||
|
|
||||||
let second_request = mcp
|
let second_request = mcp
|
||||||
.send_list_models_request(ListModelsParams {
|
.send_list_models_request(ModelListParams {
|
||||||
page_size: Some(1),
|
limit: Some(1),
|
||||||
cursor: Some(next_cursor.clone()),
|
cursor: Some(next_cursor.clone()),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -145,10 +148,10 @@ async fn list_models_pagination_works() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
let ListModelsResponse {
|
let ModelListResponse {
|
||||||
items: second_items,
|
data: second_items,
|
||||||
next_cursor: second_cursor,
|
next_cursor: second_cursor,
|
||||||
} = to_response::<ListModelsResponse>(second_response)?;
|
} = to_response::<ModelListResponse>(second_response)?;
|
||||||
|
|
||||||
assert_eq!(second_items.len(), 1);
|
assert_eq!(second_items.len(), 1);
|
||||||
assert_eq!(second_items[0].id, "gpt-5");
|
assert_eq!(second_items[0].id, "gpt-5");
|
||||||
@@ -164,8 +167,8 @@ async fn list_models_rejects_invalid_cursor() -> Result<()> {
|
|||||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_list_models_request(ListModelsParams {
|
.send_list_models_request(ModelListParams {
|
||||||
page_size: None,
|
limit: None,
|
||||||
cursor: Some("invalid".to_string()),
|
cursor: Some("invalid".to_string()),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
// v2 test suite modules
|
// v2 test suite modules
|
||||||
mod account;
|
mod account;
|
||||||
|
mod thread_archive;
|
||||||
|
mod thread_list;
|
||||||
|
mod thread_resume;
|
||||||
|
mod thread_start;
|
||||||
|
|||||||
93
codex-rs/app-server/tests/suite/v2/thread_archive.rs
Normal file
93
codex-rs/app-server/tests/suite/v2/thread_archive.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use app_test_support::McpProcess;
|
||||||
|
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_core::ARCHIVED_SESSIONS_SUBDIR;
|
||||||
|
use codex_core::find_conversation_path_by_id_str;
|
||||||
|
use std::path::Path;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_archive_moves_rollout_into_archived_directory() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
create_config_toml(codex_home.path())?;
|
||||||
|
|
||||||
|
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
|
// Start a thread.
|
||||||
|
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)?;
|
||||||
|
assert!(!thread.id.is_empty());
|
||||||
|
|
||||||
|
// Locate the rollout path recorded for this thread id.
|
||||||
|
let rollout_path = find_conversation_path_by_id_str(codex_home.path(), &thread.id)
|
||||||
|
.await?
|
||||||
|
.expect("expected rollout path for thread id to exist");
|
||||||
|
assert!(
|
||||||
|
rollout_path.exists(),
|
||||||
|
"expected {} to exist",
|
||||||
|
rollout_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Archive the thread.
|
||||||
|
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)?;
|
||||||
|
|
||||||
|
// Verify file moved.
|
||||||
|
let archived_directory = codex_home.path().join(ARCHIVED_SESSIONS_SUBDIR);
|
||||||
|
// The archived file keeps the original filename (rollout-...-<id>.jsonl).
|
||||||
|
let archived_rollout_path =
|
||||||
|
archived_directory.join(rollout_path.file_name().expect("rollout file name"));
|
||||||
|
assert!(
|
||||||
|
!rollout_path.exists(),
|
||||||
|
"expected rollout path {} to be moved",
|
||||||
|
rollout_path.display()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
archived_rollout_path.exists(),
|
||||||
|
"expected archived rollout path {} to exist",
|
||||||
|
archived_rollout_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_config_toml(codex_home: &Path) -> std::io::Result<()> {
|
||||||
|
let config_toml = codex_home.join("config.toml");
|
||||||
|
std::fs::write(config_toml, config_contents())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_contents() -> &'static str {
|
||||||
|
r#"model = "mock-model"
|
||||||
|
approval_policy = "never"
|
||||||
|
sandbox_mode = "read-only"
|
||||||
|
"#
|
||||||
|
}
|
||||||
205
codex-rs/app-server/tests/suite/v2/thread_list.rs
Normal file
205
codex-rs/app-server/tests/suite/v2/thread_list.rs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use app_test_support::McpProcess;
|
||||||
|
use app_test_support::create_fake_rollout;
|
||||||
|
use app_test_support::to_response;
|
||||||
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
|
use codex_app_server_protocol::RequestId;
|
||||||
|
use codex_app_server_protocol::ThreadListParams;
|
||||||
|
use codex_app_server_protocol::ThreadListResponse;
|
||||||
|
use serde_json::json;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use tokio::time::timeout;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_list_basic_empty() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
create_minimal_config(codex_home.path())?;
|
||||||
|
|
||||||
|
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
|
// List threads in an empty CODEX_HOME; should return an empty page with nextCursor: null.
|
||||||
|
let list_id = mcp
|
||||||
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
cursor: None,
|
||||||
|
limit: Some(10),
|
||||||
|
model_providers: None,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let list_resp: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(list_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let ThreadListResponse { data, next_cursor } = to_response::<ThreadListResponse>(list_resp)?;
|
||||||
|
assert!(data.is_empty());
|
||||||
|
assert_eq!(next_cursor, None);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal config.toml for listing.
|
||||||
|
fn create_minimal_config(codex_home: &std::path::Path) -> std::io::Result<()> {
|
||||||
|
let config_toml = codex_home.join("config.toml");
|
||||||
|
std::fs::write(
|
||||||
|
config_toml,
|
||||||
|
r#"
|
||||||
|
model = "mock-model"
|
||||||
|
approval_policy = "never"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
create_minimal_config(codex_home.path())?;
|
||||||
|
|
||||||
|
// Create three rollouts so we can paginate with limit=2.
|
||||||
|
let _a = create_fake_rollout(
|
||||||
|
codex_home.path(),
|
||||||
|
"2025-01-02T12-00-00",
|
||||||
|
"2025-01-02T12:00:00Z",
|
||||||
|
"Hello",
|
||||||
|
Some("mock_provider"),
|
||||||
|
)?;
|
||||||
|
let _b = create_fake_rollout(
|
||||||
|
codex_home.path(),
|
||||||
|
"2025-01-01T13-00-00",
|
||||||
|
"2025-01-01T13:00:00Z",
|
||||||
|
"Hello",
|
||||||
|
Some("mock_provider"),
|
||||||
|
)?;
|
||||||
|
let _c = create_fake_rollout(
|
||||||
|
codex_home.path(),
|
||||||
|
"2025-01-01T12-00-00",
|
||||||
|
"2025-01-01T12:00:00Z",
|
||||||
|
"Hello",
|
||||||
|
Some("mock_provider"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
|
// Page 1: limit 2 → expect next_cursor Some.
|
||||||
|
let page1_id = mcp
|
||||||
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
cursor: None,
|
||||||
|
limit: Some(2),
|
||||||
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let page1_resp: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(page1_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let ThreadListResponse {
|
||||||
|
data: data1,
|
||||||
|
next_cursor: cursor1,
|
||||||
|
} = to_response::<ThreadListResponse>(page1_resp)?;
|
||||||
|
assert_eq!(data1.len(), 2);
|
||||||
|
let cursor1 = cursor1.expect("expected nextCursor on first page");
|
||||||
|
|
||||||
|
// Page 2: with cursor → expect next_cursor None when no more results.
|
||||||
|
let page2_id = mcp
|
||||||
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
cursor: Some(cursor1),
|
||||||
|
limit: Some(2),
|
||||||
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let page2_resp: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(page2_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let ThreadListResponse {
|
||||||
|
data: data2,
|
||||||
|
next_cursor: cursor2,
|
||||||
|
} = to_response::<ThreadListResponse>(page2_resp)?;
|
||||||
|
assert!(data2.len() <= 2);
|
||||||
|
assert_eq!(cursor2, None, "expected nextCursor to be null on last page");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_list_respects_provider_filter() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
create_minimal_config(codex_home.path())?;
|
||||||
|
|
||||||
|
// Create rollouts under two providers.
|
||||||
|
let _a = create_fake_rollout(
|
||||||
|
codex_home.path(),
|
||||||
|
"2025-01-02T10-00-00",
|
||||||
|
"2025-01-02T10:00:00Z",
|
||||||
|
"X",
|
||||||
|
Some("mock_provider"),
|
||||||
|
)?; // mock_provider
|
||||||
|
// one with a different provider
|
||||||
|
let uuid = Uuid::new_v4();
|
||||||
|
let dir = codex_home
|
||||||
|
.path()
|
||||||
|
.join("sessions")
|
||||||
|
.join("2025")
|
||||||
|
.join("01")
|
||||||
|
.join("02");
|
||||||
|
std::fs::create_dir_all(&dir)?;
|
||||||
|
let file_path = dir.join(format!("rollout-2025-01-02T11-00-00-{uuid}.jsonl"));
|
||||||
|
let lines = [
|
||||||
|
json!({
|
||||||
|
"timestamp": "2025-01-02T11:00:00Z",
|
||||||
|
"type": "session_meta",
|
||||||
|
"payload": {
|
||||||
|
"id": uuid,
|
||||||
|
"timestamp": "2025-01-02T11:00:00Z",
|
||||||
|
"cwd": "/",
|
||||||
|
"originator": "codex",
|
||||||
|
"cli_version": "0.0.0",
|
||||||
|
"instructions": null,
|
||||||
|
"source": "vscode",
|
||||||
|
"model_provider": "other_provider"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
json!({
|
||||||
|
"timestamp": "2025-01-02T11:00:00Z",
|
||||||
|
"type":"response_item",
|
||||||
|
"payload": {"type":"message","role":"user","content":[{"type":"input_text","text":"X"}]}
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
json!({
|
||||||
|
"timestamp": "2025-01-02T11:00:00Z",
|
||||||
|
"type":"event_msg",
|
||||||
|
"payload": {"type":"user_message","message":"X","kind":"plain"}
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
];
|
||||||
|
std::fs::write(file_path, lines.join("\n") + "\n")?;
|
||||||
|
|
||||||
|
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
|
// Filter to only other_provider; expect 1 item, nextCursor None.
|
||||||
|
let list_id = mcp
|
||||||
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
cursor: None,
|
||||||
|
limit: Some(10),
|
||||||
|
model_providers: Some(vec!["other_provider".to_string()]),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let resp: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(list_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let ThreadListResponse { data, next_cursor } = to_response::<ThreadListResponse>(resp)?;
|
||||||
|
assert_eq!(data.len(), 1);
|
||||||
|
assert_eq!(next_cursor, None);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
79
codex-rs/app-server/tests/suite/v2/thread_resume.rs
Normal file
79
codex-rs/app-server/tests/suite/v2/thread_resume.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use app_test_support::McpProcess;
|
||||||
|
use app_test_support::create_mock_chat_completions_server;
|
||||||
|
use app_test_support::to_response;
|
||||||
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
|
use codex_app_server_protocol::RequestId;
|
||||||
|
use codex_app_server_protocol::ThreadResumeParams;
|
||||||
|
use codex_app_server_protocol::ThreadResumeResponse;
|
||||||
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
|
use codex_app_server_protocol::ThreadStartResponse;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_resume_returns_existing_thread() -> Result<()> {
|
||||||
|
let server = create_mock_chat_completions_server(vec![]).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??;
|
||||||
|
|
||||||
|
// Start a thread.
|
||||||
|
let start_id = mcp
|
||||||
|
.send_thread_start_request(ThreadStartParams {
|
||||||
|
model: Some("gpt-5-codex".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)?;
|
||||||
|
|
||||||
|
// Resume it via v2 API.
|
||||||
|
let resume_id = mcp
|
||||||
|
.send_thread_resume_request(ThreadResumeParams {
|
||||||
|
thread_id: thread.id.clone(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let resume_resp: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let ThreadResumeResponse { thread: resumed } =
|
||||||
|
to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||||
|
assert_eq!(resumed.id, thread.id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create a config.toml pointing at the mock model server.
|
||||||
|
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
|
||||||
|
let config_toml = codex_home.join("config.toml");
|
||||||
|
std::fs::write(
|
||||||
|
config_toml,
|
||||||
|
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 = "chat"
|
||||||
|
request_max_retries = 0
|
||||||
|
stream_max_retries = 0
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
81
codex-rs/app-server/tests/suite/v2/thread_start.rs
Normal file
81
codex-rs/app-server/tests/suite/v2/thread_start.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use app_test_support::McpProcess;
|
||||||
|
use app_test_support::create_mock_chat_completions_server;
|
||||||
|
use app_test_support::to_response;
|
||||||
|
use codex_app_server_protocol::JSONRPCNotification;
|
||||||
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
|
use codex_app_server_protocol::RequestId;
|
||||||
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
|
use codex_app_server_protocol::ThreadStartResponse;
|
||||||
|
use codex_app_server_protocol::ThreadStartedNotification;
|
||||||
|
use std::path::Path;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
||||||
|
// Provide a mock server and config so model wiring is valid.
|
||||||
|
let server = create_mock_chat_completions_server(vec![]).await;
|
||||||
|
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
create_config_toml(codex_home.path(), &server.uri())?;
|
||||||
|
|
||||||
|
// Start server and initialize.
|
||||||
|
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
|
// Start a v2 thread with an explicit model override.
|
||||||
|
let req_id = mcp
|
||||||
|
.send_thread_start_request(ThreadStartParams {
|
||||||
|
model: Some("gpt-5".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Expect a proper JSON-RPC response with a thread id.
|
||||||
|
let resp: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let ThreadStartResponse { thread } = to_response::<ThreadStartResponse>(resp)?;
|
||||||
|
assert!(!thread.id.is_empty(), "thread id should not be empty");
|
||||||
|
|
||||||
|
// A corresponding thread/started notification should arrive.
|
||||||
|
let notif: JSONRPCNotification = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_notification_message("thread/started"),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let started: ThreadStartedNotification =
|
||||||
|
serde_json::from_value(notif.params.expect("params must be present"))?;
|
||||||
|
assert_eq!(started.thread.id, thread.id);
|
||||||
|
|
||||||
|
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");
|
||||||
|
std::fs::write(
|
||||||
|
config_toml,
|
||||||
|
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 = "chat"
|
||||||
|
request_max_retries = 0
|
||||||
|
stream_max_retries = 0
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -75,6 +75,7 @@ pub use rollout::find_conversation_path_by_id_str;
|
|||||||
pub use rollout::list::ConversationItem;
|
pub use rollout::list::ConversationItem;
|
||||||
pub use rollout::list::ConversationsPage;
|
pub use rollout::list::ConversationsPage;
|
||||||
pub use rollout::list::Cursor;
|
pub use rollout::list::Cursor;
|
||||||
|
pub use rollout::list::parse_cursor;
|
||||||
pub use rollout::list::read_head_for_summary;
|
pub use rollout::list::read_head_for_summary;
|
||||||
mod function_tool;
|
mod function_tool;
|
||||||
mod state;
|
mod state;
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ async fn traverse_directories_for_paths(
|
|||||||
/// Pagination cursor token format: "<file_ts>|<uuid>" where `file_ts` matches the
|
/// Pagination cursor token format: "<file_ts>|<uuid>" where `file_ts` matches the
|
||||||
/// filename timestamp portion (YYYY-MM-DDThh-mm-ss) used in rollout filenames.
|
/// filename timestamp portion (YYYY-MM-DDThh-mm-ss) used in rollout filenames.
|
||||||
/// The cursor orders files by timestamp desc, then UUID desc.
|
/// The cursor orders files by timestamp desc, then UUID desc.
|
||||||
fn parse_cursor(token: &str) -> Option<Cursor> {
|
pub fn parse_cursor(token: &str) -> Option<Cursor> {
|
||||||
let (file_ts, uuid_str) = token.split_once('|')?;
|
let (file_ts, uuid_str) = token.split_once('|')?;
|
||||||
|
|
||||||
let Ok(uuid) = Uuid::parse_str(uuid_str) else {
|
let Ok(uuid) = Uuid::parse_str(uuid_str) else {
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ use responses::start_mock_server;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
#[ignore = "flaky on ubuntu-24.04-arm - aarch64-unknown-linux-gnu"]
|
||||||
|
// The notify script gets far enough to create (and therefore surface) the file,
|
||||||
|
// but hasn’t flushed the JSON yet. Reading an empty file produces EOF while parsing
|
||||||
|
// a value at line 1 column 0. May be caused by a slow runner.
|
||||||
async fn summarize_context_three_requests_and_instructions() -> anyhow::Result<()> {
|
async fn summarize_context_three_requests_and_instructions() -> anyhow::Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user