fix(runtime): stop/restart d'agent visible robuste — kill réel, état stopped, relance froide (#196, QA verte)

Solidifie le cycle stop/restart d'un agent visible en cellule:

- terminal/registry: kill effectif de la TUI native et de la CLI custom, arrêt idempotent du live runtime (PTY + structuré), distinction nette « agent stoppé » vs « pas de session ».

- workstate + lifecycle: projection d'un état runtime explicite « stopped » (AgentRuntimeState: running/stopping/stopped/failedStart), réconcilié avec le live state; traitement des courses « agent not found ».

- relance froide via freshStart: repart de zéro sans réattacher une session morte et recharge la configuration courante de l'agent (y compris changement de modèle).

- contrat projeté côté adapters frontend (ports/domain/normalization/mock) pour la consommation UI future (#197/#198), sans surface UI ici.

Tests QA verts: workstate_actions, orchestrator_service stop_agent, agent_lifecycle (reopen/fresh_start), workstate stopped runtime state, vitest adapter stop_live_agent.
This commit is contained in:
2026-08-12 14:56:02 +02:00
parent 4e93fa2f20
commit bad7b4c2f3
23 changed files with 440 additions and 46 deletions

View File

@ -11,8 +11,8 @@ use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use application::{
AttachLiveAgent, AttachLiveAgentInput, CloseTerminal, LiveSessionKind, LiveSessions,
StopAgentExecutionCascade, StopLiveAgent, StopLiveAgentInput, StructuredSessions,
AttachLiveAgent, AttachLiveAgentInput, CloseTerminal, LiveRuntimeState, LiveSessionKind,
LiveSessions, StopAgentExecutionCascade, StopLiveAgent, StopLiveAgentInput, StructuredSessions,
TerminalSessions,
};
use domain::ports::{
@ -329,7 +329,9 @@ async fn stop_pty_kills_and_removes_session() {
assert_eq!(out.agent_id, a);
assert_eq!(out.session_id, sid(1));
assert_eq!(out.node_id, nid(100));
assert_eq!(out.kind, LiveSessionKind::Pty);
assert_eq!(out.runtime_state, LiveRuntimeState::Stopped);
// Delegated to the close primitive: process killed and registry emptied.
assert_eq!(f.pty_port.kills(), vec![sid(1)]);
assert!(f.pty.is_empty(), "live session removed from the registry");
@ -353,6 +355,7 @@ async fn stop_structured_shuts_down_and_removes_session() {
assert_eq!(out.session_id, sid(2));
assert_eq!(out.kind, LiveSessionKind::Structured);
assert_eq!(out.runtime_state, LiveRuntimeState::Stopped);
assert!(flag.load(Ordering::SeqCst), "session.shutdown() was called");
assert!(f.structured.is_empty(), "live session removed");
assert_eq!(
@ -388,6 +391,34 @@ async fn stop_structured_runs_execution_cascade_best_effort() {
assert!(f.structured.is_empty(), "live session removed");
}
#[tokio::test]
async fn stop_pty_is_idempotent_after_success() {
let f = fixture();
let a = aid(10);
insert_pty(&f.pty, sid(1), a, nid(100));
let first = f
.stop
.execute(StopLiveAgentInput {
project: project(),
agent_id: a,
})
.await
.unwrap();
let second = f
.stop
.execute(StopLiveAgentInput {
project: project(),
agent_id: a,
})
.await
.unwrap();
assert_eq!(first, second);
assert_eq!(second.runtime_state, LiveRuntimeState::Stopped);
assert_eq!(f.pty_port.kills(), vec![sid(1)], "kill is not repeated");
}
#[tokio::test]
async fn stop_unknown_agent_is_not_found() {
let f = fixture();