fix(application,backend,app-tauri): libère l'état busy après Final et draine la queue des messages agents — Final/Error terminent immédiatement le pump structuré via reply_event_ends_turn (le provider n'est pas tenu de fermer son itérateur), release busy/FIFO idempotente bornée au ticket observé (release_turn_accounting: cancel_head + mark_idle seulement si le ticket appartient toujours au tour), cleanup tardif Final/close/timeout/cancel sans libérer un tour successeur, factoring CancelAgentChat sur la même primitive, régression #357 préservée — #362 (QA verte: cargo fmt --check, chat_bridge 36 tests, cancel_agent_chat 4 tests, backend 106 tests, drain_with_readiness_lot1 7 tests)
This commit is contained in:
@ -1165,6 +1165,7 @@ fn automatic_resume_launch_input(
|
||||
mod self_restart_contract_tests {
|
||||
use super::*;
|
||||
use domain::input::InputMediator;
|
||||
use domain::mailbox::AgentMailbox;
|
||||
|
||||
#[test]
|
||||
fn relaunch_preserves_conversation_and_never_requests_fresh_start() {
|
||||
@ -1342,6 +1343,70 @@ mod self_restart_contract_tests {
|
||||
assert_eq!(mailbox.pending(&key), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn human_turn_finish_is_idempotent() {
|
||||
let mailbox = Arc::new(InMemoryMailbox::new());
|
||||
let input = Arc::new(MediatedInbox::new(
|
||||
Arc::clone(&mailbox),
|
||||
Arc::new(SystemMillisClock),
|
||||
));
|
||||
let project_id = ProjectId::from_uuid(Uuid::from_u128(362));
|
||||
let agent_id = AgentId::from_uuid(Uuid::from_u128(1));
|
||||
let key = domain::RuntimeAgentKey::new(project_id, agent_id);
|
||||
let turn = StructuredHumanTurnGuard::start(
|
||||
Arc::clone(&input) as Arc<dyn domain::input::InputMediator>,
|
||||
Arc::clone(&mailbox) as Arc<dyn domain::mailbox::AgentMailbox>,
|
||||
Arc::new(application::RestartRendezvousCoordinator::new()),
|
||||
project_id,
|
||||
agent_id,
|
||||
"first",
|
||||
);
|
||||
|
||||
turn.finish();
|
||||
turn.finish();
|
||||
|
||||
assert_eq!(input.busy_state(key), domain::input::AgentBusyState::Idle);
|
||||
assert_eq!(mailbox.pending(&key), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn late_human_turn_finish_cannot_release_a_successor() {
|
||||
let mailbox = Arc::new(InMemoryMailbox::new());
|
||||
let input = Arc::new(MediatedInbox::new(
|
||||
Arc::clone(&mailbox),
|
||||
Arc::new(SystemMillisClock),
|
||||
));
|
||||
let project_id = ProjectId::from_uuid(Uuid::from_u128(363));
|
||||
let agent_id = AgentId::from_uuid(Uuid::from_u128(1));
|
||||
let key = domain::RuntimeAgentKey::new(project_id, agent_id);
|
||||
let turn = StructuredHumanTurnGuard::start(
|
||||
Arc::clone(&input) as Arc<dyn domain::input::InputMediator>,
|
||||
Arc::clone(&mailbox) as Arc<dyn domain::mailbox::AgentMailbox>,
|
||||
Arc::new(application::RestartRendezvousCoordinator::new()),
|
||||
project_id,
|
||||
agent_id,
|
||||
"first",
|
||||
);
|
||||
let (_, first_ticket) = turn.accounting_identity().expect("first turn owns Busy");
|
||||
|
||||
// Stop owns the first release, then the FIFO starts its successor before a
|
||||
// late Final/close/timeout path reaches the original guard.
|
||||
mailbox.cancel_head(key, first_ticket);
|
||||
input.mark_idle(key);
|
||||
let successor = domain::mailbox::Ticket::new(
|
||||
domain::mailbox::TicketId::new_random(),
|
||||
"user",
|
||||
"successor",
|
||||
);
|
||||
let successor_ticket = successor.id;
|
||||
let _pending = input.enqueue_silent(key, successor);
|
||||
|
||||
turn.finish();
|
||||
|
||||
assert_eq!(input.busy_state(key).ticket(), Some(successor_ticket));
|
||||
assert_eq!(mailbox.pending(&key), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cell_restart_stop_timeout_is_typed_and_never_reaches_the_next_step() {
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(317));
|
||||
@ -1846,11 +1911,16 @@ impl StructuredHumanTurnGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retires the FIFO head and marks the agent idle. Idempotent across clones.
|
||||
/// Retires the FIFO head and marks the agent idle. Idempotent across clones and
|
||||
/// fenced against a late cleanup from an older turn.
|
||||
pub fn finish(&self) {
|
||||
if self.armed.swap(false, std::sync::atomic::Ordering::AcqRel) {
|
||||
self.mailbox.cancel_head(self.agent, self.ticket_id);
|
||||
self.input.mark_idle(self.agent);
|
||||
application::release_turn_accounting(
|
||||
self.input.as_ref(),
|
||||
self.mailbox.as_ref(),
|
||||
self.agent,
|
||||
self.ticket_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user