feat(workstate): read-model live-state minimal des conversations/délégations (Lot A backend)

Introduit le module application `workstate` (modèle de read-model live + snapshots
des conversations/délégations en cours) et l'expose via la couche terminal
(exports mod/registry). Câble la surface Tauri : DTO, commande et state pour
exposer le live-state au frontend (lib + state + commands), avec tests.

QA verte. Réserve environnementale non bloquante : tests loopback socket Unix
réels non exécutables en sandbox (UnixListener::bind PermissionDenied),
alternatives avec skips vertes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:06:22 +02:00
parent 6cfa0b04c6
commit aae18499a9
10 changed files with 663 additions and 28 deletions

View File

@ -9,7 +9,8 @@ use app_tauri_lib::dto::{
};
use application::AppError;
use application::{
CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput, ListAgentsOutput,
AgentWorkState, CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput,
ListAgentsOutput, LiveSessionKind, LiveWorkSession, ProjectWorkState,
};
use domain::ids::{AgentId, NodeId, ProfileId, SessionId};
use domain::ports::ConversationDetails;
@ -197,6 +198,44 @@ fn live_agent_list_dto_serialises_camelcase_array() {
assert!(arr[0].get("node_id").is_none());
}
#[test]
fn project_work_state_dto_serialises_live_and_busy_camelcase() {
let agent = AgentId::from_uuid(Uuid::from_u128(11));
let profile = ProfileId::from_uuid(Uuid::from_u128(12));
let node = NodeId::from_uuid(Uuid::from_u128(13));
let session = SessionId::from_uuid(Uuid::from_u128(14));
let ticket = domain::TicketId::from_uuid(Uuid::from_u128(15));
let dto = app_tauri_lib::dto::ProjectWorkStateDto::from(ProjectWorkState {
agents: vec![AgentWorkState {
agent_id: agent,
name: "Worker".to_owned(),
profile_id: profile,
live: Some(LiveWorkSession {
node_id: node,
session_id: session,
kind: LiveSessionKind::Structured,
}),
busy: domain::AgentBusyState::Busy {
ticket,
since_ms: 1_234,
},
}],
});
let v = serde_json::to_value(&dto).unwrap();
assert_eq!(v["agents"][0]["agentId"], agent.to_string());
assert_eq!(v["agents"][0]["profileId"], profile.to_string());
assert_eq!(v["agents"][0]["live"]["nodeId"], node.to_string());
assert_eq!(v["agents"][0]["live"]["sessionId"], session.to_string());
assert_eq!(v["agents"][0]["live"]["kind"], "structured");
assert_eq!(v["agents"][0]["busy"]["state"], "busy");
assert_eq!(v["agents"][0]["busy"]["ticket"], ticket.to_string());
assert_eq!(v["agents"][0]["busy"]["sinceMs"], 1_234);
assert!(v["agents"][0].get("agent_id").is_none());
assert!(v["agents"][0]["live"].get("session_id").is_none());
assert!(v["agents"][0]["busy"].get("since_ms").is_none());
}
#[test]
fn launch_agent_request_carries_conversation_id_for_resume() {
let raw = json!({