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>
109 lines
3.6 KiB
TypeScript
109 lines
3.6 KiB
TypeScript
/**
|
|
* 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" },
|
|
]);
|
|
});
|
|
});
|
|
});
|