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:
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user