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:
@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user