fix(application,domain,infrastructure,web-server,app-tauri,frontend): arrêt d'agent fiable avec cascade sur l'arbre de propriété et statut cohérent — #320 (QA verte)
Symptôme: stopper un agent (ex Main sur UnityPlugin) laissait des travaux poursuivre, la cascade n'atteignait pas les agents lancés et la pastille restait orange pendant que l'agent continuait à en appeler d'autres. - barrière d'admission par génération d'exécution (ExecutionControl): un stop ferme l'admission de tout le sous-arbre propriété/attente AVANT toute annulation asynchrone — plus aucune nouvelle action inter-agents après stop - arbre de propriété durable en-process (launched_by) + dépendances d'attente actives: la cascade couvre transitivement agents et travaux lancés - commandes d'un agent arrêté rejetées (AppError::AgentStopped); stop lui-même admis pour rester idempotent; réactivation sur relance humaine (génération +1) et enregistrement du propriétaire au spawn - admission exposée aux adaptateurs hors commandes cœur (tickets, templates, plugins) via ensure_agent_execution_active; MCP server/tools, web-server et commandes Tauri alignés - cohérence live-state: statut Idle avec intent « Arrêté » ou « Arrêt incomplet — Certains travaux sont encore actifs » selon l'issue de la cascade; workstate et surfaces UI (AgentsPanel, LayoutGrid, ProjectWorkStatePanel) reflètent l'état réel, plus de pastille orange fantôme - tests: orchestrator_service (cascade/générations/idempotence), workstate_actions, background_task_cancel, error_codes, agents, layout, workstate QA verte sur fix/320-agent-stop-cascade. Aucun push.
This commit is contained in:
@ -259,6 +259,60 @@ describe("LayoutGrid (with MockLayoutGateway)", () => {
|
||||
expect(screen.queryByText("Idle")).toBeNull();
|
||||
});
|
||||
|
||||
it("removes the orange busy veil when backend live-state reports stopping", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const workState = new MockWorkStateGateway();
|
||||
const created = await agent.createAgent("p1", {
|
||||
name: "StoppingAgent",
|
||||
profileId: "prof-1",
|
||||
});
|
||||
const tree = await layout.loadLayout("p1");
|
||||
const leafId = leaves(tree)[0].id;
|
||||
await layout.mutateLayout("p1", {
|
||||
type: "setCellAgent",
|
||||
target: leafId,
|
||||
agent: created.id,
|
||||
});
|
||||
await layout.mutateLayout("p1", {
|
||||
type: "setSession",
|
||||
target: leafId,
|
||||
session: "stopping-session",
|
||||
});
|
||||
workState._setProjectWorkState("p1", {
|
||||
agents: [
|
||||
{
|
||||
agentId: created.id,
|
||||
name: "StoppingAgent",
|
||||
profileId: "prof-1",
|
||||
runtimeState: "stopping",
|
||||
live: {
|
||||
nodeId: leafId,
|
||||
sessionId: "stopping-session",
|
||||
kind: "pty",
|
||||
runtimeState: "stopping",
|
||||
},
|
||||
busy: { state: "busy", ticket: "stale-ticket", sinceMs: 1 },
|
||||
tickets: [],
|
||||
},
|
||||
],
|
||||
conversations: [],
|
||||
});
|
||||
|
||||
render(
|
||||
<DIProvider gateways={{ layout, agent, workState } as unknown as Gateways}>
|
||||
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByTestId("agent-cell-runtime-loading")).toBeNull(),
|
||||
);
|
||||
expect(screen.queryByTestId("target-announcements-overlay")).toBeNull();
|
||||
expect(screen.queryByLabelText("StoppingAgent arrêté")).toBeNull();
|
||||
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("relaunches a stopped agent from scratch in the same cell", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
|
||||
@ -847,6 +847,9 @@ function LeafView({
|
||||
const agentWork = agentId
|
||||
? workState?.agents.find((row) => row.agentId === agentId)
|
||||
: undefined;
|
||||
const runtimeState = agentWork?.runtimeState;
|
||||
const stoppingOrStopped =
|
||||
runtimeState === "stopping" || runtimeState === "stopped";
|
||||
const activeTicket = agentWork?.tickets.find((ticket) => ticket.status === "inProgress");
|
||||
const queuedTickets = agentWork?.tickets.filter((ticket) => ticket.status === "queued") ?? [];
|
||||
const inboxDepth = Math.max(
|
||||
@ -858,9 +861,12 @@ function LeafView({
|
||||
agentWork?.backgroundTasks?.filter((task) =>
|
||||
task.status === "completed" || task.status === "failed" || task.status === "cancelled",
|
||||
) ?? [];
|
||||
const busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
|
||||
const busyByWorkState =
|
||||
!stoppingOrStopped &&
|
||||
(agentWork?.busy.state === "busy" || Boolean(activeTicket));
|
||||
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
|
||||
const busyActive = announcementBusyActive || busyByWorkState;
|
||||
const busyActive =
|
||||
!stoppingOrStopped && (announcementBusyActive || busyByWorkState);
|
||||
const [cellRestart, setCellRestart] = useState<CellRestartState | null>(null);
|
||||
const cellRestartRef = useRef<CellRestartState | null>(null);
|
||||
const cellRestartRequestRef = useRef(0);
|
||||
@ -938,7 +944,6 @@ function LeafView({
|
||||
const [restartError, setRestartError] = useState(false);
|
||||
const [freshRestartedSession, setFreshRestartedSession] =
|
||||
useState<FreshRestartedSession | null>(null);
|
||||
const runtimeState = agentWork?.runtimeState;
|
||||
const livePinnedAgent = agentId
|
||||
? liveAgents.find((la) => la.agentId === agentId)
|
||||
: undefined;
|
||||
@ -1762,7 +1767,7 @@ function LeafView({
|
||||
an availability veil showing its live réflexions. Native TUI cells
|
||||
need the full-cell guard; custom CLI cells keep the canonical busy
|
||||
badge and composer lock only, matching their normal chat turn flow. */}
|
||||
{!cellRestart && !otherRestartActive && !modelServerOverlay && cellMode !== "custom" && agentId != null && (
|
||||
{!cellRestart && !otherRestartActive && !modelServerOverlay && !stoppingOrStopped && cellMode !== "custom" && agentId != null && (
|
||||
<TargetAnnouncementsOverlay
|
||||
projectId={projectId}
|
||||
agentId={agentId}
|
||||
|
||||
Reference in New Issue
Block a user