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:
113
frontend/src/features/first-run/ProfilesSettings.test.tsx
Normal file
113
frontend/src/features/first-run/ProfilesSettings.test.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { MockProfileGateway } from "@/adapters/mock";
|
||||
import type { Gateways } from "@/ports";
|
||||
import type { ProfileModelCatalogEntry } from "@/domain";
|
||||
import { ProfilesSettings } from "./ProfilesSettings";
|
||||
|
||||
function renderSettings(profile: MockProfileGateway = new MockProfileGateway()) {
|
||||
return {
|
||||
profile,
|
||||
...render(
|
||||
<DIProvider gateways={{ profile } as unknown as Gateways}>
|
||||
<ProfilesSettings />
|
||||
</DIProvider>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitReady() {
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
(screen.getByRole("button", { name: "Creer un profil" }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(false),
|
||||
);
|
||||
}
|
||||
|
||||
async function createProfile() {
|
||||
const before = screen.queryAllByRole("listitem").length;
|
||||
fireEvent.click(screen.getByRole("button", { name: "Creer un profil" }));
|
||||
await waitFor(() => expect(screen.getAllByRole("listitem")).toHaveLength(before + 1));
|
||||
}
|
||||
|
||||
describe("ProfilesSettings", () => {
|
||||
it("creates multiple named Codex and Claude profiles with different models", async () => {
|
||||
const { profile } = renderSettings();
|
||||
await waitReady();
|
||||
|
||||
await createProfile();
|
||||
await createProfile();
|
||||
let rows = screen.getAllByRole("listitem");
|
||||
fireEvent.change(within(rows[1]).getByLabelText(/nom du profil/), {
|
||||
target: { value: "Codex mini" },
|
||||
});
|
||||
fireEvent.change(within(rows[1]).getByLabelText(/modele du profil/), {
|
||||
target: { value: "gpt-5-mini" },
|
||||
});
|
||||
fireEvent.click(within(rows[1]).getByRole("button", { name: "Enregistrer" }));
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Claude" }));
|
||||
await waitReady();
|
||||
await createProfile();
|
||||
await createProfile();
|
||||
rows = screen.getAllByRole("listitem");
|
||||
fireEvent.change(within(rows[1]).getByLabelText(/nom du profil/), {
|
||||
target: { value: "Claude Opus" },
|
||||
});
|
||||
fireEvent.change(within(rows[1]).getByLabelText(/modele du profil/), {
|
||||
target: { value: "claude-opus-4-8" },
|
||||
});
|
||||
fireEvent.click(within(rows[1]).getByRole("button", { name: "Enregistrer" }));
|
||||
|
||||
await waitFor(async () => {
|
||||
const saved = await profile.listProfiles();
|
||||
expect(saved.filter((p) => p.structuredAdapter === "codex")).toHaveLength(2);
|
||||
expect(saved.filter((p) => p.structuredAdapter === "claude")).toHaveLength(2);
|
||||
expect(saved.map((p) => p.model)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"gpt-5-codex",
|
||||
"gpt-5-mini",
|
||||
"claude-sonnet-5",
|
||||
"claude-opus-4-8",
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("duplicates from an existing profile with '<name> copy' and preserves the model", async () => {
|
||||
const { profile } = renderSettings();
|
||||
await waitReady();
|
||||
await createProfile();
|
||||
|
||||
const row = screen.getAllByRole("listitem")[0];
|
||||
fireEvent.click(within(row).getByRole("button", { name: "Dupliquer" }));
|
||||
|
||||
await waitFor(async () => {
|
||||
const saved = await profile.listProfiles();
|
||||
expect(saved.some((p) => p.name === "OpenAI Codex CLI copy copy")).toBe(true);
|
||||
expect(saved.filter((p) => p.model === "gpt-5-codex")).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps manual model entry available when the catalogue fails", async () => {
|
||||
class CatalogueDownProfileGateway extends MockProfileGateway {
|
||||
listCodexModels(): Promise<ProfileModelCatalogEntry[]> {
|
||||
return Promise.reject(new Error("catalogue down"));
|
||||
}
|
||||
}
|
||||
|
||||
renderSettings(new CatalogueDownProfileGateway());
|
||||
await waitReady();
|
||||
expect(await screen.findByText(/saisie manuelle active/)).toBeTruthy();
|
||||
|
||||
await createProfile();
|
||||
const model = within(screen.getAllByRole("listitem")[0]).getByLabelText(
|
||||
/modele du profil/,
|
||||
) as HTMLInputElement;
|
||||
fireEvent.change(model, { target: { value: "future-codex-model" } });
|
||||
expect(model.value).toBe("future-codex-model");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user