feat: catalogue dynamique modèles Codex/Claude avec compatibilité CLI locale

Ajout du catalogue enrichi pour les modèles Codex et Claude avec:
- Compatibilité estimée avec la version CLI locale détectée
- Source d'origine (catalogue/Provider) pour chaque entrée
- Support du catalogue Provider API externe
- Matrice de compatibilité embarquée dans l'application

Frontend:
- UI de configuration des modèles avec affichage des états de compatibilité
- Suggestions dynamiques avec badges de compatibilité
- Messages d'aide contextuels (compatible/unknown/likelyTooRecent)
- Alertes non-bloquantes pour les modèles trop récents
- Gestion des échecs de catalogue avec saisie manuelle conservée

Backend:
- Ports CliVersionReader, ProviderModelCatalogue, CompatibilityMatrixSource
- Implémentations: ProcessCliVersionReader, HttpProviderModelCatalogue, EmbeddedCompatibilityMatrix
- Enrichissement des DTOs avec compatibility, cli_version, warnings
- Tests unitaires complets pour le resolver de catalogue
This commit is contained in:
2026-07-26 16:09:10 +02:00
parent e7bf1d3666
commit ca70ec75f4
25 changed files with 1582 additions and 167 deletions

View File

@ -4,7 +4,7 @@ import { fireEvent, render, screen, waitFor, within } from "@testing-library/rea
import { DIProvider } from "@/app/di";
import { MockProfileGateway } from "@/adapters/mock";
import type { Gateways } from "@/ports";
import type { ProfileModelCatalogEntry } from "@/domain";
import type { ProfileModelCatalog } from "@/domain";
import { ProfilesSettings } from "./ProfilesSettings";
function renderSettings(profile: MockProfileGateway = new MockProfileGateway()) {
@ -94,14 +94,14 @@ describe("ProfilesSettings", () => {
it("keeps manual model entry available when the catalogue fails", async () => {
class CatalogueDownProfileGateway extends MockProfileGateway {
listCodexModels(): Promise<ProfileModelCatalogEntry[]> {
listCodexModels(): Promise<ProfileModelCatalog> {
return Promise.reject(new Error("catalogue down"));
}
}
renderSettings(new CatalogueDownProfileGateway());
await waitReady();
expect(await screen.findByText(/saisie manuelle active/)).toBeTruthy();
expect(await screen.findByText(/Catalogue provider indisponible/)).toBeTruthy();
await createProfile();
const model = within(screen.getAllByRole("listitem")[0]).getByLabelText(
@ -109,5 +109,54 @@ describe("ProfilesSettings", () => {
) as HTMLInputElement;
fireEvent.change(model, { target: { value: "future-codex-model" } });
expect(model.value).toBe("future-codex-model");
expect(screen.getAllByText(/Catalogue provider indisponible/).length).toBeGreaterThan(0);
});
it("shows compatibility states in suggestions and contextual help", async () => {
renderSettings();
await waitReady();
await createProfile();
const row = screen.getAllByRole("listitem")[0];
const model = within(row).getByLabelText(/modele du profil/) as HTMLInputElement;
fireEvent.focus(model);
expect(await within(row).findByText("Compatible")).toBeTruthy();
expect(
within(row).getByText(
/Compatible avec Codex CLI 0\.145\.0 d'après le catalogue local IdeA\./,
),
).toBeTruthy();
fireEvent.change(model, { target: { value: "" } });
expect(within(row).getAllByText("Inconnu").length).toBeGreaterThan(0);
expect(within(row).getByText("Probablement trop récent")).toBeTruthy();
fireEvent.change(model, { target: { value: "future-codex-model" } });
expect(within(row).getByText("Inconnu")).toBeTruthy();
expect(
within(row).getByText(
/Compatibilité non connue pour Codex CLI 0\.145\.0 ; la saisie reste autorisée\./,
),
).toBeTruthy();
});
it("saves likely-too-recent models and shows a non-blocking warning", async () => {
const { profile } = renderSettings();
await waitReady();
await createProfile();
const row = screen.getAllByRole("listitem")[0];
const model = within(row).getByLabelText(/modele du profil/) as HTMLInputElement;
fireEvent.change(model, { target: { value: "gpt-5-mini" } });
fireEvent.click(within(row).getByRole("button", { name: "Enregistrer" }));
await waitFor(async () => {
const saved = await profile.listProfiles();
expect(saved.some((p) => p.model === "gpt-5-mini")).toBe(true);
});
expect(
await screen.findByText(/Ce modèle semble plus récent que votre Codex CLI 0\.145\.0/),
).toBeTruthy();
});
});