Files
codex/codex-rs/core/src/test_support.rs
vkg-oai fc948f8c47 Add a provider for thread-scoped instructions (#44701)
## What changed

- Expose `ThreadInstructionsProvider` through `StartThreadOptions`. Load its snapshot at startup and model-request boundaries, composing it after global instructions and before repository instructions. Empty or blank output clears only the thread contribution.
- Reject thread instructions exceeding 10,000 estimated tokens independently of the repository instruction budget. Allow host-provided instructions without a filesystem source and rename the shared future type to `LoadInstructionsFuture`.
- Retain the provider across warm resumes; require hosts to supply it again for cold resumes and offline forks. Live forks and subagents inherit applied snapshots without inheriting the source thread's provider, including when the parent is unloaded during setup or reload.
- Include thread instructions in guardian reviewer inheritance and reuse decisions.

## Testing

Add coverage for composition and clearing, refresh within an active turn, size rejection before sampling, cancellation-safe refresh, fork and resume behavior, parent eviction, and reviewer reuse invalidation.

GitOrigin-RevId: 7be9a523cbbfd67704067dfd526188dad89a3c88
2026-09-11 01:57:54 +00:00

233 lines
7.9 KiB
Rust

//! Test-only helpers exposed for cross-crate integration tests.
//!
//! Production code should not depend on this module.
//! We prefer this to using a crate feature to avoid building multiple
//! permutations of the crate.
use std::path::PathBuf;
use std::sync::Arc;
use codex_exec_server::EnvironmentManager;
use codex_extension_api::LoadInstructionsFuture;
use codex_extension_api::LoadedUserInstructions;
use codex_extension_api::UserInstructionsProvider;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::ModelProviderInfo;
use codex_models_manager::bundled_models_response;
use codex_models_manager::collaboration_mode_presets;
use codex_models_manager::manager::SharedModelsManager;
use codex_models_manager::test_support::construct_model_info_offline_for_tests;
use codex_models_manager::test_support::get_model_offline_for_tests;
use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::mcp::ClientMcpExtensions;
use codex_protocol::mcp::OPENAI_FORM_EXTENSION_ID;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::protocol::SessionSource;
use once_cell::sync::Lazy;
use crate::ThreadManager;
use crate::config::Config;
use crate::responses_metadata::CodexResponsesMetadata;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_metadata::subagent_header_value;
use crate::responses_metadata::subagent_metadata_kind;
use crate::thread_manager;
use crate::unified_exec;
static TEST_MODEL_PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
let mut response = bundled_models_response()
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
response.models.sort_by_key(|model| model.priority);
let mut presets: Vec<ModelPreset> = response.models.into_iter().map(Into::into).collect();
ModelPreset::mark_default_by_picker_visibility(&mut presets);
presets
});
/// Reattaches request-only observations to a completed turn's history for capture assertions.
/// Tests inspect this separately from the destination-filtered HTTP/WS request.
pub async fn history_with_tool_call_metadata(
thread: &crate::CodexThread,
) -> Vec<codex_protocol::models::ResponseItem> {
let history = thread.conversation_history_snapshot().await;
let mut items = history.items().cloned().collect::<Vec<_>>();
thread
.session
.services
.executed_tool_calls
.attach_to_prompt(&mut items, &mut Default::default());
items
}
/// Test-only provider that supplies no user instructions.
#[derive(Debug, Default)]
pub struct EmptyUserInstructionsProvider;
impl UserInstructionsProvider for EmptyUserInstructionsProvider {
fn load_user_instructions(&self) -> LoadInstructionsFuture<'_> {
Box::pin(async { LoadedUserInstructions::default() })
}
}
pub fn set_thread_manager_test_mode(enabled: bool) {
thread_manager::set_thread_manager_test_mode_for_tests(enabled);
}
pub fn set_deterministic_process_ids(enabled: bool) {
unified_exec::set_deterministic_process_ids_for_tests(enabled);
}
pub fn auth_manager_from_auth(auth: CodexAuth) -> Arc<AuthManager> {
AuthManager::from_auth_for_testing(auth)
}
pub fn auth_manager_from_auth_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc<AuthManager> {
AuthManager::from_auth_for_testing_with_home(auth, codex_home)
}
pub fn with_code_mode_host_program(
thread_manager: ThreadManager,
host_program: PathBuf,
config: &crate::config::Config,
) -> ThreadManager {
thread_manager.with_code_mode_host_program_for_tests(host_program, config)
}
pub fn thread_manager_with_models_provider(
auth: CodexAuth,
provider: ModelProviderInfo,
) -> ThreadManager {
ThreadManager::with_models_provider_for_tests(auth, provider)
}
pub fn thread_manager_with_models_provider_and_home(
auth: CodexAuth,
provider: ModelProviderInfo,
codex_home: PathBuf,
environment_manager: Arc<EnvironmentManager>,
) -> ThreadManager {
ThreadManager::with_models_provider_and_home_for_tests(
auth,
provider,
codex_home,
environment_manager,
)
}
pub async fn start_thread_with_user_shell_override(
thread_manager: &ThreadManager,
config: Config,
user_shell_override: crate::shell::Shell,
supports_openai_form_elicitation: bool,
) -> codex_protocol::error::Result<crate::NewThread> {
thread_manager
.start_thread_with_user_shell_override_for_tests(
config,
user_shell_override,
ClientMcpExtensions::new(
supports_openai_form_elicitation
.then(|| (OPENAI_FORM_EXTENSION_ID.to_string(), serde_json::json!({}))),
),
)
.await
}
pub async fn resume_thread_from_rollout_with_user_shell_override(
thread_manager: &ThreadManager,
config: Config,
rollout_path: PathBuf,
auth_manager: Arc<AuthManager>,
user_shell_override: crate::shell::Shell,
supports_openai_form_elicitation: bool,
) -> codex_protocol::error::Result<crate::NewThread> {
thread_manager
.resume_thread_from_rollout_with_user_shell_override_for_tests(
config,
rollout_path,
auth_manager,
user_shell_override,
ClientMcpExtensions::new(
supports_openai_form_elicitation
.then(|| (OPENAI_FORM_EXTENSION_ID.to_string(), serde_json::json!({}))),
),
)
.await
}
pub fn models_manager_with_provider(
codex_home: PathBuf,
auth_manager: Arc<AuthManager>,
provider: ModelProviderInfo,
) -> SharedModelsManager {
let provider = create_model_provider(provider, Some(auth_manager));
provider.models_manager(codex_home, /*config_model_catalog*/ None)
}
pub fn default_http_client_factory() -> HttpClientFactory {
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault)
}
pub fn get_model_offline(model: Option<&str>) -> String {
get_model_offline_for_tests(model)
}
pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo {
construct_model_info_offline_for_tests(model, &config.to_models_manager_config())
}
#[derive(Clone, Copy)]
pub enum TestCodexResponsesRequestKind {
Turn,
Prewarm,
WebsocketConnection,
}
#[allow(clippy::too_many_arguments)]
pub fn responses_metadata(
installation_id: &str,
session_id: &str,
thread_id: &str,
turn_id: Option<&str>,
window_id: String,
session_source: &SessionSource,
parent_thread_id: Option<ThreadId>,
request_kind: TestCodexResponsesRequestKind,
) -> CodexResponsesMetadata {
let request_kind = match request_kind {
TestCodexResponsesRequestKind::Turn => Some(CodexResponsesRequestKind::Turn),
TestCodexResponsesRequestKind::Prewarm => Some(CodexResponsesRequestKind::Prewarm),
TestCodexResponsesRequestKind::WebsocketConnection => None,
};
CodexResponsesMetadata {
turn_id: request_kind.and(turn_id.map(ToString::to_string)),
request_kind,
parent_thread_id,
subagent_header: subagent_header_value(session_source),
subagent_kind: request_kind.and_then(|_| subagent_metadata_kind(session_source)),
..CodexResponsesMetadata::new(
installation_id.to_string(),
session_id.to_string(),
thread_id.to_string(),
window_id,
)
}
}
pub fn with_parent_turn(mut metadata: CodexResponsesMetadata, id: &str) -> CodexResponsesMetadata {
metadata.parent_turn_id = Some(id.to_string());
metadata
}
pub fn all_model_presets() -> &'static Vec<ModelPreset> {
&TEST_MODEL_PRESETS
}
pub fn builtin_collaboration_mode_presets() -> Vec<CollaborationModeMask> {
collaboration_mode_presets::builtin_collaboration_mode_presets()
}