mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Record external agent import results (#28396)
## Summary - restore `externalAgentConfig/import/progress` notifications while keeping `externalAgentConfig/import/completed` as the must-deliver event - persist completed external-agent config imports in state DB by `importId`, including concrete success/failure details for config, AGENTS.md, skills, plugins, MCP servers, subagents, hooks, commands, and sessions - add `externalAgentConfig/import/readHistories` so clients can recover persisted import results after missing the live completion notification - include `errorType` on import failures in protocol responses/notifications and persisted DB JSON so future code can classify failures without another wire/storage shape change ## Validation - `git diff --check` - `just test -p codex-state external_agent_config_imports` - `just test -p codex-app-server-protocol` - `CODEX_SQLITE_HOME=/private/tmp/codex-app-server-sqlite-read-details just test -p codex-app-server external_agent_config_import_sends_completion_notification_for_sync_only_import` Also ran earlier broader checks before publishing: - `just test -p codex-state` - `CODEX_SQLITE_HOME=/private/tmp/codex-app-server-external-agent-test-sqlite just test -p codex-app-server external_agent_config` - `just test -p codex-external-agent-migration`
This commit is contained in:
committed by
GitHub
Unverified
parent
1e015884c5
commit
314fa3d25b
@@ -0,0 +1,136 @@
|
||||
use super::StateRuntime;
|
||||
use crate::model::datetime_to_epoch_millis;
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use sqlx::Row;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExternalAgentConfigImportSuccessRecord {
|
||||
pub item_type: String,
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub source: Option<String>,
|
||||
pub target: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExternalAgentConfigImportFailureRecord {
|
||||
pub item_type: String,
|
||||
pub error_type: Option<String>,
|
||||
pub failure_stage: String,
|
||||
pub message: String,
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExternalAgentConfigImportDetailsRecord {
|
||||
pub successes: Vec<ExternalAgentConfigImportSuccessRecord>,
|
||||
pub failures: Vec<ExternalAgentConfigImportFailureRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExternalAgentConfigImportHistoryRecord {
|
||||
pub import_id: String,
|
||||
pub completed_at_ms: i64,
|
||||
pub successes: Vec<ExternalAgentConfigImportSuccessRecord>,
|
||||
pub failures: Vec<ExternalAgentConfigImportFailureRecord>,
|
||||
}
|
||||
|
||||
impl StateRuntime {
|
||||
pub async fn record_external_agent_config_import_completed(
|
||||
&self,
|
||||
import_id: &str,
|
||||
successes: &[ExternalAgentConfigImportSuccessRecord],
|
||||
failures: &[ExternalAgentConfigImportFailureRecord],
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO external_agent_config_imports (
|
||||
import_id,
|
||||
completed_at_ms,
|
||||
successes,
|
||||
failures
|
||||
) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(import_id) DO UPDATE SET
|
||||
completed_at_ms = excluded.completed_at_ms,
|
||||
successes = excluded.successes,
|
||||
failures = excluded.failures
|
||||
"#,
|
||||
)
|
||||
.bind(import_id)
|
||||
.bind(datetime_to_epoch_millis(Utc::now()))
|
||||
.bind(serde_json::to_string(successes)?)
|
||||
.bind(serde_json::to_string(failures)?)
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn external_agent_config_import_details_record(
|
||||
&self,
|
||||
import_id: &str,
|
||||
) -> anyhow::Result<Option<ExternalAgentConfigImportDetailsRecord>> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
successes,
|
||||
failures
|
||||
FROM external_agent_config_imports
|
||||
WHERE import_id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(import_id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
row.map(|row| {
|
||||
let successes: String = row.try_get("successes")?;
|
||||
let failures: String = row.try_get("failures")?;
|
||||
Ok(ExternalAgentConfigImportDetailsRecord {
|
||||
successes: serde_json::from_str(&successes)?,
|
||||
failures: serde_json::from_str(&failures)?,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub async fn external_agent_config_import_history_records(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<ExternalAgentConfigImportHistoryRecord>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
import_id,
|
||||
completed_at_ms,
|
||||
successes,
|
||||
failures
|
||||
FROM external_agent_config_imports
|
||||
ORDER BY completed_at_ms DESC, import_id ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(|row| {
|
||||
let import_id: String = row.try_get("import_id")?;
|
||||
let completed_at_ms: i64 = row.try_get("completed_at_ms")?;
|
||||
let successes: String = row.try_get("successes")?;
|
||||
let failures: String = row.try_get("failures")?;
|
||||
Ok(ExternalAgentConfigImportHistoryRecord {
|
||||
import_id,
|
||||
completed_at_ms,
|
||||
successes: serde_json::from_str(&successes)?,
|
||||
failures: serde_json::from_str(&failures)?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "external_agent_config_imports_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,145 @@
|
||||
use super::*;
|
||||
use crate::runtime::test_support::unique_temp_dir;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn records_completion_by_import_id() -> anyhow::Result<()> {
|
||||
let runtime = StateRuntime::init(unique_temp_dir(), "test-provider".to_string()).await?;
|
||||
|
||||
runtime
|
||||
.record_external_agent_config_import_completed(
|
||||
"import-1",
|
||||
&[ExternalAgentConfigImportSuccessRecord {
|
||||
item_type: "CONFIG".to_string(),
|
||||
cwd: None,
|
||||
source: Some("settings.json".to_string()),
|
||||
target: Some("config.toml".to_string()),
|
||||
}],
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
runtime
|
||||
.record_external_agent_config_import_completed(
|
||||
"import-1",
|
||||
&[
|
||||
ExternalAgentConfigImportSuccessRecord {
|
||||
item_type: "CONFIG".to_string(),
|
||||
cwd: None,
|
||||
source: Some("settings.json".to_string()),
|
||||
target: Some("config.toml".to_string()),
|
||||
},
|
||||
ExternalAgentConfigImportSuccessRecord {
|
||||
item_type: "MCP_SERVER_CONFIG".to_string(),
|
||||
cwd: None,
|
||||
source: Some("github".to_string()),
|
||||
target: Some("github".to_string()),
|
||||
},
|
||||
],
|
||||
&[ExternalAgentConfigImportFailureRecord {
|
||||
item_type: "MCP_SERVER_CONFIG".to_string(),
|
||||
error_type: None,
|
||||
failure_stage: "import".to_string(),
|
||||
message: "failed".to_string(),
|
||||
cwd: None,
|
||||
source: Some("broken".to_string()),
|
||||
}],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
runtime
|
||||
.external_agent_config_import_details_record("import-1")
|
||||
.await?,
|
||||
Some(ExternalAgentConfigImportDetailsRecord {
|
||||
successes: vec![
|
||||
ExternalAgentConfigImportSuccessRecord {
|
||||
item_type: "CONFIG".to_string(),
|
||||
cwd: None,
|
||||
source: Some("settings.json".to_string()),
|
||||
target: Some("config.toml".to_string()),
|
||||
},
|
||||
ExternalAgentConfigImportSuccessRecord {
|
||||
item_type: "MCP_SERVER_CONFIG".to_string(),
|
||||
cwd: None,
|
||||
source: Some("github".to_string()),
|
||||
target: Some("github".to_string()),
|
||||
}
|
||||
],
|
||||
failures: vec![ExternalAgentConfigImportFailureRecord {
|
||||
item_type: "MCP_SERVER_CONFIG".to_string(),
|
||||
error_type: None,
|
||||
failure_stage: "import".to_string(),
|
||||
message: "failed".to_string(),
|
||||
cwd: None,
|
||||
source: Some("broken".to_string()),
|
||||
}],
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
runtime
|
||||
.external_agent_config_import_history_records()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|record| (
|
||||
record.import_id,
|
||||
record.successes,
|
||||
record.failures,
|
||||
record.completed_at_ms > 0
|
||||
))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(
|
||||
"import-1".to_string(),
|
||||
vec![
|
||||
ExternalAgentConfigImportSuccessRecord {
|
||||
item_type: "CONFIG".to_string(),
|
||||
cwd: None,
|
||||
source: Some("settings.json".to_string()),
|
||||
target: Some("config.toml".to_string()),
|
||||
},
|
||||
ExternalAgentConfigImportSuccessRecord {
|
||||
item_type: "MCP_SERVER_CONFIG".to_string(),
|
||||
cwd: None,
|
||||
source: Some("github".to_string()),
|
||||
target: Some("github".to_string()),
|
||||
}
|
||||
],
|
||||
vec![ExternalAgentConfigImportFailureRecord {
|
||||
item_type: "MCP_SERVER_CONFIG".to_string(),
|
||||
error_type: None,
|
||||
failure_stage: "import".to_string(),
|
||||
message: "failed".to_string(),
|
||||
cwd: None,
|
||||
source: Some("broken".to_string()),
|
||||
}],
|
||||
true
|
||||
)]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reads_all_history_records() -> anyhow::Result<()> {
|
||||
let runtime = StateRuntime::init(unique_temp_dir(), "test-provider".to_string()).await?;
|
||||
|
||||
runtime
|
||||
.record_external_agent_config_import_completed("import-1", &[], &[])
|
||||
.await?;
|
||||
runtime
|
||||
.record_external_agent_config_import_completed("import-2", &[], &[])
|
||||
.await?;
|
||||
|
||||
let mut records = runtime
|
||||
.external_agent_config_import_history_records()
|
||||
.await?;
|
||||
records.sort_by(|left, right| left.import_id.cmp(&right.import_id));
|
||||
assert_eq!(
|
||||
records
|
||||
.into_iter()
|
||||
.map(|record| record.import_id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["import-1".to_string(), "import-2".to_string()]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user