mirror of
https://github.com/openai/codex.git
synced 2026-04-18 11:44:46 +00:00
Compare commits
2 Commits
pr17693
...
dev/rasmus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdba436cc0 | ||
|
|
3e9c9ec684 |
@@ -2708,6 +2708,12 @@
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -3061,6 +3067,12 @@
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -3257,6 +3269,12 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -12781,6 +12781,12 @@
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -14085,6 +14091,12 @@
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -14387,6 +14399,12 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadStartParams",
|
||||
|
||||
@@ -10629,6 +10629,12 @@
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11933,6 +11939,12 @@
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -12235,6 +12247,12 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadStartParams",
|
||||
|
||||
@@ -168,6 +168,12 @@
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -1081,6 +1081,12 @@
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -227,6 +227,12 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"userInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadStartParams",
|
||||
|
||||
@@ -27,7 +27,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /**
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, userInstructions?: string | null | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /**
|
||||
* If true, persist additional rollout EventMsg variants required to
|
||||
* reconstruct a richer thread history on subsequent resume/fork/read.
|
||||
*/
|
||||
|
||||
@@ -36,7 +36,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /**
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, userInstructions?: string | null | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /**
|
||||
* If true, persist additional rollout EventMsg variants required to
|
||||
* reconstruct a richer thread history on subsequent resume/fork/read.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string |
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /**
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, userInstructions?: string | null | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /**
|
||||
* If true, opt into emitting raw Responses API items on the event stream.
|
||||
* This is for internal use only (e.g. Codex Cloud).
|
||||
*/
|
||||
|
||||
@@ -2663,6 +2663,14 @@ pub struct ThreadStartParams {
|
||||
pub config: Option<HashMap<String, JsonValue>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub service_name: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub user_instructions: Option<Option<String>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub base_instructions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -2786,6 +2794,14 @@ pub struct ThreadResumeParams {
|
||||
pub sandbox: Option<SandboxMode>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub user_instructions: Option<Option<String>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub base_instructions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -2866,6 +2882,14 @@ pub struct ThreadForkParams {
|
||||
pub sandbox: Option<SandboxMode>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub user_instructions: Option<Option<String>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub base_instructions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -8596,6 +8620,65 @@ mod tests {
|
||||
assert_eq!(fork.instruction_sources, Vec::<PathBuf>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_start_params_preserve_explicit_null_user_instructions() {
|
||||
let params: ThreadStartParams = serde_json::from_value(json!({
|
||||
"userInstructions": null,
|
||||
}))
|
||||
.expect("params should deserialize");
|
||||
assert_eq!(params.user_instructions, Some(None));
|
||||
|
||||
let serialized = serde_json::to_value(¶ms).expect("params should serialize");
|
||||
assert_eq!(
|
||||
serialized.get("userInstructions"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
|
||||
let serialized_without_override =
|
||||
serde_json::to_value(ThreadStartParams::default()).expect("params should serialize");
|
||||
assert_eq!(serialized_without_override.get("userInstructions"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_resume_params_preserve_explicit_null_user_instructions() {
|
||||
let params: ThreadResumeParams = serde_json::from_value(json!({
|
||||
"threadId": "thread_123",
|
||||
"userInstructions": null,
|
||||
}))
|
||||
.expect("params should deserialize");
|
||||
assert_eq!(params.user_instructions, Some(None));
|
||||
|
||||
let serialized = serde_json::to_value(¶ms).expect("params should serialize");
|
||||
assert_eq!(
|
||||
serialized.get("userInstructions"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
|
||||
let serialized_without_override =
|
||||
serde_json::to_value(ThreadResumeParams::default()).expect("params should serialize");
|
||||
assert_eq!(serialized_without_override.get("userInstructions"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_fork_params_preserve_explicit_null_user_instructions() {
|
||||
let params: ThreadForkParams = serde_json::from_value(json!({
|
||||
"threadId": "thread_123",
|
||||
"userInstructions": null,
|
||||
}))
|
||||
.expect("params should deserialize");
|
||||
assert_eq!(params.user_instructions, Some(None));
|
||||
|
||||
let serialized = serde_json::to_value(¶ms).expect("params should serialize");
|
||||
assert_eq!(
|
||||
serialized.get("userInstructions"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
|
||||
let serialized_without_override =
|
||||
serde_json::to_value(ThreadForkParams::default()).expect("params should serialize");
|
||||
assert_eq!(serialized_without_override.get("userInstructions"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_start_params_preserve_explicit_null_service_tier() {
|
||||
let params: TurnStartParams = serde_json::from_value(json!({
|
||||
|
||||
@@ -210,6 +210,10 @@ Start a fresh thread when you need a new Codex conversation.
|
||||
// current config settings.
|
||||
"model": "gpt-5.1-codex",
|
||||
"cwd": "/Users/me/project",
|
||||
// Optional resolved contents of the client's user-level AGENTS.md. Omit to
|
||||
// let app-server read its own CODEX_HOME AGENTS.md; pass null to suppress
|
||||
// that fallback for this thread.
|
||||
"userInstructions": "Use the team's local coding conventions.",
|
||||
"approvalPolicy": "never",
|
||||
"sandbox": "workspaceWrite",
|
||||
"personality": "friendly",
|
||||
|
||||
@@ -2227,6 +2227,7 @@ impl CodexMessageProcessor {
|
||||
sandbox,
|
||||
config,
|
||||
service_name,
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
dynamic_tools,
|
||||
@@ -2245,6 +2246,7 @@ impl CodexMessageProcessor {
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox,
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
personality,
|
||||
@@ -2655,6 +2657,7 @@ impl CodexMessageProcessor {
|
||||
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
|
||||
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
|
||||
sandbox: Option<SandboxMode>,
|
||||
user_instructions: Option<Option<String>>,
|
||||
base_instructions: Option<String>,
|
||||
developer_instructions: Option<String>,
|
||||
personality: Option<Personality>,
|
||||
@@ -2671,6 +2674,7 @@ impl CodexMessageProcessor {
|
||||
sandbox_mode: sandbox.map(SandboxMode::to_core),
|
||||
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
personality,
|
||||
@@ -4076,6 +4080,7 @@ impl CodexMessageProcessor {
|
||||
approvals_reviewer,
|
||||
sandbox,
|
||||
config: mut request_overrides,
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
personality,
|
||||
@@ -4109,6 +4114,7 @@ impl CodexMessageProcessor {
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox,
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
personality,
|
||||
@@ -4598,6 +4604,7 @@ impl CodexMessageProcessor {
|
||||
approvals_reviewer,
|
||||
sandbox,
|
||||
config: cli_overrides,
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
ephemeral,
|
||||
@@ -4679,6 +4686,7 @@ impl CodexMessageProcessor {
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox,
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
/*personality*/ None,
|
||||
@@ -9747,6 +9755,7 @@ mod tests {
|
||||
approvals_reviewer: None,
|
||||
sandbox: None,
|
||||
config: None,
|
||||
user_instructions: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
personality: None,
|
||||
|
||||
@@ -266,6 +266,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
|
||||
sandbox: None,
|
||||
config: None,
|
||||
service_name: None,
|
||||
user_instructions: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
personality: None,
|
||||
|
||||
@@ -26,6 +26,8 @@ use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
@@ -183,6 +185,153 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_fork_honors_explicit_null_thread_instructions() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let response_mock =
|
||||
responses::mount_sse_sequence(&server, vec![body.clone(), body.clone(), body]).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("AGENTS.md"),
|
||||
"Config user instructions sentinel",
|
||||
)?;
|
||||
|
||||
let conversation_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-05T12-00-00",
|
||||
"2025-01-05T12:00:00Z",
|
||||
"Saved user message",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let disabled_instruction_config = json!({
|
||||
"include_permissions_instructions": false,
|
||||
"include_apps_instructions": false,
|
||||
"include_environment_context": false,
|
||||
"features.apps": false,
|
||||
"features.plugins": false,
|
||||
"features.codex_hooks": false,
|
||||
"skills.bundled.enabled": false,
|
||||
});
|
||||
|
||||
let fork_params = [
|
||||
(
|
||||
json!({
|
||||
"threadId": conversation_id.clone(),
|
||||
"config": disabled_instruction_config.clone(),
|
||||
}),
|
||||
/*expect_user_instructions*/ true,
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"threadId": conversation_id.clone(),
|
||||
"config": disabled_instruction_config.clone(),
|
||||
"userInstructions": null,
|
||||
}),
|
||||
/*expect_user_instructions*/ false,
|
||||
),
|
||||
];
|
||||
|
||||
let mut forked_thread_ids = Vec::new();
|
||||
for (params, _expect_user_instructions) in fork_params {
|
||||
let fork_id = mcp.send_raw_request("thread/fork", Some(params)).await?;
|
||||
let fork_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(fork_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadForkResponse { thread, .. } = to_response::<ThreadForkResponse>(fork_resp)?;
|
||||
forked_thread_ids.push(thread.id.clone());
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![UserInput::Text {
|
||||
text: "continue".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
}
|
||||
|
||||
let refork_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/fork",
|
||||
Some(json!({
|
||||
"threadId": forked_thread_ids[1].clone(),
|
||||
"config": disabled_instruction_config.clone(),
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let refork_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(refork_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadForkResponse { thread, .. } = to_response::<ThreadForkResponse>(refork_resp)?;
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![UserInput::Text {
|
||||
text: "continue again".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
for (request, expect_user_instructions) in requests.into_iter().zip([true, false, false]) {
|
||||
let user_texts = request.message_input_texts("user");
|
||||
assert_eq!(
|
||||
user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("Config user instructions sentinel")),
|
||||
expect_user_instructions,
|
||||
"unexpected config user instruction presence: {user_texts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
@@ -1810,6 +1810,7 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: thread.id,
|
||||
model: Some("gpt-5.2-codex".to_string()),
|
||||
user_instructions: Some(Some("Resumed user instructions sentinel".to_string())),
|
||||
personality: Some(Personality::Friendly),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -1860,6 +1861,13 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
|
||||
instructions_text.contains(CODEX_5_2_INSTRUCTIONS_TEMPLATE_DEFAULT),
|
||||
"expected default base instructions from history, got {instructions_text:?}"
|
||||
);
|
||||
let user_texts = request.message_input_texts("user");
|
||||
assert!(
|
||||
user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("Resumed user instructions sentinel")),
|
||||
"expected resumed user instructions in user input, got {user_texts:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -152,6 +152,103 @@ async fn turn_start_sends_originator_header() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_honors_explicit_null_thread_instructions() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let response_mock = responses::mount_sse_sequence(&server, vec![body.clone(), body]).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never", &BTreeMap::new())?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("AGENTS.md"),
|
||||
"Config user instructions sentinel",
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let disabled_instruction_config = json!({
|
||||
"include_permissions_instructions": false,
|
||||
"include_apps_instructions": false,
|
||||
"include_environment_context": false,
|
||||
"features.apps": false,
|
||||
"features.plugins": false,
|
||||
"features.codex_hooks": false,
|
||||
"skills.bundled.enabled": false,
|
||||
});
|
||||
|
||||
let thread_start_params = [
|
||||
(
|
||||
json!({
|
||||
"model": "mock-model",
|
||||
"config": disabled_instruction_config.clone(),
|
||||
}),
|
||||
/*expect_instructions*/ true,
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"model": "mock-model",
|
||||
"config": disabled_instruction_config.clone(),
|
||||
"userInstructions": null,
|
||||
}),
|
||||
/*expect_user_instructions*/ false,
|
||||
),
|
||||
];
|
||||
|
||||
for (params, _expect_user_instructions) in thread_start_params {
|
||||
let thread_req = mcp.send_raw_request("thread/start", Some(params)).await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
}
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
for (request, expect_user_instructions) in requests.into_iter().zip([true, false]) {
|
||||
let user_texts = request.message_input_texts("user");
|
||||
assert_eq!(
|
||||
user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("Config user instructions sentinel")),
|
||||
expect_user_instructions,
|
||||
"unexpected config user instruction presence: {user_texts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
|
||||
@@ -3254,6 +3254,46 @@ fn cli_override_sets_compact_prompt() -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_uses_user_instructions_override() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(codex_home.path().join("AGENTS.md"), "from disk")?;
|
||||
let overrides = ConfigOverrides {
|
||||
user_instructions: Some(Some(" from request ".to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
overrides,
|
||||
codex_home.abs(),
|
||||
)?;
|
||||
|
||||
assert_eq!(config.user_instructions.as_deref(), Some("from request"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_explicit_null_user_instructions_skips_disk_fallback() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(codex_home.path().join("AGENTS.md"), "from disk")?;
|
||||
let overrides = ConfigOverrides {
|
||||
user_instructions: Some(None),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
overrides,
|
||||
codex_home.abs(),
|
||||
)?;
|
||||
|
||||
assert_eq!(config.user_instructions, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_compact_prompt_from_file() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -1257,6 +1257,7 @@ pub struct ConfigOverrides {
|
||||
pub js_repl_node_path: Option<PathBuf>,
|
||||
pub js_repl_node_module_dirs: Option<Vec<PathBuf>>,
|
||||
pub zsh_path: Option<PathBuf>,
|
||||
pub user_instructions: Option<Option<String>>,
|
||||
pub base_instructions: Option<String>,
|
||||
pub developer_instructions: Option<String>,
|
||||
pub personality: Option<Personality>,
|
||||
@@ -1442,10 +1443,6 @@ impl Config {
|
||||
network: network_requirements,
|
||||
} = config_layer_stack.requirements().clone();
|
||||
|
||||
let (user_instructions, user_instructions_path) =
|
||||
Self::load_instructions(Some(&codex_home))
|
||||
.map(|loaded| (Some(loaded.contents), Some(loaded.path)))
|
||||
.unwrap_or((None, None));
|
||||
let mut startup_warnings = Vec::new();
|
||||
|
||||
// Destructure ConfigOverrides fully to ensure all overrides are applied.
|
||||
@@ -1465,6 +1462,7 @@ impl Config {
|
||||
js_repl_node_path: js_repl_node_path_override,
|
||||
js_repl_node_module_dirs: js_repl_node_module_dirs_override,
|
||||
zsh_path: zsh_path_override,
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
personality,
|
||||
@@ -1847,6 +1845,20 @@ impl Config {
|
||||
});
|
||||
|
||||
let commit_attribution = cfg.commit_attribution;
|
||||
let (user_instructions, user_instructions_path) = match user_instructions {
|
||||
Some(Some(value)) => {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
(None, None)
|
||||
} else {
|
||||
(Some(trimmed.to_string()), None)
|
||||
}
|
||||
}
|
||||
Some(None) => (None, None),
|
||||
None => Self::load_instructions(Some(&codex_home))
|
||||
.map(|loaded| (Some(loaded.contents), Some(loaded.path)))
|
||||
.unwrap_or((None, None)),
|
||||
};
|
||||
|
||||
// Load base instructions override from a file if specified. If the
|
||||
// path is relative, resolve it against the effective cwd so the
|
||||
|
||||
Reference in New Issue
Block a user