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

@ -10,8 +10,8 @@ use async_trait::async_trait;
use application::{
AppExitWorkGuardDetail, ConversationLogProvider, ConversationPreviewStatus,
GetAppExitWorkGuardState, GetAppExitWorkGuardStateInput, GetProjectWorkState,
GetProjectWorkStateInput, HandoffProvider, LiveSessionKind, LiveSessions, StructuredSessions,
TerminalSessions, TicketWorkSource, TicketWorkStatus,
GetProjectWorkStateInput, HandoffProvider, LiveRuntimeState, LiveSessionKind, LiveSessions,
StructuredSessions, TerminalSessions, TicketWorkSource, TicketWorkStatus,
};
use domain::mailbox::{
AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket, TurnResolution,
@ -702,6 +702,37 @@ async fn workstate_attaches_live_structured_session_to_manifest_agent() {
assert_eq!(live.kind, LiveSessionKind::Structured);
}
#[tokio::test]
async fn workstate_exposes_stopped_runtime_state_without_live_session() {
let a = agent(10, "alpha");
let f = fixture(std::slice::from_ref(&a));
let snapshot = application::LiveSessionSnapshot {
project_id: f.project.id,
agent_id: a.id,
node_id: nid(300),
session_id: sid(3),
kind: LiveSessionKind::Pty,
};
let live = LiveSessions::new(Arc::clone(&f.pty), Arc::clone(&f.structured));
live.mark_runtime_state(snapshot, LiveRuntimeState::Stopped);
let usecase = GetProjectWorkState::new(
Arc::new(FakeContexts {
manifest: manifest(std::slice::from_ref(&a)),
}),
Arc::new(live),
Arc::clone(&f.input) as Arc<dyn InputMediator>,
Arc::clone(&f.queue) as Arc<dyn AgentQueueSnapshot>,
);
let out = usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
assert_eq!(out.agents[0].live, None);
assert_eq!(out.agents[0].runtime_state, Some(LiveRuntimeState::Stopped));
}
#[tokio::test]
async fn app_exit_guard_is_false_without_busy_agent_or_active_background_task() {
let a = agent(10, "alpha");