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:
@ -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)", () => {
|
||||
|
||||
Reference in New Issue
Block a user