feat: implémentation et QA ticket99 - agent model configuration v2

- backend A-C: VO Codex/Claude, renderers/projecteurs modèle, SecretRef/env, catalogues/use cases/Tauri commands
- frontend D: profils Codex/Claude provider->model->secret, validations, conservation SecretRef
- fix: app-tauri embedded_server isolant IDEA_WEB_ROOT

QA: backend 57/57 tests, frontend 74/74 tests OK
This commit is contained in:
2026-07-26 11:56:25 +02:00
parent 13fb538880
commit dae07d35bb
27 changed files with 2358 additions and 143 deletions

View File

@ -183,6 +183,131 @@ describe("FirstRunWizard (with MockProfileGateway)", () => {
});
});
describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)", () => {
it("saves a Codex provider profile and clears the literal key", async () => {
const { profile } = renderWizard();
await waitForLoaded();
fireEvent.click(screen.getByLabelText("use OpenAI Codex CLI"));
const row = within(
screen.getByLabelText("use OpenAI Codex CLI").closest("li")!,
);
const providerSelect = await row.findByLabelText("OpenAI Codex CLI provider");
const modelSelect = row.getByLabelText(
"OpenAI Codex CLI model",
) as HTMLSelectElement;
expect(modelSelect.disabled).toBe(true);
fireEvent.change(providerSelect, { target: { value: "openai" } });
expect(modelSelect.value).toBe("");
expect(modelSelect.disabled).toBe(false);
fireEvent.change(modelSelect, { target: { value: "gpt-5-codex" } });
fireEvent.change(row.getByLabelText("OpenAI Codex CLI api key"), {
target: { value: "sk-codex-secret" },
});
fireEvent.click(row.getByRole("button", { name: "Enregistrer Codex" }));
await waitFor(async () => {
const saved = await profile.listProfiles();
const codex = saved.find((p) => p.command === "codex");
expect(codex?.codexProvider).toEqual({
providerId: "openai",
model: "gpt-5-codex",
apiKeyRef: "mock-secret-mock-codex",
custom: undefined,
});
expect(JSON.stringify(codex)).not.toContain("sk-codex-secret");
});
expect((row.getByLabelText("OpenAI Codex CLI api key") as HTMLInputElement).value).toBe("");
});
it("saves a Claude provider profile without exposing the literal key", async () => {
const { profile } = renderWizard();
await waitForLoaded();
const claudeToggle = screen.getByLabelText(
"use Claude Code",
) as HTMLInputElement;
if (!claudeToggle.checked) fireEvent.click(claudeToggle);
const row = within(claudeToggle.closest("li")!);
fireEvent.change(await row.findByLabelText("Claude Code provider"), {
target: { value: "anthropic" },
});
fireEvent.change(row.getByLabelText("Claude Code model"), {
target: { value: "claude-sonnet-4-5" },
});
const apiKey = row.getByLabelText("Claude Code api key") as HTMLInputElement;
fireEvent.change(apiKey, { target: { value: "sk-claude-secret" } });
fireEvent.click(row.getByRole("button", { name: "Enregistrer Claude" }));
await waitFor(async () => {
const saved = await profile.listProfiles();
const claude = saved.find((p) => p.command === "claude");
expect(claude?.claudeProvider).toEqual({
providerId: "anthropic",
model: "claude-sonnet-4-5",
apiKeyRef: "mock-secret-mock-claude",
});
expect(JSON.stringify(claude)).not.toContain("sk-claude-secret");
});
expect(apiKey.value).toBe("");
});
it("keeps an existing Codex SecretRef when editing provider/model", async () => {
const profile = new MockProfileGateway();
await profile.configureProfiles([
{
id: "codex-existing",
name: "Codex configured",
command: "codex",
args: [],
contextInjection: { strategy: "conventionFile", target: "AGENTS.md" },
detect: "codex --version",
cwdTemplate: "{projectRoot}",
structuredAdapter: "codex",
codexProvider: {
providerId: "openai",
model: "gpt-5-mini",
apiKeyRef: "existing-secret-ref",
},
},
]);
const gateways = {
profile,
modelServer: new MockModelServerGateway(),
} as unknown as Gateways;
render(
<DIProvider gateways={gateways}>
<FirstRunWizard forceOpen />
</DIProvider>,
);
await waitForLoaded();
const row = within(
screen.getByLabelText("use Codex configured").closest("li")!,
);
expect((row.getByLabelText("Codex configured api key") as HTMLInputElement).value).toBe("");
fireEvent.change(row.getByLabelText("Codex configured model"), {
target: { value: "gpt-5" },
});
fireEvent.change(row.getByLabelText("Codex configured api key"), {
target: { value: "sk-new-secret" },
});
fireEvent.click(row.getByRole("button", { name: "Enregistrer Codex" }));
await waitFor(async () => {
const [saved] = await profile.listProfiles();
expect(saved.codexProvider?.model).toBe("gpt-5");
expect(saved.codexProvider?.apiKeyRef).toBe("existing-secret-ref");
expect(JSON.stringify(saved)).not.toContain("sk-new-secret");
});
});
});
describe("FirstRunWizard — OpenCode + llama.cpp local profile", () => {
const OPENCODE = "OpenCode + llama.cpp";