feat: finalise multi-profil Codex/Claude avec catalogue de modèles
- Backend : clone_profile_from_seed généralisé (non OpenCode) - Backend : catalogue static Claude/Codex (3 modèles chacun, 1 recommandé) - Backend : commandes Tauri list_claude_models/list_codex_models - Frontend : ProfilesSettings refonte en onglets Codex/Claude + create/duplicate/edit/delete - Frontend : ModelSelect searchable partagé + fallback saisie manuelle - Frontend : assignation agent nom · modèle - Tests QA : 4 profils modèles distincts (2 Claude, 2 Codex) assignés à agents
This commit is contained in:
@ -24,9 +24,10 @@ use domain::profile::{
|
||||
use domain::project::ProjectPath;
|
||||
|
||||
use application::{
|
||||
reference_profile_id, reference_profiles, CloneOpenCodeProfileFromSeed,
|
||||
CloneOpenCodeProfileFromSeedInput, ConfigureProfiles, ConfigureProfilesInput, DeleteProfile,
|
||||
DeleteProfileInput, DetectProfiles, DetectProfilesInput, FirstRunState, ListProfiles,
|
||||
reference_profile_id, reference_profiles, AppError, CloneOpenCodeProfileFromSeed,
|
||||
CloneOpenCodeProfileFromSeedInput, CloneProfileFromSeed, CloneProfileFromSeedInput,
|
||||
ConfigureProfiles, ConfigureProfilesInput, DeleteProfile, DeleteProfileInput, DetectProfiles,
|
||||
DetectProfilesInput, FirstRunState, ListClaudeModels, ListCodexModels, ListProfiles,
|
||||
ReferenceProfiles, SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput, SaveProfile,
|
||||
SaveProfileInput, CODEX_SUBMIT_DELAY_MS,
|
||||
};
|
||||
@ -869,6 +870,114 @@ async fn clone_opencode_profile_falls_back_to_catalogue_when_persisted_seed_is_n
|
||||
assert_eq!(out.profile.name, "OpenCode + llama.cpp copy");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clone_profile_from_seed_creates_codex_profile_with_fresh_id_and_model_override() {
|
||||
let store = FakeProfileStore::default();
|
||||
let clone = CloneProfileFromSeed::new(
|
||||
Arc::new(store.clone()),
|
||||
Arc::new(SeqIds::new(vec![uuid::Uuid::from_u128(3801)])),
|
||||
);
|
||||
|
||||
let out = clone
|
||||
.execute(CloneProfileFromSeedInput {
|
||||
seed_profile_id: reference_profile_id("codex"),
|
||||
name: Some("Codex GPT-5".to_owned()),
|
||||
model: Some("gpt-5-codex".to_owned()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
out.profile.id,
|
||||
ProfileId::from_uuid(uuid::Uuid::from_u128(3801))
|
||||
);
|
||||
assert_eq!(out.profile.name, "Codex GPT-5");
|
||||
assert_eq!(out.profile.model.as_deref(), Some("gpt-5-codex"));
|
||||
assert_eq!(
|
||||
out.profile.structured_adapter,
|
||||
Some(StructuredAdapter::Codex)
|
||||
);
|
||||
assert_eq!(store.0.lock().unwrap().profiles, vec![out.profile]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clone_profile_from_seed_prefers_persisted_seed_and_preserves_model_by_default() {
|
||||
let store = FakeProfileStore::default();
|
||||
let persisted = reference_profiles()
|
||||
.into_iter()
|
||||
.find(|profile| profile.id == reference_profile_id("claude"))
|
||||
.expect("seed exists")
|
||||
.with_model("claude-opus-4-8");
|
||||
SaveProfile::new(Arc::new(store.clone()))
|
||||
.execute(SaveProfileInput {
|
||||
profile: persisted.clone(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let clone = CloneProfileFromSeed::new(
|
||||
Arc::new(store.clone()),
|
||||
Arc::new(SeqIds::new(vec![uuid::Uuid::from_u128(3802)])),
|
||||
);
|
||||
let out = clone
|
||||
.execute(CloneProfileFromSeedInput {
|
||||
seed_profile_id: persisted.id,
|
||||
name: None,
|
||||
model: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(out.profile.name, "Claude Code copy");
|
||||
assert_eq!(out.profile.model.as_deref(), Some("claude-opus-4-8"));
|
||||
assert_eq!(
|
||||
out.profile.structured_adapter,
|
||||
Some(StructuredAdapter::Claude)
|
||||
);
|
||||
assert_ne!(out.profile.id, persisted.id);
|
||||
assert_eq!(store.0.lock().unwrap().profiles.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clone_profile_from_seed_rejects_blank_model_override() {
|
||||
let store = FakeProfileStore::default();
|
||||
let clone = CloneProfileFromSeed::new(
|
||||
Arc::new(store),
|
||||
Arc::new(SeqIds::new(vec![uuid::Uuid::from_u128(3803)])),
|
||||
);
|
||||
|
||||
let err = clone
|
||||
.execute(CloneProfileFromSeedInput {
|
||||
seed_profile_id: reference_profile_id("claude"),
|
||||
name: Some("Claude blank".to_owned()),
|
||||
model: Some(" ".to_owned()),
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, AppError::Invalid(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clone_profile_from_seed_rejects_blank_name_override() {
|
||||
let store = FakeProfileStore::default();
|
||||
let clone = CloneProfileFromSeed::new(
|
||||
Arc::new(store),
|
||||
Arc::new(SeqIds::new(vec![uuid::Uuid::from_u128(3804)])),
|
||||
);
|
||||
|
||||
let err = clone
|
||||
.execute(CloneProfileFromSeedInput {
|
||||
seed_profile_id: reference_profile_id("codex"),
|
||||
name: Some(" ".to_owned()),
|
||||
model: Some("gpt-5-codex".to_owned()),
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, AppError::Invalid(_)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ReferenceProfiles / catalogue
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -1049,3 +1158,22 @@ fn catalogue_gemini_and_aider_stay_pty_without_adapter() {
|
||||
assert_eq!(by_command["gemini"].structured_adapter, None);
|
||||
assert_eq!(by_command["aider"].structured_adapter, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_and_codex_model_catalogues_are_static_and_searchable() {
|
||||
let claude = ListClaudeModels::new().execute().models;
|
||||
let codex = ListCodexModels::new().execute().models;
|
||||
|
||||
assert!(claude
|
||||
.iter()
|
||||
.any(|model| model.model_id == "claude-sonnet-5" && model.recommended));
|
||||
assert!(codex
|
||||
.iter()
|
||||
.any(|model| model.model_id == "gpt-5-codex" && model.recommended));
|
||||
assert!(claude
|
||||
.iter()
|
||||
.all(|model| model.adapter == StructuredAdapter::Claude && !model.display_name.is_empty()));
|
||||
assert!(codex
|
||||
.iter()
|
||||
.all(|model| model.adapter == StructuredAdapter::Codex && !model.display_name.is_empty()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user