feat(tickets): création d'un assistant IA de ticket (backend + frontend)

Ajoute le chat assistant IA attaché à un ticket (#8).

Backend Rust :
- use cases OpenTicketAssistant/CloseTicketAssistant + tests
- politique d'outils par agent (domain/agent_tool_policy) et policy MCP
- store de contexte assistant + gabarit default_ticket_assistant.md + tests
- events TicketAssistantOpened/Closed
- commandes Tauri open_ticket_chat/close_ticket_chat et câblage state/lib/events

Frontend :
- gateway (ports, adapters ticket + mock, domain)
- hook useTicketAssistant + composant TicketAssistantPanel
- intégration dans TicketDetail

Tests verts : vitest tickets.test.tsx (23), cargo test application::ticket_assistant (1),
infrastructure::assistant_context_store (2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 06:06:24 +02:00
parent f1803768fd
commit 1fdf62c089
29 changed files with 2173 additions and 49 deletions

View File

@ -10,6 +10,7 @@ import {
import { DIProvider } from "@/app/di";
import {
MockAgentGateway,
MockProfileGateway,
MockSystemGateway,
MockTicketGateway,
} from "@/adapters/mock";
@ -43,6 +44,18 @@ function renderView(
);
}
async function seedAssistantProfile(profile: MockProfileGateway) {
return profile.saveProfile({
id: "qa-assistant",
name: "QA Assistant",
command: "codex",
args: [],
contextInjection: { strategy: "conventionFile", target: "AGENTS.md" },
detect: null,
cwdTemplate: "{projectRoot}",
});
}
describe("MockTicketGateway", () => {
let system: MockSystemGateway;
let ticket: MockTicketGateway;
@ -427,6 +440,66 @@ describe("TicketsView", () => {
),
);
});
it("opens, streams, and closes the ticket AI assistant chat (#8)", async () => {
const t = await ticket.create(PROJECT_ID, { title: "Assistant target" });
const profile = new MockProfileGateway();
await seedAssistantProfile(profile);
const gateways = { ticket, system, agent, profile } as unknown as Gateways;
render(
<DIProvider gateways={gateways}>
<TicketDetail
projectId={PROJECT_ID}
ticketRef={t.ref}
onClose={() => {}}
onOpenRef={() => {}}
/>
</DIProvider>,
);
const detail = await screen.findByRole("dialog", { name: `ticket ${t.ref}` });
expect(within(detail).getByText("Assistant IA")).toBeTruthy();
const profileSelect = await within(detail).findByLabelText(
"profil de l'assistant",
);
expect(within(detail).getByText("Ouvrir la conversation")).toBeTruthy();
fireEvent.change(profileSelect, { target: { value: "qa-assistant" } });
fireEvent.click(within(detail).getByText("Ouvrir la conversation"));
expect(
await within(detail).findByLabelText("message à l'assistant"),
).toBeTruthy();
fireEvent.change(within(detail).getByLabelText("message à l'assistant"), {
target: { value: "Crée un plan de correction" },
});
fireEvent.click(within(detail).getByText("Envoyer"));
expect(await within(detail).findByText("Crée un plan de correction")).toBeTruthy();
expect(
await within(detail).findByText(
"Assistant: reçu « Crée un plan de correction ».",
),
).toBeTruthy();
fireEvent.click(within(detail).getByText("Fermer la conversation"));
await waitFor(() =>
expect(
within(detail).queryByLabelText("message à l'assistant"),
).toBeNull(),
);
expect(within(detail).getByText("Ouvrir la conversation")).toBeTruthy();
expect(
within(detail).queryByText("Crée un plan de correction"),
).toBeNull();
expect(
within(detail).queryByText("Assistant: reçu « Crée un plan de correction »."),
).toBeNull();
});
});
describe("SprintManager (#11)", () => {