feat(workstate): snapshot des délégations en file par agent (Lot B backend)
Ajoute un port de lecture ségrégué `AgentQueueSnapshot` (ISP) distinct du `AgentMailbox` mutant : il expose `queue_for(agent)` qui renvoie des `QueuedTicketSnapshot` clonés, ordonnés FIFO, avec position recalculée (0 = tête). Observer la file ne la mute jamais ; le one-shot reply sender reste dans l'adaptateur. - domain : value object `QueuedTicketSnapshot` + trait `AgentQueueSnapshot` (object-safe, partagé en `Arc<dyn …>`). - infrastructure : `InMemoryMailbox` implémente la vue lecture en plus de la vue mutation ; positions recalculées à chaque appel. - application : le read-model work-state liste les délégations en attente via ce port, troncature de l'aperçu de tâche en conservant la longueur d'origine. - app-tauri : DTO `camelCase` des tickets en file câblé dans l'état. Tests verts : domain mailbox (6), infrastructure mailbox (13), application workstate (12), app-tauri dto_agents (20). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -9,8 +9,9 @@ use app_tauri_lib::dto::{
|
||||
};
|
||||
use application::AppError;
|
||||
use application::{
|
||||
AgentWorkState, CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput,
|
||||
ListAgentsOutput, LiveSessionKind, LiveWorkSession, ProjectWorkState,
|
||||
AgentTicketState, AgentWorkState, CreateAgentOutput, InspectConversationOutput,
|
||||
LaunchAgentOutput, ListAgentsOutput, LiveSessionKind, LiveWorkSession, ProjectWorkState,
|
||||
TicketWorkSource, TicketWorkStatus,
|
||||
};
|
||||
use domain::ids::{AgentId, NodeId, ProfileId, SessionId};
|
||||
use domain::ports::ConversationDetails;
|
||||
@ -219,6 +220,7 @@ fn project_work_state_dto_serialises_live_and_busy_camelcase() {
|
||||
ticket,
|
||||
since_ms: 1_234,
|
||||
},
|
||||
tickets: vec![],
|
||||
}],
|
||||
});
|
||||
|
||||
@ -231,11 +233,77 @@ fn project_work_state_dto_serialises_live_and_busy_camelcase() {
|
||||
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_eq!(v["agents"][0]["tickets"], json!([]));
|
||||
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 project_work_state_dto_serialises_tickets_camelcase() {
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(11));
|
||||
let profile = ProfileId::from_uuid(Uuid::from_u128(12));
|
||||
let requester = AgentId::from_uuid(Uuid::from_u128(20));
|
||||
let head_ticket = domain::TicketId::from_uuid(Uuid::from_u128(30));
|
||||
let next_ticket = domain::TicketId::from_uuid(Uuid::from_u128(31));
|
||||
let conversation = domain::ConversationId::from_uuid(Uuid::from_u128(40));
|
||||
let dto = app_tauri_lib::dto::ProjectWorkStateDto::from(ProjectWorkState {
|
||||
agents: vec![AgentWorkState {
|
||||
agent_id: agent,
|
||||
name: "Worker".to_owned(),
|
||||
profile_id: profile,
|
||||
live: None,
|
||||
busy: domain::AgentBusyState::Idle,
|
||||
tickets: vec![
|
||||
AgentTicketState {
|
||||
ticket_id: head_ticket,
|
||||
conversation_id: conversation,
|
||||
position: 0,
|
||||
status: TicketWorkStatus::InProgress,
|
||||
source: TicketWorkSource::Agent {
|
||||
agent_id: requester,
|
||||
},
|
||||
requester_label: "Main".to_owned(),
|
||||
task_preview: "Analyser le module".to_owned(),
|
||||
task_len: 842,
|
||||
},
|
||||
AgentTicketState {
|
||||
ticket_id: next_ticket,
|
||||
conversation_id: conversation,
|
||||
position: 1,
|
||||
status: TicketWorkStatus::Queued,
|
||||
source: TicketWorkSource::Human,
|
||||
requester_label: "User".to_owned(),
|
||||
task_preview: "Vérifier".to_owned(),
|
||||
task_len: 8,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
let head = &v["agents"][0]["tickets"][0];
|
||||
assert_eq!(head["ticketId"], head_ticket.to_string());
|
||||
assert_eq!(head["conversationId"], conversation.to_string());
|
||||
assert_eq!(head["position"], 0);
|
||||
assert_eq!(head["status"], "inProgress");
|
||||
assert_eq!(head["source"]["kind"], "agent");
|
||||
assert_eq!(head["source"]["agentId"], requester.to_string());
|
||||
assert_eq!(head["requesterLabel"], "Main");
|
||||
assert_eq!(head["taskPreview"], "Analyser le module");
|
||||
assert_eq!(head["taskLen"], 842);
|
||||
// snake_case must not leak.
|
||||
assert!(head.get("ticket_id").is_none());
|
||||
assert!(head.get("task_preview").is_none());
|
||||
assert!(head["source"].get("agent_id").is_none());
|
||||
|
||||
let next = &v["agents"][0]["tickets"][1];
|
||||
assert_eq!(next["status"], "queued");
|
||||
assert_eq!(next["source"]["kind"], "human");
|
||||
assert!(next["source"].get("agentId").is_none());
|
||||
assert_eq!(next["requesterLabel"], "User");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_agent_request_carries_conversation_id_for_resume() {
|
||||
let raw = json!({
|
||||
|
||||
Reference in New Issue
Block a user