fix(agents): speed up detection and sync ZCode default model

This commit is contained in:
lzt404 committed 2026-08-15 15:28:01 +08:00
1 parent 607c930d7b
commit b312b904a2
9 files changed
+525 -96

No files matched your search

+65 -17
View File
@@ -1,5 +1,13 @@
use super::*;
const AGENT_STATUS_DETECTION_CONCURRENCY: usize = 4;
#[derive(Clone, Copy)]
enum AgentStatusDetectionTarget {
Client(AgentClient),
PiProvider,
}
pub(crate) fn inspect_agent_config_statuses(
app: &tauri::AppHandle,
config: &GuiConfigFile,
@@ -9,23 +17,63 @@ pub(crate) fn inspect_agent_config_statuses(
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
let api_key = effective_agent_api_key(config);
let mut statuses = [
AgentClient::ClaudeCode,
AgentClient::ClaudeDesktop,
AgentClient::Codex,
AgentClient::OpenCode,
AgentClient::OpenClaw,
AgentClient::Hermes,
AgentClient::DeepSeekHarness,
AgentClient::ZCode,
AgentClient::KimiCode,
AgentClient::GrokBuild,
]
.into_iter()
.map(|client| inspect_agent_config(client, &home, config.port, api_key))
.collect::<Vec<_>>();
statuses.push(inspect_pi_provider_status(&home, config.port, api_key));
Ok(statuses)
let targets = [
AgentStatusDetectionTarget::Client(AgentClient::ClaudeCode),
AgentStatusDetectionTarget::Client(AgentClient::ClaudeDesktop),
AgentStatusDetectionTarget::Client(AgentClient::Codex),
AgentStatusDetectionTarget::Client(AgentClient::OpenCode),
AgentStatusDetectionTarget::Client(AgentClient::OpenClaw),
AgentStatusDetectionTarget::Client(AgentClient::Hermes),
AgentStatusDetectionTarget::Client(AgentClient::DeepSeekHarness),
AgentStatusDetectionTarget::Client(AgentClient::ZCode),
AgentStatusDetectionTarget::Client(AgentClient::KimiCode),
AgentStatusDetectionTarget::Client(AgentClient::GrokBuild),
AgentStatusDetectionTarget::PiProvider,
];
let queue = Mutex::new(targets.into_iter().enumerate());
let results = Mutex::new(vec![None; targets.len()]);
thread::scope(|scope| {
let mut workers = Vec::with_capacity(AGENT_STATUS_DETECTION_CONCURRENCY);
for _ in 0..AGENT_STATUS_DETECTION_CONCURRENCY {
let queue = &queue;
let results = &results;
let home = &home;
workers.push(scope.spawn(move || -> Result<(), String> {
loop {
let next = queue
.lock()
.map_err(|_| "智能体检测任务队列锁已损坏".to_string())?
.next();
let Some((index, target)) = next else {
return Ok(());
};
let status = match target {
AgentStatusDetectionTarget::Client(client) => {
inspect_agent_config(client, home, config.port, api_key)
}
AgentStatusDetectionTarget::PiProvider => {
inspect_pi_provider_status(home, config.port, api_key)
}
};
results
.lock()
.map_err(|_| "智能体检测结果锁已损坏".to_string())?[index] = Some(status);
}
}));
}
for worker in workers {
worker
.join()
.map_err(|_| "智能体检测工作线程异常退出".to_string())??;
}
Ok::<(), String>(())
})?;
results
.into_inner()
.map_err(|_| "智能体检测结果锁已损坏".to_string())?
.into_iter()
.map(|status| status.ok_or_else(|| "智能体检测结果不完整".to_string()))
.collect()
}
pub(crate) fn refresh_agent_config_status_cache(
+100 -37
View File
@@ -215,16 +215,31 @@ pub(crate) fn build_agent_updates_with_oauth(
])
}
AgentClient::ZCode => {
let before = read_optional_text(&paths[0])?;
let after =
build_zcode_agent_config(before.as_deref(), &root_base, api_key, model, models)
let app_before = read_optional_text(&paths[0])?;
let cli_before = read_optional_text(&paths[1])?;
let app_after =
build_zcode_agent_config(app_before.as_deref(), &root_base, api_key, model, models)
.or_else(|_| {
build_zcode_agent_config(None, &root_base, api_key, model, models)
})?;
Ok(vec![AgentFileUpdate {
path: paths[0].clone(),
after,
}])
let cli_after = build_zcode_cli_agent_config(
cli_before.as_deref(),
&root_base,
api_key,
model,
models,
)
.or_else(|_| build_zcode_cli_agent_config(None, &root_base, api_key, model, models))?;
Ok(vec![
AgentFileUpdate {
path: paths[0].clone(),
after: app_after,
},
AgentFileUpdate {
path: paths[1].clone(),
after: cli_after,
},
])
}
AgentClient::KimiCode => {
let before = read_optional_text(&paths[0])?;
@@ -1031,35 +1046,59 @@ pub(crate) fn remove_opencode_managed_configuration(
}
pub(crate) fn remove_zcode_managed_configuration(paths: &[PathBuf]) -> Result<Vec<String>, String> {
let Some(path) = paths.first() else {
if paths.is_empty() {
return Err("ZCode 当前平台配置路径不可用".to_string());
};
}
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let updated = update_agent_json_file(path, "ZCode 配置", |root| {
let mut changed = false;
if root
.get("model")
.and_then(serde_json::Value::as_str)
.is_some_and(|model| model.starts_with(&prefix))
{
root.remove("model");
changed = true;
let mut changed_paths = Vec::new();
for path in paths {
let updated = update_agent_json_file(path, "ZCode 配置", |root| {
let mut changed = false;
let remove_model = if root
.get("model")
.and_then(serde_json::Value::as_str)
.is_some_and(|model| model.starts_with(&prefix))
{
true
} else if let Some(model) = root
.get_mut("model")
.and_then(serde_json::Value::as_object_mut)
{
if model
.get("main")
.and_then(serde_json::Value::as_str)
.is_some_and(|model| model.starts_with(&prefix))
{
model.remove("main");
changed = true;
}
model.is_empty()
} else {
false
};
if remove_model {
root.remove("model");
changed = true;
}
let providers_empty = if let Some(providers) = root
.get_mut("provider")
.and_then(serde_json::Value::as_object_mut)
{
changed |= providers.remove(MANAGED_AGENT_PROVIDER_ID).is_some();
providers.is_empty()
} else {
false
};
if providers_empty {
root.remove("provider");
}
changed
})?;
if updated {
changed_paths.push(path_to_string(path));
}
let providers_empty = if let Some(providers) = root
.get_mut("provider")
.and_then(serde_json::Value::as_object_mut)
{
changed |= providers.remove(MANAGED_AGENT_PROVIDER_ID).is_some();
providers.is_empty()
} else {
false
};
if providers_empty {
root.remove("provider");
}
changed
})?;
Ok(updated.then(|| path_to_string(path)).into_iter().collect())
}
Ok(changed_paths)
}
pub(crate) fn remove_kimi_code_managed_configuration(
@@ -2706,6 +2745,27 @@ pub(crate) fn build_zcode_agent_config(
api_key: &str,
model: &str,
available_models: &[AgentModelOption],
) -> Result<String, String> {
build_zcode_config(existing, base_url, api_key, model, available_models, false)
}
pub(crate) fn build_zcode_cli_agent_config(
existing: Option<&str>,
base_url: &str,
api_key: &str,
model: &str,
available_models: &[AgentModelOption],
) -> Result<String, String> {
build_zcode_config(existing, base_url, api_key, model, available_models, true)
}
fn build_zcode_config(
existing: Option<&str>,
base_url: &str,
api_key: &str,
model: &str,
available_models: &[AgentModelOption],
cli_config: bool,
) -> Result<String, String> {
let mut root = parse_agent_json_object(existing, "ZCode config.json")?;
let providers = ensure_json_object_entry(&mut root, "provider");
@@ -2739,10 +2799,13 @@ pub(crate) fn build_zcode_agent_config(
options.insert("baseURL".to_string(), serde_json::json!(base_url));
options.insert("apiKey".to_string(), serde_json::json!(api_key));
managed_provider.insert("models".to_string(), serde_json::Value::Object(models));
root.insert(
"model".to_string(),
serde_json::json!(format!("{MANAGED_AGENT_PROVIDER_ID}/{model}")),
);
let model = format!("{MANAGED_AGENT_PROVIDER_ID}/{model}");
if cli_config {
ensure_json_object_entry(&mut root, "model")
.insert("main".to_string(), serde_json::json!(model));
} else {
root.insert("model".to_string(), serde_json::json!(model));
}
render_agent_json(root, "ZCode 配置")
}
+128 -27
View File
@@ -1,5 +1,8 @@
use super::*;
const AGENT_VERSION_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
const AGENT_VERSION_PROBE_POLL_INTERVAL: Duration = Duration::from_millis(20);
pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBuf> {
match client {
AgentClient::ClaudeCode => {
@@ -32,7 +35,10 @@ pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBu
directory.join(DEEPSEEK_HARNESS_CREDENTIALS_FILE),
]
}
AgentClient::ZCode => vec![home.join(".zcode/v2").join(ZCODE_CONFIG_FILE)],
AgentClient::ZCode => vec![
home.join(".zcode/v2").join(ZCODE_CONFIG_FILE),
home.join(".zcode/cli").join(ZCODE_CONFIG_FILE),
],
AgentClient::KimiCode => vec![kimi_code_home(home).join(KIMI_CODE_CONFIG_FILE)],
AgentClient::GrokBuild => vec![grok_build_home(home).join(GROK_BUILD_CONFIG_FILE)],
}
@@ -968,8 +974,19 @@ pub(crate) fn inspect_agent_managed_config(
.map(|(configured, model)| (configured, model, false)),
AgentClient::DeepSeekHarness => inspect_deepseek_harness_config(paths, port, api_key)
.map(|(configured, model)| (configured, model, false)),
AgentClient::ZCode => inspect_zcode_agent_config(&paths[0], port, api_key)
.map(|(configured, model)| (configured, model, false)),
AgentClient::ZCode => {
if paths.len() != 2 {
return Err("ZCode 配置路径数量无效".to_string());
}
let (app_configured, app_model) = inspect_zcode_agent_config(&paths[0], port, api_key)?;
let (cli_configured, cli_model) = inspect_zcode_agent_config(&paths[1], port, api_key)?;
let models_match = app_model.is_some() && app_model == cli_model;
Ok((
app_configured && cli_configured && models_match,
cli_model.or(app_model),
false,
))
}
AgentClient::KimiCode => inspect_kimi_code_agent_config(&paths[0], port, api_key)
.map(|(configured, model)| (configured, model, false)),
AgentClient::GrokBuild => inspect_grok_build_agent_config(&paths[0], port, api_key)
@@ -1094,20 +1111,29 @@ pub(crate) fn agent_has_managed_marker(
}
AgentClient::DeepSeekHarness => deepseek_harness_has_managed_marker(paths),
AgentClient::ZCode => {
if !paths[0].is_file() {
return Ok(false);
}
let root = read_agent_json_or_empty(&paths[0], "ZCode 配置")?;
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let provider_exists = root
.get("provider")
.and_then(|value| value.get(MANAGED_AGENT_PROVIDER_ID))
.is_some();
let model_selected = root
.get("model")
.and_then(serde_json::Value::as_str)
.is_some_and(|model| model.starts_with(&prefix));
Ok(provider_exists && model_selected)
for path in paths {
if !path.is_file() {
continue;
}
let root = read_agent_json_or_empty(path, "ZCode 配置")?;
let provider_exists = root
.get("provider")
.and_then(|value| value.get(MANAGED_AGENT_PROVIDER_ID))
.is_some();
let model_selected = root
.get("model")
.and_then(|model| {
model
.as_str()
.or_else(|| model.get("main").and_then(serde_json::Value::as_str))
})
.is_some_and(|model| model.starts_with(&prefix));
if provider_exists && model_selected {
return Ok(true);
}
}
Ok(false)
}
AgentClient::KimiCode => inspect_managed_toml_model_marker(
&paths[0],
@@ -1322,11 +1348,12 @@ pub(crate) fn find_zcode_desktop_executable(home: &Path) -> Option<PathBuf> {
pub(crate) fn read_macos_app_version(application: &Path) -> Option<String> {
let info_plist = application.join("Contents/Info.plist");
for key in ["CFBundleShortVersionString", "CFBundleVersion"] {
let output = Command::new("/usr/bin/plutil")
let mut command = Command::new("/usr/bin/plutil");
command
.args(["-extract", key, "raw", "-o", "-"])
.arg(&info_plist)
.output()
.ok()?;
.arg(&info_plist);
let output =
command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
if !output.status.success() {
continue;
}
@@ -1420,7 +1447,7 @@ if ($package -and $package.Version) {
&encoded_command,
]);
configure_background_command(&mut command);
let output = command.output().ok()?;
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
if !output.status.success() {
return None;
}
@@ -1456,7 +1483,7 @@ if ($package -and $package.Version) {
&encoded_command,
]);
configure_background_command(&mut command);
let output = command.output().ok()?;
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
if !output.status.success() {
return None;
}
@@ -1543,7 +1570,7 @@ foreach ($shortcutFile in (Get-ChildItem -LiteralPath $shortcutRoots -Filter '*.
&encoded_command,
]);
configure_background_command(&mut command);
let output = command.output().ok()?;
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
if !output.status.success() {
return None;
}
@@ -1596,7 +1623,7 @@ if ($version) {{
&encoded_command,
]);
configure_background_command(&mut command);
let output = command.output().ok()?;
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
if !output.status.success() {
return None;
}
@@ -1642,7 +1669,9 @@ pub(crate) fn find_windows_codex_app_id_via_registry() -> Option<String> {
let mut command = Command::new(windows_registry_executable());
command.args(["query", PACKAGES_KEY, "/f", package_name, "/k", "/s"]);
configure_background_command(&mut command);
let Ok(output) = command.output() else {
let Ok(Some(output)) =
command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT)
else {
continue;
};
if !output.status.success() {
@@ -1914,6 +1943,74 @@ pub(crate) fn windows_batch_executable_argument(executable: &Path) -> String {
format!("\"{}\"", path_to_string(executable))
}
pub(crate) fn command_output_with_timeout(
command: &mut Command,
timeout: Duration,
) -> io::Result<Option<std::process::Output>> {
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn()?;
let mut stdout = child
.stdout
.take()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "无法读取智能体探测标准输出"))?;
let mut stderr = child
.stderr
.take()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "无法读取智能体探测错误输出"))?;
thread::scope(|scope| {
let stdout_reader = scope.spawn(move || {
let mut output = Vec::new();
stdout.read_to_end(&mut output).map(|_| output)
});
let stderr_reader = scope.spawn(move || {
let mut output = Vec::new();
stderr.read_to_end(&mut output).map(|_| output)
});
let deadline = Instant::now() + timeout;
let status = loop {
if let Some(status) = child.try_wait()? {
break Some(status);
}
if Instant::now() >= deadline {
terminate_agent_probe_process(&mut child);
break None;
}
thread::sleep(AGENT_VERSION_PROBE_POLL_INTERVAL);
};
let stdout = stdout_reader
.join()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "智能体探测输出线程异常退出"))??;
let stderr = stderr_reader
.join()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "智能体探测错误线程异常退出"))??;
Ok(status.map(|status| std::process::Output {
status,
stdout,
stderr,
}))
})
}
fn terminate_agent_probe_process(child: &mut Child) {
#[cfg(target_os = "windows")]
{
let process_id = child.id().to_string();
let mut command = Command::new("taskkill");
command
.args(["/PID", &process_id, "/T", "/F"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
configure_background_command(&mut command);
let _ = command.status();
}
let _ = child.kill();
let _ = child.wait();
}
pub(crate) fn read_agent_version(path: &Path) -> Option<String> {
#[cfg(target_os = "windows")]
let mut command = windows_command_for_executable(path, false);
@@ -1923,7 +2020,7 @@ pub(crate) fn read_agent_version(path: &Path) -> Option<String> {
command.arg("--version");
configure_background_command(&mut command);
let output = command.output().ok()?;
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
if !output.status.success() {
return None;
}
@@ -2471,7 +2568,11 @@ pub(crate) fn inspect_zcode_agent_config(
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let model = root
.get("model")
.and_then(serde_json::Value::as_str)
.and_then(|model| {
model
.as_str()
.or_else(|| model.get("main").and_then(serde_json::Value::as_str))
})
.map(str::trim)
.filter(|value| !value.is_empty())
.and_then(|value| value.strip_prefix(&prefix))
+44 -15
View File
@@ -189,7 +189,11 @@ pub(crate) fn validate_agent_applied_state(
.map(|file| file.path.clone())
.collect::<Vec<_>>();
let valid_paths = state_paths == expected_paths
|| (client == AgentClient::Codex && state_paths.as_slice() == paths);
|| (client == AgentClient::Codex && state_paths.as_slice() == paths)
|| (client == AgentClient::ZCode
&& paths
.first()
.is_some_and(|path| state_paths == [path.clone()]));
if !valid_paths {
return Err("智能体应用状态文件数量或路径不匹配".to_string());
}
@@ -385,7 +389,11 @@ pub(crate) fn validate_agent_record(
.map(|file| file.path.clone())
.collect::<Vec<_>>();
let valid_paths = record_paths.as_slice() == paths
|| (client == AgentClient::Codex && record_paths == expected_paths);
|| (client == AgentClient::Codex && record_paths == expected_paths)
|| (client == AgentClient::ZCode
&& paths
.first()
.is_some_and(|path| record_paths == [path.clone()]));
if !valid_paths {
return Err("智能体备份状态文件数量或路径不匹配".to_string());
}
@@ -728,9 +736,10 @@ pub(crate) fn fresh_agent_contents_with_oauth(
build_deepseek_harness_settings(None, &openai_base, model, models)?,
build_deepseek_harness_credentials(None, api_key)?,
]),
AgentClient::ZCode => Ok(vec![build_zcode_agent_config(
None, &root_base, api_key, model, models,
)?]),
AgentClient::ZCode => Ok(vec![
build_zcode_agent_config(None, &root_base, api_key, model, models)?,
build_zcode_cli_agent_config(None, &root_base, api_key, model, models)?,
]),
AgentClient::KimiCode => Ok(vec![build_kimi_code_agent_config(
None,
&openai_base,
@@ -1430,14 +1439,36 @@ pub(crate) fn commit_agent_configuration(
.collect::<Result<Vec<_>, String>>()?;
snapshots.push((state_path.clone(), previous_state));
let created_session_backups = existing_state
.as_ref()
.is_none_or(|state| state.backup_files.is_empty());
let mut created_backup_paths = Vec::new();
let backup_files =
if let Some(state) = existing_state.filter(|state| !state.backup_files.is_empty()) {
state.backup_files
let mut backup_files = state.backup_files;
let missing_updates = updates
.iter()
.filter(|update| !backup_files.iter().any(|file| file.path == update.path))
.map(|update| AgentFileUpdate {
path: update.path.clone(),
after: update.after.clone(),
})
.collect::<Vec<_>>();
let added_backups = prepare_agent_session_backups(client, &missing_updates)?;
created_backup_paths.extend(
added_backups
.iter()
.filter(|file| file.existed_before)
.map(|file| file.backup_path.clone()),
);
backup_files.extend(added_backups);
backup_files
} else {
prepare_agent_session_backups(client, updates)?
let backup_files = prepare_agent_session_backups(client, updates)?;
created_backup_paths.extend(
backup_files
.iter()
.filter(|file| file.existed_before)
.map(|file| file.backup_path.clone()),
);
backup_files
};
let transaction = (|| -> Result<Vec<String>, String> {
@@ -1484,11 +1515,9 @@ pub(crate) fn commit_agent_configuration(
}
Err(error) => {
let rollback = restore_agent_snapshots_direct(&snapshots);
if created_session_backups {
for backup in &backup_files {
if backup.existed_before && backup.backup_path.is_file() {
let _ = fs::remove_file(&backup.backup_path);
}
for backup_path in created_backup_paths {
if backup_path.is_file() {
let _ = fs::remove_file(backup_path);
}
}
match rollback {
+105
View File
@@ -684,6 +684,55 @@ fn zcode_agent_config_preserves_other_providers_and_uses_anthropic_messages() {
fs::remove_dir_all(home).unwrap();
}
#[test]
fn zcode_cli_config_sets_main_model_and_both_configs_must_match() {
let home = agent_test_home("zcode-cli-default-model");
let paths = agent_config_paths(AgentClient::ZCode, &home);
fs::create_dir_all(paths[0].parent().unwrap()).unwrap();
fs::create_dir_all(paths[1].parent().unwrap()).unwrap();
let models = test_agent_models(&["gpt-test", "deepseek-test"]);
let app_config = build_zcode_agent_config(
None,
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"gpt-test",
&models,
)
.unwrap();
let cli_config = build_zcode_cli_agent_config(
Some(r#"{"model":{"lite":"other/lite"},"plugins":{"keep":true}}"#),
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"gpt-test",
&models,
)
.unwrap();
let cli_value: serde_json::Value = serde_json::from_str(&cli_config).unwrap();
assert_eq!(cli_value["model"]["main"], "cpa-gui/gpt-test");
assert_eq!(cli_value["model"]["lite"], "other/lite");
assert_eq!(cli_value["plugins"]["keep"], true);
fs::write(&paths[0], app_config).unwrap();
fs::write(&paths[1], &cli_config).unwrap();
assert_eq!(
inspect_agent_managed_config(AgentClient::ZCode, &paths, 8317, DEFAULT_API_KEY).unwrap(),
(true, Some("gpt-test".to_string()), false)
);
let mut mismatched = cli_value;
mismatched["model"]["main"] = serde_json::json!("cpa-gui/deepseek-test");
fs::write(
&paths[1],
serde_json::to_string_pretty(&mismatched).unwrap(),
)
.unwrap();
assert_eq!(
inspect_agent_managed_config(AgentClient::ZCode, &paths, 8317, DEFAULT_API_KEY).unwrap(),
(false, Some("deepseek-test".to_string()), false)
);
fs::remove_dir_all(home).unwrap();
}
#[test]
fn zcode_inspection_requires_the_default_model_to_belong_to_the_managed_provider() {
let home = agent_test_home("zcode-default-model-inspection");
@@ -1583,6 +1632,62 @@ fn detected_agent_version_requires_a_real_version_value() {
assert!(normalize_detected_agent_version(&"1".repeat(257)).is_none());
}
#[test]
fn agent_probe_command_captures_output_before_timeout() {
#[cfg(target_os = "windows")]
let mut command = {
let mut command = Command::new(windows_command_processor());
command.args(["/D", "/C", "echo agent 1.2.3"]);
configure_background_command(&mut command);
command
};
#[cfg(not(target_os = "windows"))]
let mut command = {
let mut command = Command::new("sh");
command.args(["-c", "printf 'agent 1.2.3\\n'"]);
command
};
let output = command_output_with_timeout(&mut command, Duration::from_secs(2))
.unwrap()
.unwrap();
assert!(output.status.success());
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"agent 1.2.3"
);
}
#[test]
fn agent_probe_command_stops_at_timeout() {
#[cfg(target_os = "windows")]
let mut command = {
let mut command = Command::new(windows_powershell_executable());
command.args([
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-Command",
"Start-Sleep -Seconds 5",
]);
configure_background_command(&mut command);
command
};
#[cfg(not(target_os = "windows"))]
let mut command = {
let mut command = Command::new("sh");
command.args(["-c", "sleep 5"]);
command
};
let started = Instant::now();
let output = command_output_with_timeout(&mut command, Duration::from_millis(100)).unwrap();
assert!(output.is_none());
assert!(started.elapsed() < Duration::from_secs(3));
}
#[test]
fn codex_model_list_is_empty_when_cpa_has_no_writable_models() {
let prepared = prepare_codex_agent_models(&[]).unwrap();
+65
View File
@@ -424,6 +424,71 @@ fn zcode_runtime_edits_survive_close_and_original_provider_is_restored() {
fs::remove_dir_all(home).unwrap();
}
#[test]
fn zcode_legacy_single_file_state_upgrades_and_restores_cli_config() {
let home = agent_test_home("zcode-legacy-state-upgrade");
let paths = agent_config_paths(AgentClient::ZCode, &home);
fs::create_dir_all(paths[0].parent().unwrap()).unwrap();
fs::create_dir_all(paths[1].parent().unwrap()).unwrap();
let original_app = br#"{"model":"other/original","provider":{"other":{"keep":true}}}"#;
let original_cli =
br#"{"model":{"main":"other/original","lite":"other/lite"},"plugins":{"keep":true}}"#;
let models = test_agent_models(&["gpt-one", "gpt-two"]);
let managed_app = build_zcode_agent_config(
Some(std::str::from_utf8(original_app).unwrap()),
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"gpt-one",
&models,
)
.unwrap();
fs::write(&paths[0], managed_app).unwrap();
fs::write(&paths[1], original_cli).unwrap();
let app_backup = dated_agent_backup_path(&paths[0]).unwrap();
fs::write(&app_backup, original_app).unwrap();
let state_path = agent_state_path(&paths).unwrap();
write_agent_applied_state(
&state_path,
&AgentAppliedState {
version: AGENT_APPLIED_STATE_VERSION,
client: AgentClient::ZCode.id().to_string(),
model: "gpt-one".to_string(),
claude_desktop_model_mappings: None,
backup_files: vec![AgentAppliedBackupFile {
path: paths[0].clone(),
backup_path: app_backup,
existed_before: true,
}],
updated_at_unix: 1,
},
)
.unwrap();
apply_agent_configuration(
AgentClient::ZCode,
&home,
8317,
DEFAULT_API_KEY,
"gpt-two",
&models,
None,
)
.unwrap();
let upgraded = load_agent_applied_state(AgentClient::ZCode, &home)
.unwrap()
.unwrap();
assert_eq!(upgraded.backup_files.len(), 2);
let cli_value: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&paths[1]).unwrap()).unwrap();
assert_eq!(cli_value["model"]["main"], "cpa-gui/gpt-two");
restore_agent_session_configuration(AgentClient::ZCode, &home).unwrap();
assert_eq!(fs::read(&paths[0]).unwrap(), original_app);
assert_eq!(fs::read(&paths[1]).unwrap(), original_cli);
assert!(!state_path.exists());
fs::remove_dir_all(home).unwrap();
}
#[test]
fn session_merge_preserves_runtime_fields_for_other_agent_formats() {
let claude_original = r#"{"env":{"KEEP_ENV":"original"},"keep":"claude"}"#;
+1
View File
@@ -885,6 +885,7 @@ export function AgentsPage() {
const configurationAction = resolveAgentConfigurationAction({
client: selected,
modificationState: activeStatus?.modificationState ?? 'unconfigured',
configurationSynchronized: Boolean(activeStatus?.configured),
selectedModel,
appliedModel,
oauthConfiguration,
+3
View File
@@ -32,6 +32,7 @@ export type AgentModelMappings = {
type ResolveAgentConfigurationActionOptions = {
client: AgentConfigurationClientId;
modificationState: AgentConfigurationModificationState;
configurationSynchronized?: boolean;
selectedModel: string;
appliedModel: string;
oauthConfiguration: boolean;
@@ -82,6 +83,7 @@ export const resolveAgentModelMappingsDraftSourceForClient = <T>(
export function resolveAgentConfigurationAction({
client,
modificationState,
configurationSynchronized = true,
selectedModel,
appliedModel,
oauthConfiguration,
@@ -91,6 +93,7 @@ export function resolveAgentConfigurationAction({
}: ResolveAgentConfigurationActionOptions): AgentConfigurationAction {
if (modificationState !== 'applied') return 'apply';
if (client === 'pi') return 'close';
if (client === 'zcode' && !configurationSynchronized) return 'update';
const modelChanged = client === 'claude-code' || client === 'claude-desktop'
? !sameAgentModelMappings(modelMappings, appliedModelMappings)
+14
View File
@@ -161,6 +161,20 @@ describe('agent configuration update action', () => {
expect(appliedConfiguration(client, ' MODEL-A ')).toBe('close');
});
test('ZCode updates an applied legacy configuration even when the model is unchanged', () => {
expect(resolveAgentConfigurationAction({
client: 'zcode',
modificationState: 'applied',
configurationSynchronized: false,
selectedModel: 'model-a',
appliedModel: 'model-a',
oauthConfiguration: false,
appliedOauthConfiguration: false,
modelMappings: { opus: '', sonnet: '', haiku: '' },
appliedModelMappings: { opus: '', sonnet: '', haiku: '' },
})).toBe('update');
});
test('Codex also updates when only OAuth configuration changes', () => {
expect(resolveAgentConfigurationAction({
client: 'codex',