feat(agent): menu de profils restreint Claude/Codex + retrait custom (D7) — §17

Dernier lot de §17 : seuls les profils pilotables en mode structuré sont
proposés à la sélection/création.

- domain : AgentProfile::is_selectable() = structured_adapter.is_some(),
  source unique de vérité du prédicat.
- infrastructure : AgentSessionFactory::supports délègue à is_selectable
  (supports et is_selectable ne peuvent plus diverger).
- application : selectable_reference_profiles() = reference_profiles()
  filtré ; ReferenceProfiles et FirstRunState exposent la liste filtrée
  (Claude/Codex). reference_profiles() brut reste à 4 (data intacte) ⇒
  un agent Gemini/Aider/custom legacy déjà configuré continue de tourner.
- frontend : bloc AddCustomProfile retiré du wizard first-run, action
  addCustom retirée du viewmodel ; wizard n'affiche que la liste filtrée.

Tests (QA) : is_selectable (table de vérité), cohérence stricte
is_selectable<->supports, liste exposée=2 / data brute=4, garde
anti-régression Vitest sur l'absence du bloc custom — validées par
mutation. cargo test --workspace : 820 passed. npx vitest run : 344 passed.

§17 COMPLET (D0->D7). Suivi restant : D6b (surfacer reply dans le writer
wire .response.json pour la délégation par protocole fichier).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:57:51 +02:00
parent dd1194abe8
commit 6de4e5a6e0
15 changed files with 324 additions and 245 deletions

View File

@ -1,13 +1,13 @@
/**
* L5 — the first-run wizard wired to the stateful {@link MockProfileGateway} via
* the real {@link DIProvider}. Covers: pre-filled editable rows, detection ✓/✗,
* adding a custom profile, and finishing (configure ⇒ first run closed ⇒ onDone).
* the real {@link DIProvider}. Covers: pre-filled editable rows (only the
* selectable Claude/Codex profiles, §17.3/D7), detection ✓/✗, the **absence** of
* any custom-profile block, and finishing (configure ⇒ first run closed ⇒ onDone).
*/
import { describe, it, expect, vi } from "vitest";
import {
render,
screen,
within,
waitFor,
fireEvent,
} from "@testing-library/react";
@ -38,13 +38,19 @@ async function waitForLoaded() {
}
describe("FirstRunWizard (with MockProfileGateway)", () => {
it("renders the four pre-filled, editable reference profiles", async () => {
it("renders only the selectable (Claude/Codex) pre-filled, editable profiles", async () => {
renderWizard();
await waitForLoaded();
for (const name of ["Claude Code", "OpenAI Codex CLI", "Gemini CLI", "Aider"]) {
// Only structured-drivable profiles are offered (§17.3/D7).
for (const name of ["Claude Code", "OpenAI Codex CLI"]) {
expect(screen.getByText(name)).toBeTruthy();
}
// Gemini/Aider are no longer proposed for selection.
expect(screen.queryByText("Gemini CLI")).toBeNull();
expect(screen.queryByText("Aider")).toBeNull();
expect(screen.queryByLabelText("Aider command")).toBeNull();
// Commands are editable inputs, pre-filled.
const claudeCmd = screen.getByLabelText(
"Claude Code command",
@ -55,9 +61,11 @@ describe("FirstRunWizard (with MockProfileGateway)", () => {
it("editing a command updates the input value", async () => {
renderWizard();
await waitForLoaded();
const cmd = screen.getByLabelText("Aider command") as HTMLInputElement;
fireEvent.change(cmd, { target: { value: "aider-2" } });
expect(cmd.value).toBe("aider-2");
const cmd = screen.getByLabelText(
"OpenAI Codex CLI command",
) as HTMLInputElement;
fireEvent.change(cmd, { target: { value: "codex-2" } });
expect(cmd.value).toBe("codex-2");
});
it("detection shows ✓ for claude and ✗ for the rest", async () => {
@ -71,49 +79,25 @@ describe("FirstRunWizard (with MockProfileGateway)", () => {
screen.getByLabelText("Claude Code availability").textContent,
).toMatch(/installed/);
});
expect(screen.getByLabelText("Aider availability").textContent).toMatch(
/not found/,
);
expect(
screen.getByLabelText("OpenAI Codex CLI availability").textContent,
).toMatch(/not found/);
});
it("adds a valid custom profile as a new row", async () => {
// §17.3/D7 — anti-regression guard. The custom-profile block was removed
// because we cannot drive an arbitrary command in structured mode. This test
// asserts the *absence* of that block: it must fail the instant any
// `AddCustomProfile` UI (its label/role/inputs) reappears.
it("offers no custom-profile block (removed in D7)", async () => {
renderWizard();
await waitForLoaded();
const form = screen.getByLabelText("add custom profile");
fireEvent.change(within(form).getByLabelText("custom name"), {
target: { value: "My AI" },
});
fireEvent.change(within(form).getByLabelText("custom command"), {
target: { value: "my-ai" },
});
fireEvent.click(
within(form).getByRole("button", { name: "Add custom profile" }),
);
expect(await screen.findByText("My AI")).toBeTruthy();
// The custom command input now exists as a row.
expect((screen.getByLabelText("My AI command") as HTMLInputElement).value).toBe(
"my-ai",
);
});
it("add button is disabled until the custom draft is valid", async () => {
renderWizard();
await waitForLoaded();
const form = screen.getByLabelText("add custom profile");
const addBtn = within(form).getByRole("button", {
name: "Add custom profile",
}) as HTMLButtonElement;
expect(addBtn.disabled).toBe(true);
fireEvent.change(within(form).getByLabelText("custom name"), {
target: { value: "X" },
});
fireEvent.change(within(form).getByLabelText("custom command"), {
target: { value: "x" },
});
expect(addBtn.disabled).toBe(false);
expect(screen.queryByLabelText("add custom profile")).toBeNull();
expect(
screen.queryByRole("button", { name: "Add custom profile" }),
).toBeNull();
expect(screen.queryByLabelText("custom name")).toBeNull();
expect(screen.queryByLabelText("custom command")).toBeNull();
});
it("auto-detects on open and pre-checks only installed CLIs", async () => {
@ -129,9 +113,6 @@ describe("FirstRunWizard (with MockProfileGateway)", () => {
expect(
(screen.getByLabelText("use OpenAI Codex CLI") as HTMLInputElement).checked,
).toBe(false);
expect(
(screen.getByLabelText("use Aider") as HTMLInputElement).checked,
).toBe(false);
// Availability was filled automatically, without clicking the button.
expect(
screen.getByLabelText("Claude Code availability").textContent,
@ -169,13 +150,13 @@ describe("FirstRunWizard (with MockProfileGateway)", () => {
).toBe(true),
);
// Keep Aider too (not installed, unchecked by default).
fireEvent.click(screen.getByLabelText("use Aider"));
// Keep Codex too (not installed, unchecked by default).
fireEvent.click(screen.getByLabelText("use OpenAI Codex CLI"));
fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
await waitFor(async () => {
const saved = await profile.listProfiles();
expect(saved.map((p) => p.command)).toEqual(["claude", "aider"]);
expect(saved.map((p) => p.command)).toEqual(["claude", "codex"]);
});
});