Report I/O subtypes for session config import failures (#37723)

## What changed

- Append a stable `std::io::ErrorKind` category to the
  `failed_to_load_session_config` subtype, including categories such as
  `invalid_data`, `not_found`, and `permission_denied`.
- Propagate the categorized subtype through session import failure reporting.

## Testing

- Add an app-server test that verifies an invalid config reports
  `failed_to_load_session_config_invalid_data` in both the import completion
  notification and analytics event.

GitOrigin-RevId: e9a1a7cd36979911f422805180dd91b6ed65e14e
This commit is contained in:
charlesgong-openai
2026-08-09 18:37:06 +00:00
committed by copyberry
parent a16863f870
commit 50ef7395fa
2 changed files with 129 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::io::ErrorKind;
use std::path::PathBuf;
use std::sync::Arc;
@@ -170,7 +171,7 @@ impl ExternalAgentSessionImporter {
record_import_error(
&mut item_result,
stage,
Some(sub_error_type),
Some(sub_error_type.as_str()),
message,
Some(source_path.display().to_string()),
);
@@ -408,8 +409,25 @@ impl ExternalAgentSessionImporter {
)
.await
.map_err(|err| {
let io_kind = match err.kind() {
ErrorKind::NotFound => "not_found",
ErrorKind::PermissionDenied => "permission_denied",
ErrorKind::AlreadyExists => "already_exists",
ErrorKind::InvalidInput => "invalid_input",
ErrorKind::InvalidData => "invalid_data",
ErrorKind::IsADirectory => "is_a_directory",
ErrorKind::NotADirectory => "not_a_directory",
ErrorKind::TimedOut => "timed_out",
ErrorKind::WriteZero => "write_zero",
ErrorKind::UnexpectedEof => "unexpected_eof",
ErrorKind::StorageFull => "storage_full",
ErrorKind::QuotaExceeded => "quota_exceeded",
ErrorKind::FileTooLarge => "file_too_large",
ErrorKind::ReadOnlyFilesystem => "read_only_filesystem",
_ => "other",
};
SessionImportStepFailure::new(
"failed_to_load_session_config",
format!("failed_to_load_session_config_{io_kind}"),
format!("failed to load imported session config: {err}"),
)
})?;
@@ -583,18 +601,18 @@ struct SessionImportFailure {
source_path: PathBuf,
message: String,
stage: &'static str,
sub_error_type: &'static str,
sub_error_type: String,
}
struct SessionImportStepFailure {
sub_error_type: &'static str,
sub_error_type: String,
message: String,
}
impl SessionImportStepFailure {
fn new(sub_error_type: &'static str, message: String) -> Self {
fn new(sub_error_type: impl Into<String>, message: String) -> Self {
Self {
sub_error_type,
sub_error_type: sub_error_type.into(),
message,
}
}

View File

@@ -1379,6 +1379,111 @@ async fn external_agent_config_import_completed_tracks_analytics_event() -> Resu
Ok(())
}
#[tokio::test]
async fn external_agent_config_import_reports_session_config_error_subtype() -> Result<()> {
let analytics_server = start_analytics_events_server().await?;
let codex_home = TempDir::new()?;
write_analytics_config(codex_home.path(), &analytics_server.uri())?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-123")
.chatgpt_account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
let project_root = codex_home.path().join("repo");
let session_dir = external_agent_home(codex_home.path()).join("projects/repo");
let session_path = session_dir.join("session.jsonl");
let recent_timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
std::fs::create_dir_all(&project_root)?;
std::fs::create_dir_all(&session_dir)?;
std::fs::write(
&session_path,
serde_json::json!({
"type": "user",
"cwd": &project_root,
"timestamp": &recent_timestamp,
"message": { "content": "first request" },
})
.to_string(),
)?;
let home_dir = codex_home.path().display().to_string();
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
.await?;
std::fs::write(
codex_home.path().join("config.toml"),
"chatgpt_base_url = [",
)?;
let request_id = mcp
.send_raw_request(
"externalAgentConfig/import",
Some(serde_json::json!({
"source": "test_import",
"providerId": "test-provider-42",
"migrationItems": [{
"itemType": "SESSIONS",
"description": "Migrate recent sessions",
"cwd": null,
"details": {
"sessions": [{
"path": session_path,
"cwd": project_root,
"title": "first request"
}]
}
}]
})),
)
.await?;
let response: ExternalAgentConfigImportResponse =
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
let import_id = assert_import_response(response);
let completed: ExternalAgentConfigImportCompletedNotification = timeout(
DEFAULT_TIMEOUT,
mcp.read_notification("externalAgentConfig/import/completed"),
)
.await??;
assert_eq!(completed.import_id, import_id);
assert_eq!(completed.item_type_results.len(), 1);
assert_eq!(completed.item_type_results[0].successes.len(), 0);
assert_eq!(completed.item_type_results[0].failures.len(), 1);
let failure = &completed.item_type_results[0].failures[0];
assert_eq!(failure.failure_stage, "session_persist");
assert_eq!(
failure.sub_error_type.as_deref(),
Some("failed_to_load_session_config_invalid_data")
);
let event = wait_for_analytics_event(
&analytics_server,
DEFAULT_TIMEOUT,
"codex_onboarding_external_agent_import_failure",
)
.await?;
let event_params = &event["event_params"];
assert_eq!(event_params["import_id"], serde_json::json!(import_id));
assert_eq!(event_params["source"], "test_import");
assert_eq!(event_params["provider_id"], "test-provider-42");
assert_eq!(event_params["type"], "SESSIONS");
assert_eq!(event_params["failure_stage"], "session_persist");
assert_eq!(
event_params["sub_error_type"],
"failed_to_load_session_config_invalid_data"
);
assert!(event_params.get("raw_errors").is_none());
assert!(event_params.get("message").is_none());
Ok(())
}
#[tokio::test]
async fn external_agent_config_import_reinstalls_plugins_from_known_marketplaces() -> Result<()> {
let codex_home = TempDir::new()?;