feat(agent): conversation par paire + entrée médiée + pivot terminal/MCP
Coeur inter-agents consolidé et surface front réalignée sur la décision "terminal natif PTY, pas d'UI chat" (Option 1). Domaine - nouveaux modules conversation, mailbox, input, fileguard (ports + types) - orchestrator/profile/events étendus (conversation par paire, FIFO) Application / Infrastructure - orchestrator/service + context_guard : sérialisation FIFO par agent, garde RW mémoire/contexte, dispatch ask/reply - adapters in-memory conversation / mailbox / input / fileguard - registry session + lifecycle agent durcis (1 agent = 1 session vivante) - outils MCP idea_* alignés sur le nouveau dispatch Frontend - MediatedInput + useAgentBusy : entrée utilisateur médiée par IdeA, terminal = vue sortie inchangée - suppression de la vue chat structurée (AgentChatView) — abandonnée - adapter input + ports mis à jour Divers - .ideai/ : mémoire projet + briefs de cadrage versionnés ; requests/ runtime ignoré ; agents projet réels (DevBackend/DevFrontend/QA) Tests : Rust (domain/application/infrastructure/app-tauri) + front (346) verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
108
frontend/src/features/terminals/MediatedInput.test.tsx
Normal file
108
frontend/src/features/terminals/MediatedInput.test.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* F1 — {@link MediatedInput} wired to {@link MockInputGateway} through the real
|
||||
* {@link DIProvider}. Asserts the mediated-input contract (cadrage §4.2/§6):
|
||||
*
|
||||
* - "Envoyer" routes to `InputGateway.submit` with the right args;
|
||||
* - "Interrompre" routes to `InputGateway.interrupt`;
|
||||
* - while busy, "Envoyer" is dimmed/aria-disabled but the enqueue STILL goes
|
||||
* through (forward/fallback — never block the user), and "Interrompre" stays
|
||||
* active.
|
||||
*
|
||||
* Fully offline: gateways are mocks, no backend.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
|
||||
import type { Gateways } from "@/ports";
|
||||
import { MockInputGateway, MockSystemGateway } from "@/adapters/mock";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { MediatedInput } from "./MediatedInput";
|
||||
|
||||
function renderInput(
|
||||
input: MockInputGateway,
|
||||
props: Partial<React.ComponentProps<typeof MediatedInput>> = {},
|
||||
) {
|
||||
const gateways = {
|
||||
input,
|
||||
system: new MockSystemGateway(),
|
||||
} as unknown as Gateways;
|
||||
return render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<MediatedInput projectId="p1" agentId="a1" {...props} />
|
||||
</DIProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("MediatedInput (with MockInputGateway)", () => {
|
||||
it("mounts and renders the input strip", () => {
|
||||
renderInput(new MockInputGateway());
|
||||
expect(screen.getByTestId("mediated-input")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Envoyer routes to gateway.submit with project/agent/text", async () => {
|
||||
const input = new MockInputGateway();
|
||||
renderInput(input);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Message à l'agent"), {
|
||||
target: { value: "hello agent" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input.submits).toEqual([
|
||||
{ projectId: "p1", agentId: "a1", text: "hello agent" },
|
||||
]);
|
||||
});
|
||||
// No interrupt was triggered.
|
||||
expect(input.interrupts).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not submit blank/whitespace-only text", async () => {
|
||||
const input = new MockInputGateway();
|
||||
renderInput(input);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Message à l'agent"), {
|
||||
target: { value: " " },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||
|
||||
expect(input.submits).toEqual([]);
|
||||
});
|
||||
|
||||
it("Interrompre routes to gateway.interrupt (not an enqueue)", async () => {
|
||||
const input = new MockInputGateway();
|
||||
renderInput(input);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Interrompre" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input.interrupts).toEqual([{ projectId: "p1", agentId: "a1" }]);
|
||||
});
|
||||
expect(input.submits).toEqual([]);
|
||||
});
|
||||
|
||||
it("while busy: Envoyer is aria-disabled but the enqueue still goes through, Interrompre stays active", async () => {
|
||||
const input = new MockInputGateway();
|
||||
renderInput(input, { busy: true });
|
||||
|
||||
const send = screen.getByRole("button", { name: "Envoyer" });
|
||||
const interrupt = screen.getByRole("button", { name: "Interrompre" });
|
||||
|
||||
// Visually disabled (forward/fallback: dimmed, not hard-disabled).
|
||||
expect(send.getAttribute("aria-disabled")).toBe("true");
|
||||
// Interrompre is never disabled.
|
||||
expect(interrupt.hasAttribute("disabled")).toBe(false);
|
||||
|
||||
// The enqueue path is NOT blocked while busy.
|
||||
fireEvent.change(screen.getByLabelText("Message à l'agent"), {
|
||||
target: { value: "queued while busy" },
|
||||
});
|
||||
fireEvent.click(send);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input.submits).toEqual([
|
||||
{ projectId: "p1", agentId: "a1", text: "queued while busy" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user