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