feat: finalise ticket99 - implémentation agent model configuration v2

- agent/lifecycle.rs: lifecycle management per profile
- agent/provider_catalogue.rs: provider registration with model support
- agent/usecases.rs: usecases for profile-based agent invocation
- agent/mod.rs: expose agent capabilities via AgentManager
- backend/dto.rs: AgentModelConfig, AgentProviderConfig DTOs
- domain/profile.rs: extend Profile avec agent capabilities
- domain/permission.rs: permission checks pour agent access
- infrastructure/assistant/mod.rs: agent integration
- infrastructure/permission/{claude,codex}.rs: permission handlers
- web-server/lib.rs: agent endpoints
- commands.rs: agent commands
- frontend/adapters/{http,profile,mock,domain}.ts: adapters
- frontend/first-run/FirstRunWizard.{test.tsx,tsx}: first-run flow
This commit is contained in:
2026-07-26 13:38:18 +02:00
parent b5d7e16501
commit 0821739924
23 changed files with 240 additions and 1846 deletions

View File

@ -183,47 +183,41 @@ describe("FirstRunWizard (with MockProfileGateway)", () => {
});
});
describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)", () => {
it("saves a Codex provider profile and clears the literal key", async () => {
describe("FirstRunWizard — Codex/Claude model configuration (ticket #99)", () => {
it("shows only a free-form model field for Codex and persists no provider/API key", async () => {
const { profile } = renderWizard();
await waitForLoaded();
await waitFor(() =>
expect(
(screen.getByLabelText("use Claude Code") as HTMLInputElement).checked,
).toBe(true),
);
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);
expect(row.queryByLabelText("OpenAI Codex CLI provider")).toBeNull();
expect(row.queryByLabelText("OpenAI Codex CLI provider search")).toBeNull();
expect(row.queryByLabelText("OpenAI Codex CLI api key")).toBeNull();
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.change(row.getByLabelText("OpenAI Codex CLI model"), {
target: { value: "gpt-5-codex" },
});
fireEvent.click(row.getByRole("button", { name: "Enregistrer Codex" }));
fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
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(codex?.model).toBe("gpt-5-codex");
expect(JSON.stringify(codex)).not.toContain("provider");
expect(JSON.stringify(codex)).not.toContain("apiKey");
});
expect((row.getByLabelText("OpenAI Codex CLI api key") as HTMLInputElement).value).toBe("");
});
it("saves a Claude provider profile without exposing the literal key", async () => {
it("shows only a free-form model field for Claude and persists no provider/API key", async () => {
const { profile } = renderWizard();
await waitForLoaded();
@ -233,31 +227,26 @@ describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)",
if (!claudeToggle.checked) fireEvent.click(claudeToggle);
const row = within(claudeToggle.closest("li")!);
fireEvent.change(await row.findByLabelText("Claude Code provider"), {
target: { value: "anthropic" },
});
expect(row.queryByLabelText("Claude Code provider")).toBeNull();
expect(row.queryByLabelText("Claude Code provider search")).toBeNull();
expect(row.queryByLabelText("Claude Code api key")).toBeNull();
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" }));
fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
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(claude?.model).toBe("claude-sonnet-4-5");
expect(JSON.stringify(claude)).not.toContain("provider");
expect(JSON.stringify(claude)).not.toContain("apiKey");
});
expect(apiKey.value).toBe("");
});
it("keeps an existing Codex SecretRef when editing provider/model", async () => {
it("edits an existing Codex model without rendering provider/API key fields", async () => {
const profile = new MockProfileGateway();
await profile.configureProfiles([
{
@ -269,11 +258,7 @@ describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)",
detect: "codex --version",
cwdTemplate: "{projectRoot}",
structuredAdapter: "codex",
codexProvider: {
providerId: "openai",
model: "gpt-5-mini",
apiKeyRef: "existing-secret-ref",
},
model: "gpt-5-mini",
},
]);
const gateways = {
@ -290,20 +275,22 @@ describe("FirstRunWizard — Codex/Claude provider configuration (ticket #99)",
const row = within(
screen.getByLabelText("use Codex configured").closest("li")!,
);
expect((row.getByLabelText("Codex configured api key") as HTMLInputElement).value).toBe("");
expect(row.queryByLabelText("Codex configured provider")).toBeNull();
expect(row.queryByLabelText("Codex configured api key")).toBeNull();
expect(
(row.getByLabelText("Codex configured model") as HTMLInputElement).value,
).toBe("gpt-5-mini");
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" }));
fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
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");
expect(saved.model).toBe("gpt-5");
expect(JSON.stringify(saved)).not.toContain("provider");
expect(JSON.stringify(saved)).not.toContain("apiKey");
});
});
});