feat(background): livraison auto au propriétaire + projection des tâches de fond dans le work-state (T1+T3)
T1 — Wake automatique du propriétaire à la complétion. La complétion d'une tâche de fond est désormais livrée à l'agent propriétaire dès que la session accepte l'envoi (wake.rs : mark_completion_delivered au send accepté), via un drain de flux dédié (structured.rs : drain_reply_stream_with_readiness). L'inbox médiée enfile l'item sans démarrer de tour ni marquer l'agent busy (input/mod.rs : enqueue FIFO silencieux). Régression couverte (tests/agent_wake.rs, tests input/mod.rs). T3 — Tâches de fond projetées dans le read-model du panneau Work. AgentWorkState porte désormais background_tasks (VO AgentBackgroundTaskState), alimenté par un builder best-effort with_background_tasks(store) : union list_open_for_agent + dispatch des completions non livrées par owner_agent_id, erreur store => Vec vide (aucune régression live/busy/tickets). DTO Tauri backgroundTasks et wiring du BackgroundTaskStore côté state.rs. Le frontend, déjà câblé, affiche Cancel/Retry (mapping queued/waiting -> pending, tri sur updatedAtMs). Borne V1 : une tâche terminale déjà livrée n'est plus énumérable (Retry limité à la fenêtre non livrée). Tests : cargo build --workspace OK ; cargo test -p application / -p app-tauri / -p infrastructure verts ; frontend build + vitest verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -298,6 +298,16 @@ impl BackgroundTaskStore for FakeTaskStore {
|
||||
#[derive(Default)]
|
||||
struct FakeSession {
|
||||
prompts: Mutex<Vec<String>>,
|
||||
events: Mutex<Option<Vec<ReplyEvent>>>,
|
||||
}
|
||||
|
||||
impl FakeSession {
|
||||
fn with_events(events: Vec<ReplyEvent>) -> Self {
|
||||
Self {
|
||||
prompts: Mutex::new(Vec::new()),
|
||||
events: Mutex::new(Some(events)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -312,12 +322,12 @@ impl AgentSession for FakeSession {
|
||||
|
||||
async fn send(&self, prompt: &str) -> Result<ReplyStream, AgentSessionError> {
|
||||
self.prompts.lock().unwrap().push(prompt.to_owned());
|
||||
Ok(Box::new(
|
||||
let events = self.events.lock().unwrap().take().unwrap_or_else(|| {
|
||||
vec![ReplyEvent::Final {
|
||||
content: "ack".to_owned(),
|
||||
}]
|
||||
.into_iter(),
|
||||
))
|
||||
});
|
||||
Ok(Box::new(events.into_iter()))
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<(), AgentSessionError> {
|
||||
@ -421,15 +431,16 @@ async fn owner_busy_does_not_start_concurrent_wake_and_keeps_item_queued() {
|
||||
.unwrap();
|
||||
turns.force_busy(owner, ticket(99));
|
||||
|
||||
service(inbox.clone(), turns, tasks, sessions)
|
||||
let err = service(inbox.clone(), turns, tasks, sessions)
|
||||
.wake_agent(
|
||||
&project,
|
||||
owner,
|
||||
WakeReason::BackgroundCompletion { task_id },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err, WakeError::AgentBusy { agent_id: owner });
|
||||
assert!(session.prompts.lock().unwrap().is_empty());
|
||||
assert_eq!(inbox.snapshot(owner).depth, 1);
|
||||
}
|
||||
@ -489,6 +500,35 @@ async fn completion_is_marked_delivered_after_successful_wake() {
|
||||
assert_eq!(tasks.delivered(), vec![task_id]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn completion_is_marked_delivered_once_send_is_accepted_even_if_drain_fails() {
|
||||
let project = project();
|
||||
let owner = agent(1);
|
||||
let task_id = task_id(10);
|
||||
let inbox = Arc::new(FakeInbox::default());
|
||||
let turns = Arc::new(SharedTurnState::default());
|
||||
let tasks = Arc::new(FakeTaskStore::default());
|
||||
let session = Arc::new(FakeSession::with_events(Vec::new()));
|
||||
let sessions = Arc::new(FakeSessionProvider::with_session(session.clone()));
|
||||
tasks.insert(completed_task(&project, owner, task_id, "done"));
|
||||
inbox
|
||||
.enqueue_message(owner, completion_item(owner, task_id, ticket(20)))
|
||||
.unwrap();
|
||||
|
||||
let err = service(inbox, turns, tasks.clone(), sessions)
|
||||
.wake_agent(
|
||||
&project,
|
||||
owner,
|
||||
WakeReason::BackgroundCompletion { task_id },
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, WakeError::Session(_)));
|
||||
assert_eq!(session.prompts.lock().unwrap().len(), 1);
|
||||
assert_eq!(tasks.delivered(), vec![task_id]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wake_drains_exactly_one_item_per_turn() {
|
||||
let project = project();
|
||||
|
||||
Reference in New Issue
Block a user