feat(chat): livre la CLI custom de chat agent (#147) et corrige Cancel
Implémente la vue chat structurée par cellule agent (toggle TUI/CLI custom, préférence persistée `preferred_view`, reattach live, composer + pièces jointes) avec le socle backend AgentSession/ChatBridge (UserPrompt, cancel_current_turn, routage interrupt_agent, commande cancel_agent_chat). Corrige le bug bloquant relevé par QA : le bouton Cancel de CustomAgentChatView interrompait tout le tour via closeAgentChat au lieu de n'annuler que le tour courant via cancelAgentChat, ce qui tuait la session contrairement au contrat produit validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
97
frontend/src/features/agents/CustomAgentChatView.test.tsx
Normal file
97
frontend/src/features/agents/CustomAgentChatView.test.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { DIProvider } from "@/app/di";
|
||||
import type { AgentProfile } from "@/domain";
|
||||
import type { Gateways } from "@/ports";
|
||||
import { CustomAgentChatView } from "./CustomAgentChatView";
|
||||
|
||||
const profile: AgentProfile = {
|
||||
id: "structured",
|
||||
name: "Structured Codex",
|
||||
command: "codex",
|
||||
args: [],
|
||||
contextInjection: { strategy: "conventionFile", target: "AGENTS.md" },
|
||||
detect: null,
|
||||
cwdTemplate: "{projectRoot}",
|
||||
structuredAdapter: "codex",
|
||||
};
|
||||
|
||||
describe("CustomAgentChatView", () => {
|
||||
it("cancels only the current turn and keeps the structured session alive", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback: [],
|
||||
})),
|
||||
sendAgentChat: vi.fn(() => new Promise<void>(() => {})),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
const onSessionId = vi.fn();
|
||||
|
||||
render(
|
||||
<DIProvider
|
||||
gateways={{
|
||||
agent,
|
||||
system: { pickFile: vi.fn(async () => null) },
|
||||
} as unknown as Gateways}
|
||||
>
|
||||
<CustomAgentChatView
|
||||
projectId="project-1"
|
||||
agentId="agent-1"
|
||||
agentName="Worker"
|
||||
profile={profile}
|
||||
cwd="/repo"
|
||||
nodeId="node-1"
|
||||
sessionId="chat-session-1"
|
||||
conversationId="conversation-1"
|
||||
onSessionId={onSessionId}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
|
||||
"chat-session-1",
|
||||
expect.any(Function),
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
target: { value: "first turn" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(agent.sendAgentChat).toHaveBeenCalledWith(
|
||||
"chat-session-1",
|
||||
"first turn",
|
||||
expect.any(Function),
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Cancel" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(agent.cancelAgentChat).toHaveBeenCalledWith("chat-session-1"),
|
||||
);
|
||||
expect(agent.closeAgentChat).not.toHaveBeenCalled();
|
||||
expect(onSessionId).not.toHaveBeenCalledWith(null);
|
||||
expect(screen.getByText("Tour interrompu.")).toBeTruthy();
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
|
||||
target: { value: "second turn" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(2));
|
||||
expect(agent.sendAgentChat).toHaveBeenLastCalledWith(
|
||||
"chat-session-1",
|
||||
"second turn",
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user