feat(backend,application,frontend,domain,web-server,app-tauri): bouton Redémarrer l'agent dans la barre supérieure des cellules — #317 (QA verte)

La cellule expose un bouton Redémarrer l'agent valable pour les trois
modes de présentation (headless, TUI native, CLI custom), sans jamais
prendre la présentation pour vérité runtime.

- Application: use case RestartCellAgentRuntime validant le binding
  exact cellule/runtime via les snapshots live (RestartRuntimeNotFound,
  ConflictAgentVisibleElsewhere, AgentNotBoundToCell) puis déléguant au
  scheduler self-restart existant (schedule_cell); CellAgentPresentation
  purement informationnelle.
- Backend/app-tauri/web-server: commande restart_cell_agent sur contrat
  DTO identique Tauri/HTTP, stop borné par timeout typé
  (RestartStopTimeout), relance préservant routage TUI natif, géométrie
  de cellule (node/rows/cols) et conversation; évenements domaine
  enrichis.
- Frontend: bouton dans la barre de cellule avec confirmation,
  overlay d'état Redémarrage…/échec (libellés par code d'erreur, Fermer/
  Réessayer), gardes d'identité de cellule et de restarts externes,
  gateway/mock/http adapters et ports alignés.
- Tests: dto_agents, workstate_actions, contrats web-server,
  LayoutGrid.cellControlsLayering, adapters agent/mock/http.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 15:00:43 +02:00
parent 46449bdf46
commit 7fb9d8bb86
26 changed files with 1404 additions and 58 deletions

View File

@ -194,6 +194,183 @@ function expectedOperation(action: CellAction, cellId: string) {
}
describe("LayoutGrid — ticket #48 cell control layering", () => {
it("shows restart only for an agent cell and disables its inputs while scheduled", async () => {
const setup = await makeSplitAgentGrid();
const restart = vi
.spyOn(setup.gateways.agent, "restartCellAgentRuntime")
.mockResolvedValue({
restartId: "restart-cell-1",
status: "scheduled",
agentId: setup.agentId,
nodeId: setup.bId,
kind: "pty",
presentation: "tui",
layoutId: null,
});
renderGrid(setup.gateways);
const button = await screen.findByRole("button", { name: "Redémarrer l’agent" });
expect(screen.getAllByRole("button", { name: "Redémarrer l’agent" })).toHaveLength(1);
fireEvent.click(button);
await waitFor(() =>
expect(restart).toHaveBeenCalledWith({
projectId: "p1",
nodeId: setup.bId,
agentId: setup.agentId,
presentation: "tui",
}),
);
expect(
(screen.getByRole("button", {
name: "Redémarrage de l’agent…",
}) as HTMLButtonElement).disabled,
).toBe(true);
expect(
(screen.getByLabelText(`agent selector ${setup.bId}`) as HTMLSelectElement)
.disabled,
).toBe(true);
expect(screen.getByText("Redémarrage de l’agent…")).toBeTruthy();
await flush();
act(() => {
setup.system.emit({
type: "agentRestartCompleted",
agentId: setup.agentId,
restartId: "restart-cell-1",
});
});
await waitFor(() =>
expect(
(screen.getByRole("button", {
name: "Redémarrer l’agent",
}) as HTMLButtonElement).disabled,
).toBe(false),
);
});
it("confirms a busy restart and forwards the custom CLI presentation", async () => {
const setup = await makeSplitAgentGrid();
window.localStorage.setItem(`idea.agent-cell-mode.p1.${setup.bId}`, "custom");
setup.workState._setProjectWorkState("p1", {
agents: [
{
agentId: setup.agentId,
name: "Worker",
profileId: "mock-claude",
busy: { state: "busy", sinceMs: 1 },
},
],
});
const restart = vi
.spyOn(setup.gateways.agent, "restartCellAgentRuntime")
.mockResolvedValue({
restartId: "restart-cell-2",
status: "scheduled",
agentId: setup.agentId,
nodeId: setup.bId,
kind: "structured",
presentation: "customCli",
layoutId: null,
});
renderGrid(setup.gateways);
fireEvent.click(await screen.findByRole("button", { name: "Redémarrer l’agent" }));
const dialog = await screen.findByRole("alertdialog", {
name: "Redémarrer l’agent ?",
});
expect(restart).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "Interrompre et redémarrer" }));
await waitFor(() =>
expect(restart).toHaveBeenCalledWith({
projectId: "p1",
nodeId: setup.bId,
agentId: setup.agentId,
presentation: "customCli",
}),
);
expect(dialog.isConnected).toBe(false);
window.localStorage.removeItem(`idea.agent-cell-mode.p1.${setup.bId}`);
});
it("distinguishes an asynchronous relaunch failure and offers retry/close", async () => {
const setup = await makeSplitAgentGrid();
vi.spyOn(setup.gateways.agent, "restartCellAgentRuntime").mockResolvedValue({
restartId: "restart-cell-3",
status: "scheduled",
agentId: setup.agentId,
nodeId: setup.bId,
kind: "pty",
presentation: "tui",
layoutId: null,
});
renderGrid(setup.gateways);
fireEvent.click(await screen.findByRole("button", { name: "Redémarrer l’agent" }));
await flush();
act(() => {
setup.system.emit({
type: "agentRestartFailed",
agentId: setup.agentId,
restartId: "restart-cell-3",
phase: "launching",
code: "LAUNCH_FAILED",
message: "provider unavailable",
});
});
expect(await screen.findByText("Impossible de relancer l’agent.")).toBeTruthy();
expect(screen.getByText("provider unavailable")).toBeTruthy();
expect(screen.getByRole("button", { name: "Réessayer" })).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Fermer" }));
expect(screen.queryByTestId("cell-agent-restart-state")).toBeNull();
});
it("distinguishes an asynchronous stop failure", async () => {
const setup = await makeSplitAgentGrid();
vi.spyOn(setup.gateways.agent, "restartCellAgentRuntime").mockResolvedValue({
restartId: "restart-cell-stop",
status: "scheduled",
agentId: setup.agentId,
nodeId: setup.bId,
kind: "pty",
presentation: "tui",
layoutId: null,
});
renderGrid(setup.gateways);
fireEvent.click(await screen.findByRole("button", { name: "Redémarrer l’agent" }));
await flush();
act(() => {
setup.system.emit({
type: "agentRestartFailed",
agentId: setup.agentId,
restartId: "restart-cell-stop",
phase: "stopping",
code: "STOP_TIMEOUT",
message: "stop timed out",
});
});
expect(await screen.findByText("Impossible d’arrêter l’agent.")).toBeTruthy();
});
it("surfaces a synchronous typed binding error", async () => {
const setup = await makeSplitAgentGrid();
vi.spyOn(setup.gateways.agent, "restartCellAgentRuntime").mockRejectedValue({
code: "AGENT_NOT_BOUND_TO_CELL",
message: "stale cell binding",
});
renderGrid(setup.gateways);
fireEvent.click(await screen.findByRole("button", { name: "Redémarrer l’agent" }));
expect(
await screen.findByText("L’agent n’est plus associé à cette cellule."),
).toBeTruthy();
expect(screen.getByText("stale cell binding")).toBeTruthy();
});
it("keeps custom CLI mode controls compact inside the cell toolbar (#151)", async () => {
const setup = await makeSplitAgentGrid();