From 068940495abe1a33176e215976a1080d94db7e51 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 21 Aug 2026 07:43:26 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20am=20setup=20=E2=80=94=20ecrire=20settin?= =?UTF-8?q?gs.providers..base=5Furl=20+=20default=5Fmodel=20(top-leve?= =?UTF-8?q?l=20'providers:'=20rejete=20par=20le=20validateur)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le wizard persistait le modele par defaut sous la cle 'providers..default_model' -> bloc 'providers:' au top-level de la config user -> 'unknown field providers' (le registre vit sous settings.providers ; le schema exige base_url quand un bloc provider est ecrit). Regression couverte par setup_persists_under_settings_providers. v1.1.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- ROADMAP.md | 8 +++++++ src/setup.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 361a1ab..8630918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "agent-manager" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index e040278..f3d11dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-manager" -version = "1.1.0" +version = "1.1.1" edition = "2021" description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog." license = "MIT" diff --git a/ROADMAP.md b/ROADMAP.md index 2aa073b..9b48f15 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -62,6 +62,14 @@ > 6 rôles copilot générés pour aichat (am-copilot, am-operator, am-dev, > am-do, am-analyst, am-orchestrator), `am ai --role `, tip > d'onboarding dans le banner REPL et les commandes IA (non-bloquant). +> +> 🔧 **Maintenance v1.1.1 (2026-08-20)** : fix `am setup` — le wizard écrivait +> `providers.` au TOP-LEVEL de la config user (clé rejetée par le +> validateur : « unknown field `providers` »). Le registre vivant sous +> `settings.providers`, le wizard écrit désormais +> `settings.providers..base_url` + `.default_model` (le schéma exige +> base_url quand un bloc provider est écrit). Test de régression +> `setup_persists_under_settings_providers`. --- diff --git a/src/setup.rs b/src/setup.rs index 056012c..f6291d3 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -229,10 +229,26 @@ pub fn run(app: &App) -> Result { } } - // 3) Default model. + // 3) Default model. The registry lives under settings.providers — a + // top-level `providers:` key is rejected by the schema validator, and a + // provider block written to the user config requires base_url too. let model = pick_model(app, &provider)?; - if provider != "custom" { - config_cmd::persist_setting(app, &format!("providers.{provider}.default_model"), &model)?; + if provider == "ollama" { + // base_url was already written by the Add above. + config_cmd::persist_setting(app, "settings.providers.ollama.default_model", &model)?; + } else if provider != "custom" { + if let Some(def) = crate::providers::get(&app.config, &provider) { + config_cmd::persist_setting( + app, + &format!("settings.providers.{provider}.base_url"), + &def.base_url, + )?; + config_cmd::persist_setting( + app, + &format!("settings.providers.{provider}.default_model"), + &model, + )?; + } } // 4) Default provider. @@ -295,11 +311,16 @@ mod tests { use crate::app::App; use crate::cli::Cli; use clap::Parser; + use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; static COUNTER: AtomicU32 = AtomicU32::new(0); fn app_with(settings_yaml: &str) -> App { + app_with_path(settings_yaml).0 + } + + fn app_with_path(settings_yaml: &str) -> (App, PathBuf) { let id = COUNTER.fetch_add(1, Ordering::SeqCst); let dir = std::env::temp_dir().join(format!( "am-setup-test-{}-{id}", @@ -315,7 +336,7 @@ mod tests { ) .unwrap(); let cli = Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]); - App::from_cli(cli).unwrap() + (App::from_cli(cli).unwrap(), cfg) } #[test] @@ -333,4 +354,36 @@ mod tests { ); assert!(!needs_setup(&app)); } + + #[test] + fn setup_persists_under_settings_providers() { + // Regression: a top-level `providers:` key is rejected by the + // schema validator ("unknown field `providers`"). The wizard must + // write settings.providers..default_model. + let (app, cfg) = app_with_path(""); + config_cmd::persist_setting( + &app, + "settings.providers.deepseek.base_url", + "https://api.deepseek.com", + ) + .unwrap(); + config_cmd::persist_setting( + &app, + "settings.providers.deepseek.default_model", + "deepseek-reasoner", + ) + .unwrap(); + config_cmd::persist_setting(&app, "settings.default_provider", "deepseek").unwrap(); + + // Reloading the config must succeed (validation passes) and the + // merged registry must reflect the new default model. + let cli = Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]); + let app2 = App::from_cli(cli).expect("config must stay valid after setup writes"); + let def = crate::providers::get(&app2.config, "deepseek").unwrap(); + assert_eq!(def.default_model.as_deref(), Some("deepseek-reasoner")); + assert_eq!( + crate::providers::default_name(&app2.config).as_deref(), + Some("deepseek") + ); + } }