1use std::collections::HashMap;
13use std::future::Future;
14
15use meerkat_core::lifecycle::InputId;
16#[cfg(test)]
17use meerkat_core::lifecycle::RunId;
18use meerkat_core::lifecycle::core_executor::CoreApplyTerminal;
19use meerkat_core::types::{RunResult, SessionId};
20use meerkat_core::{TurnErrorMetadata, TurnTerminalCauseKind, TurnTerminalOutcome};
21use serde_json::Value;
22
23use crate::meerkat_machine::driver::{
24 RuntimeCompletionResultAttempt, RuntimeCompletionResultAuthority,
25 RuntimeCompletionResultRealized,
26};
27use crate::meerkat_machine::dsl::RuntimeCompletionResultClass;
28use crate::tokio::sync::oneshot;
29
30#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36pub enum CompletionWaitError {
37 #[error("completion channel closed without an authorized result")]
38 ChannelClosed,
39 #[error("{0}")]
40 AuthorityUnavailable(String),
41}
42
43impl CompletionWaitError {
44 pub fn wait_failure_observation(
45 &self,
46 ) -> crate::meerkat_machine::dsl::RuntimeCompletionWaitFailureObservation {
47 match self {
48 Self::ChannelClosed => {
49 crate::meerkat_machine::dsl::RuntimeCompletionWaitFailureObservation::ChannelClosed
50 }
51 Self::AuthorityUnavailable(_) => {
52 crate::meerkat_machine::dsl::RuntimeCompletionWaitFailureObservation::AuthorityUnavailable
53 }
54 }
55 }
56}
57
58#[derive(Debug)]
60pub enum CompletionOutcome {
61 Completed(Box<RunResult>),
63 CompletedWithoutResult,
65 CallbackPending { tool_name: String, args: Value },
68 Cancelled,
70 Abandoned {
74 reason: String,
75 error: TurnErrorMetadata,
76 },
77 AbandonedWithError {
79 reason: String,
80 error: TurnErrorMetadata,
81 },
82 CompletedWithFinalizationFailure { error: TurnErrorMetadata },
88 RuntimeTerminated {
91 reason: String,
92 error: TurnErrorMetadata,
93 },
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct CompletionCleanupObservation {
102 owner_session_id: SessionId,
103 owner_agent_runtime_id: Option<crate::meerkat_machine::dsl::AgentRuntimeId>,
104 owner_fence_token: Option<crate::meerkat_machine::dsl::FenceToken>,
105 owner_runtime_generation: Option<crate::meerkat_machine::dsl::Generation>,
106 owner_runtime_epoch_id: Option<crate::meerkat_machine::dsl::RuntimeEpochId>,
107 observed_outcome: crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome,
108}
109
110impl CompletionCleanupObservation {
111 fn from_realized_result(realized: RuntimeCompletionResultRealized) -> Self {
112 Self {
113 owner_session_id: realized.session_id().clone(),
114 owner_agent_runtime_id: realized.agent_runtime_id().cloned(),
115 owner_fence_token: realized.fence_token(),
116 owner_runtime_generation: realized.runtime_generation(),
117 owner_runtime_epoch_id: realized.runtime_epoch_id().cloned(),
118 observed_outcome: realized.cleanup_observation(),
119 }
120 }
121
122 pub(crate) fn owner_session_id(&self) -> &SessionId {
123 &self.owner_session_id
124 }
125
126 pub(crate) fn owner_agent_runtime_id(
127 &self,
128 ) -> Option<&crate::meerkat_machine::dsl::AgentRuntimeId> {
129 self.owner_agent_runtime_id.as_ref()
130 }
131
132 pub(crate) fn owner_fence_token(&self) -> Option<crate::meerkat_machine::dsl::FenceToken> {
133 self.owner_fence_token
134 }
135
136 pub(crate) fn owner_runtime_generation(
137 &self,
138 ) -> Option<crate::meerkat_machine::dsl::Generation> {
139 self.owner_runtime_generation
140 }
141
142 pub(crate) fn owner_runtime_epoch_id(
143 &self,
144 ) -> Option<&crate::meerkat_machine::dsl::RuntimeEpochId> {
145 self.owner_runtime_epoch_id.as_ref()
146 }
147
148 pub(crate) fn observed_outcome(
149 &self,
150 ) -> crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome {
151 self.observed_outcome
152 }
153}
154
155#[derive(Debug)]
158struct CompletionDelivery {
159 outcome: CompletionOutcome,
160 cleanup_observation: CompletionCleanupObservation,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct CompletionWaiterEntrySnapshot {
169 pub input_id: InputId,
170 pub waiter_count: usize,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Default)]
178pub struct CompletionRegistrySnapshot {
179 pub input_count: usize,
180 pub waiter_count: usize,
181 pub waiting_inputs: Vec<CompletionWaiterEntrySnapshot>,
182}
183
184#[derive(Debug)]
186pub struct CompletionHandle {
187 rx: oneshot::Receiver<Result<CompletionDelivery, CompletionWaitError>>,
188}
189
190impl CompletionHandle {
191 async fn try_wait_delivery(self) -> Result<CompletionDelivery, CompletionWaitError> {
192 self.rx
193 .await
194 .unwrap_or(Err(CompletionWaitError::ChannelClosed))
195 }
196
197 pub async fn try_wait(self) -> Result<CompletionOutcome, CompletionWaitError> {
199 self.try_wait_delivery()
200 .await
201 .map(|delivery| delivery.outcome)
202 }
203
204 pub async fn try_wait_with_cleanup_observation(
207 self,
208 ) -> Result<(CompletionOutcome, CompletionCleanupObservation), CompletionWaitError> {
209 let delivery = self.try_wait_delivery().await?;
210 Ok((delivery.outcome, delivery.cleanup_observation))
211 }
212
213 pub async fn wait(self) -> Result<CompletionOutcome, CompletionWaitError> {
215 self.try_wait().await
216 }
217
218 #[cfg(test)]
220 pub(crate) async fn wait_authorized(self) -> CompletionOutcome {
221 self.wait()
222 .await
223 .expect("completion waiter closed without an authorized result")
224 }
225
226 pub fn with_cleanup<F, Fut>(self, cleanup: F) -> Self
230 where
231 F: FnOnce() -> Fut + Send + 'static,
232 Fut: Future<Output = ()> + Send + 'static,
233 {
234 let (tx, rx) = oneshot::channel();
235 crate::tokio::spawn(async move {
236 let outcome = self.try_wait_delivery().await;
237 cleanup().await;
238 let _ = tx.send(outcome);
239 });
240 Self { rx }
241 }
242
243 pub fn with_outcome_cleanup<F, Fut>(self, cleanup: F) -> Self
248 where
249 F: FnOnce(CompletionCleanupObservation) -> Fut + Send + 'static,
250 Fut: Future<Output = ()> + Send + 'static,
251 {
252 let (tx, rx) = oneshot::channel();
253 crate::tokio::spawn(async move {
254 let outcome = match self.try_wait_delivery().await {
255 Ok(delivery) => {
256 cleanup(delivery.cleanup_observation.clone()).await;
257 Ok(delivery)
258 }
259 Err(error) => Err(error),
260 };
261 let _ = tx.send(outcome);
262 });
263 Self { rx }
264 }
265
266 pub fn with_completion_cleanup<F, Fut>(self, cleanup: F) -> Self
269 where
270 F: FnOnce(Result<CompletionCleanupObservation, CompletionWaitError>) -> Fut
271 + Send
272 + 'static,
273 Fut: Future<Output = ()> + Send + 'static,
274 {
275 let (tx, rx) = oneshot::channel();
276 crate::tokio::spawn(async move {
277 let outcome = self.try_wait_delivery().await;
278 match &outcome {
279 Ok(delivery) => cleanup(Ok(delivery.cleanup_observation.clone())).await,
280 Err(error) => cleanup(Err(error.clone())).await,
281 }
282 let _ = tx.send(outcome);
283 });
284 Self { rx }
285 }
286
287 #[cfg(test)]
288 fn already_resolved_internal(
289 outcome: CompletionOutcome,
290 realized: RuntimeCompletionResultRealized,
291 ) -> Self {
292 let (tx, rx) = oneshot::channel();
293 let _ = tx.send(Ok(CompletionDelivery {
294 outcome,
295 cleanup_observation: CompletionCleanupObservation::from_realized_result(realized),
296 }));
297 Self { rx }
298 }
299
300 #[cfg(test)]
301 pub(crate) fn already_resolved_with_generated_class(
302 outcome: CompletionOutcome,
303 expected_class: crate::meerkat_machine::dsl::RuntimeCompletionResultClass,
304 terminal: crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation,
305 finalization: crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation,
306 ) -> Result<Self, crate::RuntimeDriverError> {
307 let run_id = if terminal
308 == crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::RuntimeTerminated
309 {
310 None
311 } else {
312 Some(RunId::new())
313 };
314 let authority =
315 crate::meerkat_machine::driver::machine_resolve_pre_resolved_runtime_completion_result(
316 run_id.as_ref(),
317 terminal,
318 finalization,
319 )?;
320 let attempt = authority.begin_surface_resolution();
321 if !attempt.allows(expected_class) {
322 let generated_class = attempt.class();
323 attempt.fail();
324 return Err(crate::RuntimeDriverError::Internal(format!(
325 "generated runtime completion authority returned {generated_class:?}, expected {expected_class:?}",
326 )));
327 }
328 Ok(Self::already_resolved_internal(outcome, attempt.realize()))
329 }
330
331 #[cfg(test)]
332 pub(crate) fn already_completed_without_result() -> Result<Self, crate::RuntimeDriverError> {
333 Self::already_resolved_with_generated_class(
334 CompletionOutcome::CompletedWithoutResult,
335 crate::meerkat_machine::dsl::RuntimeCompletionResultClass::CompletedWithoutResult,
336 crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::NoResult,
337 crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Succeeded,
338 )
339 }
340
341 #[cfg(test)]
342 pub(crate) fn already_runtime_apply_failed(
343 reason: String,
344 error: TurnErrorMetadata,
345 ) -> Result<Self, crate::RuntimeDriverError> {
346 Self::already_resolved_with_generated_class(
347 CompletionOutcome::AbandonedWithError { reason, error },
348 crate::meerkat_machine::dsl::RuntimeCompletionResultClass::AbandonedWithError,
349 crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::NoResult,
350 crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Failed,
351 )
352 }
353
354 #[cfg(test)]
355 pub(crate) fn already_runtime_terminated(
356 reason: String,
357 ) -> Result<Self, crate::RuntimeDriverError> {
358 Self::already_resolved_with_generated_class(
359 CompletionOutcome::runtime_terminated(&reason),
360 crate::meerkat_machine::dsl::RuntimeCompletionResultClass::RuntimeTerminated,
361 crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::RuntimeTerminated,
362 crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Succeeded,
363 )
364 }
365
366 #[cfg(test)]
367 pub(crate) fn already_callback_pending(
368 tool_name: String,
369 args: Value,
370 ) -> Result<Self, crate::RuntimeDriverError> {
371 Self::already_resolved_with_generated_class(
372 CompletionOutcome::CallbackPending { tool_name, args },
373 crate::meerkat_machine::dsl::RuntimeCompletionResultClass::CallbackPending,
374 crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::CallbackPending,
375 crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Succeeded,
376 )
377 }
378}
379
380impl CompletionOutcome {
381 fn runtime_terminated(reason: &str) -> Self {
385 Self::RuntimeTerminated {
386 reason: reason.to_string(),
387 error: TurnErrorMetadata::terminal(
388 TurnTerminalCauseKind::FatalFailure,
389 TurnTerminalOutcome::Failed,
390 reason,
391 ),
392 }
393 }
394
395 pub fn abandoned_reason(&self) -> Option<&str> {
396 match self {
397 Self::Abandoned { reason, .. } | Self::AbandonedWithError { reason, .. } => {
398 Some(reason)
399 }
400 _ => None,
401 }
402 }
403
404 pub fn error_metadata(&self) -> Option<&TurnErrorMetadata> {
405 match self {
406 Self::Abandoned { error, .. }
407 | Self::AbandonedWithError { error, .. }
408 | Self::CompletedWithFinalizationFailure { error, .. }
409 | Self::RuntimeTerminated { error, .. } => Some(error),
410 _ => None,
411 }
412 }
413}
414
415#[derive(Default)]
420pub(crate) struct CompletionRegistry {
421 waiters:
422 HashMap<InputId, Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>>,
423}
424
425impl CompletionRegistry {
426 pub(crate) fn new() -> Self {
427 Self::default()
428 }
429
430 fn take_waiters(
431 &mut self,
432 input_id: &InputId,
433 ) -> Option<Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>> {
434 self.waiters.remove(input_id)
435 }
436
437 fn send_outcome(
438 senders: Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>,
439 outcome: CompletionOutcome,
440 cleanup_observation: CompletionCleanupObservation,
441 ) {
442 for tx in senders {
443 let outcome = match &outcome {
444 CompletionOutcome::Completed(result) => {
445 CompletionOutcome::Completed(Box::new(result.as_ref().clone()))
446 }
447 CompletionOutcome::CompletedWithoutResult => {
448 CompletionOutcome::CompletedWithoutResult
449 }
450 CompletionOutcome::CallbackPending { tool_name, args } => {
451 CompletionOutcome::CallbackPending {
452 tool_name: tool_name.clone(),
453 args: args.clone(),
454 }
455 }
456 CompletionOutcome::Cancelled => CompletionOutcome::Cancelled,
457 CompletionOutcome::Abandoned { reason, error } => CompletionOutcome::Abandoned {
458 reason: reason.clone(),
459 error: error.clone(),
460 },
461 CompletionOutcome::AbandonedWithError { reason, error } => {
462 CompletionOutcome::AbandonedWithError {
463 reason: reason.clone(),
464 error: error.clone(),
465 }
466 }
467 CompletionOutcome::CompletedWithFinalizationFailure { error } => {
468 CompletionOutcome::CompletedWithFinalizationFailure {
469 error: error.clone(),
470 }
471 }
472 CompletionOutcome::RuntimeTerminated { reason, error } => {
473 CompletionOutcome::RuntimeTerminated {
474 reason: reason.clone(),
475 error: error.clone(),
476 }
477 }
478 };
479 let _ = tx.send(Ok(CompletionDelivery {
480 outcome,
481 cleanup_observation: cleanup_observation.clone(),
482 }));
483 }
484 }
485
486 fn send_error(
487 senders: Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>,
488 error: CompletionWaitError,
489 ) {
490 for tx in senders {
491 let _ = tx.send(Err(error.clone()));
492 }
493 }
494
495 fn authority_mismatch_error(
496 authority: &RuntimeCompletionResultAttempt,
497 expected: RuntimeCompletionResultClass,
498 ) -> CompletionWaitError {
499 CompletionWaitError::AuthorityUnavailable(format!(
500 "generated runtime completion authority returned {:?}, expected {expected:?}",
501 authority.class()
502 ))
503 }
504
505 fn fail_input_authority_mismatch(
506 &mut self,
507 input_id: &InputId,
508 authority: RuntimeCompletionResultAttempt,
509 expected: RuntimeCompletionResultClass,
510 ) {
511 let error = Self::authority_mismatch_error(&authority, expected);
512 authority.fail();
513 if let Some(senders) = self.take_waiters(input_id) {
514 Self::send_error(senders, error);
515 }
516 }
517
518 #[cfg(test)]
519 fn fail_inputs_authority_mismatch<I>(
520 &mut self,
521 input_ids: I,
522 authority: RuntimeCompletionResultAttempt,
523 expected: RuntimeCompletionResultClass,
524 ) where
525 I: IntoIterator<Item = InputId>,
526 {
527 let error = Self::authority_mismatch_error(&authority, expected);
528 authority.fail();
529 self.fail_inputs(input_ids, error);
530 }
531
532 fn fail_inputs_authority_unavailable<I>(
533 &mut self,
534 input_ids: I,
535 authority: RuntimeCompletionResultAttempt,
536 reason: impl Into<String>,
537 ) where
538 I: IntoIterator<Item = InputId>,
539 {
540 authority.fail();
541 self.fail_inputs(
542 input_ids,
543 CompletionWaitError::AuthorityUnavailable(reason.into()),
544 );
545 }
546
547 fn cleanup_from_realized_attempt(
548 authority: RuntimeCompletionResultAttempt,
549 ) -> CompletionCleanupObservation {
550 CompletionCleanupObservation::from_realized_result(authority.realize())
551 }
552
553 pub(crate) fn register(&mut self, input_id: InputId) -> CompletionHandle {
558 let (tx, rx) = oneshot::channel();
559 self.waiters.entry(input_id).or_default().push(tx);
560 CompletionHandle { rx }
561 }
562
563 fn resolve_completed(
565 &mut self,
566 input_id: &InputId,
567 result: RunResult,
568 cleanup_observation: CompletionCleanupObservation,
569 ) {
570 if let Some(senders) = self.take_waiters(input_id) {
571 Self::send_outcome(
572 senders,
573 CompletionOutcome::Completed(Box::new(result)),
574 cleanup_observation,
575 );
576 }
577 }
578
579 #[cfg(test)]
580 pub(crate) fn resolve_completed_authorized(
581 &mut self,
582 input_id: &InputId,
583 result: RunResult,
584 authority: RuntimeCompletionResultAuthority,
585 ) {
586 let expected = RuntimeCompletionResultClass::Completed;
587 let attempt = authority.begin_surface_resolution();
588 if !attempt.allows(expected) {
589 self.fail_input_authority_mismatch(input_id, attempt, expected);
590 return;
591 }
592 self.resolve_completed(
593 input_id,
594 result,
595 Self::cleanup_from_realized_attempt(attempt),
596 );
597 }
598
599 pub(crate) fn resolve_runtime_completion_authorized<I>(
600 &mut self,
601 input_ids: I,
602 terminal: Option<&CoreApplyTerminal>,
603 authority: RuntimeCompletionResultAuthority,
604 finalization_error: Option<TurnErrorMetadata>,
605 ) where
606 I: IntoIterator<Item = InputId>,
607 {
608 let input_ids: Vec<InputId> = input_ids.into_iter().collect();
609 let attempt = authority.begin_surface_resolution();
610 match attempt.class() {
611 RuntimeCompletionResultClass::Completed => {
612 let Some(CoreApplyTerminal::RunResult(result)) = terminal else {
613 self.fail_inputs_authority_unavailable(
614 input_ids,
615 attempt,
616 "runtime completion authority resolved Completed without result payload",
617 );
618 return;
619 };
620 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
621 for input_id in input_ids {
622 self.resolve_completed(
623 &input_id,
624 result.as_ref().clone(),
625 cleanup_observation.clone(),
626 );
627 }
628 }
629 RuntimeCompletionResultClass::CompletedWithoutResult => {
630 if !matches!(terminal, Some(CoreApplyTerminal::NoPendingBoundary) | None) {
631 self.fail_inputs_authority_unavailable(
632 input_ids,
633 attempt,
634 "runtime completion authority resolved CompletedWithoutResult with terminal payload",
635 );
636 return;
637 }
638 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
639 for input_id in input_ids {
640 self.resolve_without_result(&input_id, cleanup_observation.clone());
641 }
642 }
643 RuntimeCompletionResultClass::CallbackPending => {
644 let Some(CoreApplyTerminal::CallbackPending { tool_name, args }) = terminal else {
645 self.fail_inputs_authority_unavailable(
646 input_ids,
647 attempt,
648 "runtime completion authority resolved CallbackPending without callback payload",
649 );
650 return;
651 };
652 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
653 for input_id in input_ids {
654 self.resolve_callback_pending(
655 &input_id,
656 tool_name.clone(),
657 args.clone(),
658 cleanup_observation.clone(),
659 );
660 }
661 }
662 RuntimeCompletionResultClass::Cancelled => {
663 if terminal.is_some() || finalization_error.is_some() {
664 self.fail_inputs_authority_unavailable(
665 input_ids,
666 attempt,
667 "runtime completion authority resolved Cancelled with payload",
668 );
669 return;
670 }
671 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
672 for input_id in input_ids {
673 self.resolve_cancelled(&input_id, cleanup_observation.clone());
674 }
675 }
676 RuntimeCompletionResultClass::AbandonedWithError => {
677 if matches!(terminal, Some(CoreApplyTerminal::RunResult(_))) {
678 self.fail_inputs_authority_unavailable(
679 input_ids,
680 attempt,
681 "runtime completion authority resolved AbandonedWithError with result payload",
682 );
683 return;
684 }
685 let Some(error) = finalization_error else {
686 self.fail_inputs_authority_unavailable(
687 input_ids,
688 attempt,
689 "runtime completion authority resolved AbandonedWithError without typed error",
690 );
691 return;
692 };
693 let reason = error
694 .detail
695 .clone()
696 .unwrap_or_else(|| "runtime finalization failed".to_string());
697 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
698 for input_id in input_ids {
699 self.resolve_abandoned_with_error(
700 &input_id,
701 reason.clone(),
702 error.clone(),
703 cleanup_observation.clone(),
704 );
705 }
706 }
707 RuntimeCompletionResultClass::CompletedWithFinalizationFailure => {
708 let Some(CoreApplyTerminal::RunResult(_result)) = terminal else {
709 self.fail_inputs_authority_unavailable(
710 input_ids,
711 attempt,
712 "runtime completion authority resolved CompletedWithFinalizationFailure without result payload",
713 );
714 return;
715 };
716 let Some(error) = finalization_error else {
717 self.fail_inputs_authority_unavailable(
718 input_ids,
719 attempt,
720 "runtime completion authority resolved finalization failure without typed error",
721 );
722 return;
723 };
724 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
725 for input_id in input_ids {
726 self.resolve_completed_with_finalization_failure(
727 &input_id,
728 error.clone(),
729 cleanup_observation.clone(),
730 );
731 }
732 }
733 RuntimeCompletionResultClass::RuntimeTerminated => {
734 if terminal.is_some() || finalization_error.is_some() {
735 self.fail_inputs_authority_unavailable(
736 input_ids,
737 attempt,
738 "runtime completion authority resolved RuntimeTerminated with payload",
739 );
740 return;
741 }
742 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
743 for input_id in input_ids {
744 if let Some(senders) = self.take_waiters(&input_id) {
745 Self::send_outcome(
746 senders,
747 CompletionOutcome::runtime_terminated("runtime terminated"),
748 cleanup_observation.clone(),
749 );
750 }
751 }
752 }
753 }
754 }
755
756 fn resolve_without_result(
758 &mut self,
759 input_id: &InputId,
760 cleanup_observation: CompletionCleanupObservation,
761 ) {
762 if let Some(senders) = self.take_waiters(input_id) {
763 Self::send_outcome(
764 senders,
765 CompletionOutcome::CompletedWithoutResult,
766 cleanup_observation,
767 );
768 }
769 }
770
771 pub(crate) fn resolve_without_result_authorized(
772 &mut self,
773 input_id: &InputId,
774 authority: RuntimeCompletionResultAuthority,
775 ) {
776 let expected = RuntimeCompletionResultClass::CompletedWithoutResult;
777 let attempt = authority.begin_surface_resolution();
778 if !attempt.allows(expected) {
779 self.fail_input_authority_mismatch(input_id, attempt, expected);
780 return;
781 }
782 self.resolve_without_result(input_id, Self::cleanup_from_realized_attempt(attempt));
783 }
784
785 fn resolve_callback_pending(
787 &mut self,
788 input_id: &InputId,
789 tool_name: String,
790 args: Value,
791 cleanup_observation: CompletionCleanupObservation,
792 ) {
793 if let Some(senders) = self.take_waiters(input_id) {
794 Self::send_outcome(
795 senders,
796 CompletionOutcome::CallbackPending { tool_name, args },
797 cleanup_observation,
798 );
799 }
800 }
801
802 #[cfg(test)]
803 pub(crate) fn resolve_callback_pending_authorized(
804 &mut self,
805 input_id: &InputId,
806 tool_name: String,
807 args: Value,
808 authority: RuntimeCompletionResultAuthority,
809 ) {
810 let expected = RuntimeCompletionResultClass::CallbackPending;
811 let attempt = authority.begin_surface_resolution();
812 if !attempt.allows(expected) {
813 self.fail_input_authority_mismatch(input_id, attempt, expected);
814 return;
815 }
816 self.resolve_callback_pending(
817 input_id,
818 tool_name,
819 args,
820 Self::cleanup_from_realized_attempt(attempt),
821 );
822 }
823
824 fn resolve_cancelled(
826 &mut self,
827 input_id: &InputId,
828 cleanup_observation: CompletionCleanupObservation,
829 ) {
830 if let Some(senders) = self.take_waiters(input_id) {
831 Self::send_outcome(senders, CompletionOutcome::Cancelled, cleanup_observation);
832 }
833 }
834
835 #[cfg(test)]
836 pub(crate) fn resolve_cancelled_authorized(
837 &mut self,
838 input_id: &InputId,
839 authority: RuntimeCompletionResultAuthority,
840 ) {
841 let expected = RuntimeCompletionResultClass::Cancelled;
842 let attempt = authority.begin_surface_resolution();
843 if !attempt.allows(expected) {
844 self.fail_input_authority_mismatch(input_id, attempt, expected);
845 return;
846 }
847 self.resolve_cancelled(input_id, Self::cleanup_from_realized_attempt(attempt));
848 }
849
850 fn resolve_abandoned_with_error(
852 &mut self,
853 input_id: &InputId,
854 reason: String,
855 error: TurnErrorMetadata,
856 cleanup_observation: CompletionCleanupObservation,
857 ) {
858 if let Some(senders) = self.take_waiters(input_id) {
859 Self::send_outcome(
860 senders,
861 CompletionOutcome::AbandonedWithError { reason, error },
862 cleanup_observation,
863 );
864 }
865 }
866
867 #[cfg(test)]
868 pub(crate) fn resolve_abandoned_with_error_authorized(
869 &mut self,
870 input_id: &InputId,
871 reason: String,
872 error: TurnErrorMetadata,
873 authority: RuntimeCompletionResultAuthority,
874 ) {
875 let expected = RuntimeCompletionResultClass::AbandonedWithError;
876 let attempt = authority.begin_surface_resolution();
877 if !attempt.allows(expected) {
878 self.fail_input_authority_mismatch(input_id, attempt, expected);
879 return;
880 }
881 self.resolve_abandoned_with_error(
882 input_id,
883 reason,
884 error,
885 Self::cleanup_from_realized_attempt(attempt),
886 );
887 }
888
889 fn resolve_completed_with_finalization_failure(
892 &mut self,
893 input_id: &InputId,
894 error: TurnErrorMetadata,
895 cleanup_observation: CompletionCleanupObservation,
896 ) {
897 if let Some(senders) = self.take_waiters(input_id) {
898 Self::send_outcome(
899 senders,
900 CompletionOutcome::CompletedWithFinalizationFailure { error },
901 cleanup_observation,
902 );
903 }
904 }
905
906 #[cfg(test)]
907 pub(crate) fn resolve_completed_with_finalization_failure_authorized(
908 &mut self,
909 input_id: &InputId,
910 error: TurnErrorMetadata,
911 authority: RuntimeCompletionResultAuthority,
912 ) {
913 let expected = RuntimeCompletionResultClass::CompletedWithFinalizationFailure;
914 let attempt = authority.begin_surface_resolution();
915 if !attempt.allows(expected) {
916 self.fail_input_authority_mismatch(input_id, attempt, expected);
917 return;
918 }
919 self.resolve_completed_with_finalization_failure(
920 input_id,
921 error,
922 Self::cleanup_from_realized_attempt(attempt),
923 );
924 }
925
926 pub(crate) fn resolve_all_runtime_terminated(
932 &mut self,
933 reason: &str,
934 authority: RuntimeCompletionResultAuthority,
935 ) {
936 let expected = RuntimeCompletionResultClass::RuntimeTerminated;
937 let attempt = authority.begin_surface_resolution();
938 if !attempt.allows(expected) {
939 let error = Self::authority_mismatch_error(&attempt, expected);
940 attempt.fail();
941 self.fail_all_waiters(error);
942 return;
943 }
944 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
945 for (_, senders) in self.waiters.drain() {
946 Self::send_outcome(
947 senders,
948 CompletionOutcome::runtime_terminated(reason),
949 cleanup_observation.clone(),
950 );
951 }
952 }
953
954 #[cfg(test)]
955 pub(crate) fn resolve_inputs_runtime_terminated<I>(
956 &mut self,
957 input_ids: I,
958 reason: &str,
959 authority: RuntimeCompletionResultAuthority,
960 ) where
961 I: IntoIterator<Item = InputId>,
962 {
963 let input_ids: Vec<InputId> = input_ids.into_iter().collect();
964 let expected = RuntimeCompletionResultClass::RuntimeTerminated;
965 let attempt = authority.begin_surface_resolution();
966 if !attempt.allows(expected) {
967 self.fail_inputs_authority_mismatch(input_ids, attempt, expected);
968 return;
969 }
970 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
971 for input_id in input_ids {
972 if let Some(senders) = self.take_waiters(&input_id) {
973 Self::send_outcome(
974 senders,
975 CompletionOutcome::runtime_terminated(reason),
976 cleanup_observation.clone(),
977 );
978 }
979 }
980 }
981
982 pub(crate) fn fail_all_waiters(&mut self, error: CompletionWaitError) {
983 for (_, senders) in self.waiters.drain() {
984 Self::send_error(senders, error.clone());
985 }
986 }
987
988 pub(crate) fn fail_inputs<I>(&mut self, input_ids: I, error: CompletionWaitError)
989 where
990 I: IntoIterator<Item = InputId>,
991 {
992 for input_id in input_ids {
993 if let Some(senders) = self.take_waiters(&input_id) {
994 Self::send_error(senders, error.clone());
995 }
996 }
997 }
998
999 pub(crate) fn resolve_not_pending_runtime_terminated<F>(
1002 &mut self,
1003 mut is_still_pending: F,
1004 reason: &str,
1005 authority: RuntimeCompletionResultAuthority,
1006 ) where
1007 F: FnMut(&InputId) -> bool,
1008 {
1009 let expected = RuntimeCompletionResultClass::RuntimeTerminated;
1010 let attempt = authority.begin_surface_resolution();
1011 if !attempt.allows(expected) {
1012 let error = Self::authority_mismatch_error(&attempt, expected);
1013 attempt.fail();
1014 self.waiters.retain(|input_id, senders| {
1015 if is_still_pending(input_id) {
1016 return true;
1017 }
1018
1019 Self::send_error(std::mem::take(senders), error.clone());
1020 false
1021 });
1022 return;
1023 }
1024
1025 let mut ready = Vec::new();
1026 self.waiters.retain(|input_id, senders| {
1027 if is_still_pending(input_id) {
1028 return true;
1029 }
1030
1031 ready.push(std::mem::take(senders));
1032 false
1033 });
1034 if ready.is_empty() {
1035 attempt.abandon();
1036 return;
1037 }
1038 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
1039 for senders in ready {
1040 Self::send_outcome(
1041 senders,
1042 CompletionOutcome::runtime_terminated(reason),
1043 cleanup_observation.clone(),
1044 );
1045 }
1046 }
1047
1048 pub(crate) fn diagnostic_snapshot(&self) -> CompletionRegistrySnapshot {
1050 let mut waiting_inputs: Vec<_> = self
1051 .waiters
1052 .iter()
1053 .map(|(input_id, senders)| CompletionWaiterEntrySnapshot {
1054 input_id: input_id.clone(),
1055 waiter_count: senders.len(),
1056 })
1057 .collect();
1058 waiting_inputs
1059 .sort_by(|left, right| left.input_id.to_string().cmp(&right.input_id.to_string()));
1060
1061 CompletionRegistrySnapshot {
1062 input_count: waiting_inputs.len(),
1063 waiter_count: waiting_inputs.iter().map(|entry| entry.waiter_count).sum(),
1064 waiting_inputs,
1065 }
1066 }
1067
1068 #[cfg(test)]
1073 pub fn debug_has_waiters(&self) -> bool {
1074 !self.waiters.is_empty()
1075 }
1076
1077 #[cfg(test)]
1082 pub fn debug_waiter_count(&self) -> usize {
1083 self.waiters.values().map(Vec::len).sum()
1084 }
1085}
1086
1087#[cfg(test)]
1088#[allow(clippy::unwrap_used, clippy::panic)]
1089mod tests {
1090 use super::*;
1091 use crate::meerkat_machine::dsl::{
1092 RuntimeCompletionObservedOutcome, RuntimeCompletionResultClass,
1093 };
1094 use meerkat_core::types::{SessionId, Usage};
1095
1096 fn make_run_result() -> RunResult {
1097 RunResult {
1098 text: "hello".into(),
1099 session_id: SessionId::new(),
1100 usage: Usage::default(),
1101 turns: 1,
1102 tool_calls: 0,
1103 terminal_cause_kind: None,
1104 structured_output: None,
1105 extraction_error: None,
1106 schema_warnings: None,
1107 skill_diagnostics: None,
1108 }
1109 }
1110
1111 fn authority(
1112 result_class: RuntimeCompletionResultClass,
1113 cleanup_observation: RuntimeCompletionObservedOutcome,
1114 ) -> RuntimeCompletionResultAuthority {
1115 crate::meerkat_machine::driver::test_runtime_completion_authority(
1116 result_class,
1117 cleanup_observation,
1118 )
1119 }
1120
1121 #[tokio::test]
1122 async fn register_and_complete() {
1123 let mut registry = CompletionRegistry::new();
1124 let input_id = InputId::new();
1125 let handle = registry.register(input_id.clone());
1126
1127 assert!(registry.debug_has_waiters());
1128 assert_eq!(registry.debug_waiter_count(), 1);
1129
1130 let result = make_run_result();
1131 registry.resolve_completed_authorized(
1132 &input_id,
1133 result,
1134 authority(
1135 RuntimeCompletionResultClass::Completed,
1136 RuntimeCompletionObservedOutcome::Completed,
1137 ),
1138 );
1139
1140 match handle.wait_authorized().await {
1141 CompletionOutcome::Completed(r) => assert_eq!(r.text, "hello"),
1142 other => panic!("Expected Completed, got {other:?}"),
1143 }
1144 }
1145
1146 #[tokio::test]
1147 async fn register_and_fail_waiter() {
1148 let mut registry = CompletionRegistry::new();
1149 let input_id = InputId::new();
1150 let handle = registry.register(input_id.clone());
1151
1152 registry.fail_inputs(
1153 [input_id],
1154 CompletionWaitError::AuthorityUnavailable("retired".into()),
1155 );
1156
1157 match handle.try_wait().await {
1158 Err(CompletionWaitError::AuthorityUnavailable(reason)) => assert_eq!(reason, "retired"),
1159 other => panic!("Expected wait error, got {other:?}"),
1160 }
1161 }
1162
1163 #[tokio::test]
1164 async fn mismatched_result_authority_fails_waiter_closed() {
1165 let mut registry = CompletionRegistry::new();
1166 let input_id = InputId::new();
1167 let handle = registry.register(input_id.clone());
1168
1169 registry.resolve_completed_authorized(
1170 &input_id,
1171 make_run_result(),
1172 authority(
1173 RuntimeCompletionResultClass::Cancelled,
1174 RuntimeCompletionObservedOutcome::Cancelled,
1175 ),
1176 );
1177
1178 assert!(!registry.debug_has_waiters());
1179 match handle.try_wait().await {
1180 Err(CompletionWaitError::AuthorityUnavailable(reason)) => {
1181 assert!(reason.contains("Cancelled"));
1182 assert!(reason.contains("Completed"));
1183 }
1184 other => panic!("Expected authority mismatch wait error, got {other:?}"),
1185 }
1186 }
1187
1188 #[tokio::test]
1189 async fn resolve_all_runtime_terminated() {
1190 let mut registry = CompletionRegistry::new();
1191 let h1 = registry.register(InputId::new());
1192 let h2 = registry.register(InputId::new());
1193
1194 registry.resolve_all_runtime_terminated(
1195 "runtime stopped",
1196 authority(
1197 RuntimeCompletionResultClass::RuntimeTerminated,
1198 RuntimeCompletionObservedOutcome::RuntimeTerminated,
1199 ),
1200 );
1201
1202 assert!(!registry.debug_has_waiters());
1203
1204 match h1.wait_authorized().await {
1205 CompletionOutcome::RuntimeTerminated { reason, .. } => {
1206 assert_eq!(reason, "runtime stopped");
1207 }
1208 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1209 }
1210 match h2.wait_authorized().await {
1211 CompletionOutcome::RuntimeTerminated { reason, .. } => {
1212 assert_eq!(reason, "runtime stopped");
1213 }
1214 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1215 }
1216 }
1217
1218 #[tokio::test]
1219 async fn mismatched_runtime_terminated_authority_fails_all_waiters_closed() {
1220 let mut registry = CompletionRegistry::new();
1221 let h1 = registry.register(InputId::new());
1222 let h2 = registry.register(InputId::new());
1223
1224 registry.resolve_all_runtime_terminated(
1225 "runtime stopped",
1226 authority(
1227 RuntimeCompletionResultClass::CompletedWithoutResult,
1228 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1229 ),
1230 );
1231
1232 assert!(!registry.debug_has_waiters());
1233 for handle in [h1, h2] {
1234 match handle.try_wait().await {
1235 Err(CompletionWaitError::AuthorityUnavailable(reason)) => {
1236 assert!(reason.contains("CompletedWithoutResult"));
1237 assert!(reason.contains("RuntimeTerminated"));
1238 }
1239 other => panic!("Expected authority mismatch wait error, got {other:?}"),
1240 }
1241 }
1242 }
1243
1244 #[tokio::test]
1245 async fn cleanup_rejects_observation_from_another_session() {
1246 let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1247 let source_session_id = SessionId::new();
1248 let target_session_id = SessionId::new();
1249 adapter
1250 .prepare_bindings(source_session_id.clone())
1251 .await
1252 .expect("source session should prepare runtime bindings");
1253 adapter
1254 .prepare_bindings(target_session_id.clone())
1255 .await
1256 .expect("target session should prepare runtime bindings");
1257
1258 let input = crate::Input::Prompt(crate::PromptInput::new(
1259 "source session pending completion",
1260 None,
1261 ));
1262 let (_outcome, handle) = adapter
1263 .accept_input_with_completion(&source_session_id, input)
1264 .await
1265 .expect("source input should be accepted");
1266 let handle = handle.expect("source input should have a completion waiter");
1267 adapter
1268 .stop_runtime_executor(&source_session_id, "source stopped")
1269 .await
1270 .expect("source stop should resolve waiter");
1271 let (_outcome, observation) = handle
1272 .try_wait_with_cleanup_observation()
1273 .await
1274 .expect("waiter should resolve with generated cleanup observation");
1275
1276 assert_eq!(observation.owner_session_id(), &source_session_id);
1277 let err = adapter
1278 .resolve_runtime_completion_cleanup(
1279 &target_session_id,
1280 observation,
1281 false,
1282 crate::meerkat_machine::dsl::RuntimeCompletionLiveSessionObservation::Absent,
1283 )
1284 .await
1285 .expect_err("cleanup must reject an observation minted for another session");
1286 assert!(
1287 matches!(err, crate::RuntimeDriverError::ValidationFailed { .. }),
1288 "expected generated cleanup validation failure, got {err:?}"
1289 );
1290 }
1291
1292 #[tokio::test]
1293 async fn cleanup_rejects_stale_same_session_observation_after_rebinding() {
1294 let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1295 let session_id = SessionId::new();
1296 adapter
1297 .prepare_bindings(session_id.clone())
1298 .await
1299 .expect("session should prepare initial runtime bindings");
1300
1301 let input = crate::Input::Prompt(crate::PromptInput::new(
1302 "same session pending completion",
1303 None,
1304 ));
1305 let (_outcome, handle) = adapter
1306 .accept_input_with_completion(&session_id, input)
1307 .await
1308 .expect("input should be accepted");
1309 let handle = handle.expect("input should have a completion waiter");
1310 adapter
1311 .stop_runtime_executor(&session_id, "first runtime stopped")
1312 .await
1313 .expect("stop should resolve waiter");
1314 let (_outcome, stale_observation) = handle
1315 .try_wait_with_cleanup_observation()
1316 .await
1317 .expect("waiter should resolve with generated cleanup observation");
1318
1319 adapter.unregister_session(&session_id).await;
1320 adapter
1321 .prepare_bindings(session_id.clone())
1322 .await
1323 .expect("session should prepare replacement runtime bindings");
1324
1325 let err = adapter
1326 .resolve_runtime_completion_cleanup(
1327 &session_id,
1328 stale_observation,
1329 false,
1330 crate::meerkat_machine::dsl::RuntimeCompletionLiveSessionObservation::Absent,
1331 )
1332 .await
1333 .expect_err("cleanup must reject an observation minted for a prior runtime binding");
1334 assert!(
1335 matches!(err, crate::RuntimeDriverError::ValidationFailed { .. }),
1336 "expected generated cleanup validation failure, got {err:?}"
1337 );
1338 }
1339
1340 #[tokio::test]
1341 async fn wait_failure_authority_releases_pre_admission_and_classifies_public_reason() {
1342 let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1343 let session_id = SessionId::new();
1344 adapter
1345 .prepare_bindings(session_id.clone())
1346 .await
1347 .expect("session should prepare runtime bindings");
1348
1349 let authority = adapter
1350 .resolve_runtime_completion_wait_failure(
1351 &session_id,
1352 &CompletionWaitError::AuthorityUnavailable("missing generated result".into()),
1353 )
1354 .await
1355 .expect("wait-failure authority should resolve");
1356
1357 assert!(authority.releases_pre_admission());
1358 assert_eq!(
1359 authority.public_error_class,
1360 crate::meerkat_machine::dsl::RuntimeCompletionWaitFailurePublicErrorClass::InternalError
1361 );
1362 assert_eq!(
1363 authority.public_reason,
1364 crate::meerkat_machine::dsl::RuntimeCompletionWaitFailurePublicReason::CompletionAuthorityUnavailable
1365 );
1366 assert!(!authority.resumable);
1367 }
1368
1369 #[tokio::test]
1370 async fn wait_failure_authority_rejects_missing_session_authority() {
1371 let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1372 let session_id = SessionId::new();
1373
1374 let err = adapter
1375 .resolve_runtime_completion_wait_failure(
1376 &session_id,
1377 &CompletionWaitError::ChannelClosed,
1378 )
1379 .await
1380 .expect_err("wait-failure authority must fail closed without a session authority");
1381
1382 assert!(
1383 matches!(err, crate::RuntimeDriverError::ValidationFailed { .. }),
1384 "expected generated wait-failure validation failure, got {err:?}"
1385 );
1386 }
1387
1388 #[tokio::test]
1389 async fn resolve_nonexistent_is_a_noop() {
1390 let mut registry = CompletionRegistry::new();
1391 registry.resolve_completed_authorized(
1392 &InputId::new(),
1393 make_run_result(),
1394 authority(
1395 RuntimeCompletionResultClass::Completed,
1396 RuntimeCompletionObservedOutcome::Completed,
1397 ),
1398 );
1399 registry.fail_inputs(
1400 [InputId::new()],
1401 CompletionWaitError::AuthorityUnavailable("gone".into()),
1402 );
1403 assert!(!registry.debug_has_waiters());
1404 }
1405
1406 #[tokio::test]
1407 async fn dropped_sender_gives_wait_error() {
1408 let mut registry = CompletionRegistry::new();
1409 let input_id = InputId::new();
1410 let handle = registry.register(input_id);
1411
1412 drop(registry);
1414
1415 assert!(matches!(
1416 handle.try_wait().await,
1417 Err(CompletionWaitError::ChannelClosed)
1418 ));
1419 }
1420
1421 #[tokio::test]
1422 async fn multi_waiter_all_receive_result() {
1423 let mut registry = CompletionRegistry::new();
1424 let input_id = InputId::new();
1425
1426 let h1 = registry.register(input_id.clone());
1427 let h2 = registry.register(input_id.clone());
1428 let h3 = registry.register(input_id.clone());
1429
1430 assert_eq!(registry.debug_waiter_count(), 3);
1431
1432 let result = make_run_result();
1433 registry.resolve_completed_authorized(
1434 &input_id,
1435 result,
1436 authority(
1437 RuntimeCompletionResultClass::Completed,
1438 RuntimeCompletionObservedOutcome::Completed,
1439 ),
1440 );
1441
1442 assert!(!registry.debug_has_waiters());
1443
1444 for handle in [h1, h2, h3] {
1445 match handle.wait_authorized().await {
1446 CompletionOutcome::Completed(r) => assert_eq!(r.text, "hello"),
1447 other => panic!("Expected Completed, got {other:?}"),
1448 }
1449 }
1450 }
1451
1452 #[tokio::test]
1453 async fn resolve_without_result_sends_variant() {
1454 let mut registry = CompletionRegistry::new();
1455 let input_id = InputId::new();
1456 let handle = registry.register(input_id.clone());
1457
1458 registry.resolve_without_result_authorized(
1459 &input_id,
1460 authority(
1461 RuntimeCompletionResultClass::CompletedWithoutResult,
1462 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1463 ),
1464 );
1465
1466 match handle.wait_authorized().await {
1467 CompletionOutcome::CompletedWithoutResult => {}
1468 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1469 }
1470 }
1471
1472 #[tokio::test]
1473 async fn resolve_without_result_multi_waiter() {
1474 let mut registry = CompletionRegistry::new();
1475 let input_id = InputId::new();
1476 let h1 = registry.register(input_id.clone());
1477 let h2 = registry.register(input_id.clone());
1478
1479 registry.resolve_without_result_authorized(
1480 &input_id,
1481 authority(
1482 RuntimeCompletionResultClass::CompletedWithoutResult,
1483 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1484 ),
1485 );
1486
1487 for handle in [h1, h2] {
1488 match handle.wait_authorized().await {
1489 CompletionOutcome::CompletedWithoutResult => {}
1490 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1491 }
1492 }
1493 }
1494
1495 #[tokio::test]
1496 async fn resolve_callback_pending_sends_variant() {
1497 let mut registry = CompletionRegistry::new();
1498 let input_id = InputId::new();
1499 let handle = registry.register(input_id.clone());
1500
1501 registry.resolve_callback_pending_authorized(
1502 &input_id,
1503 "browser".to_string(),
1504 serde_json::json!({ "url": "https://example.com" }),
1505 authority(
1506 RuntimeCompletionResultClass::CallbackPending,
1507 RuntimeCompletionObservedOutcome::CallbackPending,
1508 ),
1509 );
1510
1511 match handle.wait_authorized().await {
1512 CompletionOutcome::CallbackPending { tool_name, args } => {
1513 assert_eq!(tool_name, "browser");
1514 assert_eq!(args, serde_json::json!({ "url": "https://example.com" }));
1515 }
1516 other => panic!("Expected CallbackPending, got {other:?}"),
1517 }
1518 }
1519
1520 #[tokio::test]
1521 async fn resolve_cancelled_sends_variant() {
1522 let mut registry = CompletionRegistry::new();
1523 let input_id = InputId::new();
1524 let handle = registry.register(input_id.clone());
1525
1526 registry.resolve_cancelled_authorized(
1527 &input_id,
1528 authority(
1529 RuntimeCompletionResultClass::Cancelled,
1530 RuntimeCompletionObservedOutcome::Cancelled,
1531 ),
1532 );
1533
1534 match handle.wait_authorized().await {
1535 CompletionOutcome::Cancelled => {}
1536 other => panic!("Expected Cancelled, got {other:?}"),
1537 }
1538 }
1539
1540 #[tokio::test]
1541 async fn already_resolved_handle() {
1542 let handle = CompletionHandle::already_completed_without_result()
1543 .expect("generated completion authority should classify no-result completion");
1544 match handle.wait_authorized().await {
1545 CompletionOutcome::CompletedWithoutResult => {}
1546 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1547 }
1548 }
1549
1550 #[tokio::test]
1551 async fn outcome_cleanup_observes_and_relays_result() {
1552 use std::sync::Arc;
1553 use std::sync::atomic::{AtomicBool, Ordering};
1554
1555 let mut registry = CompletionRegistry::new();
1556 let input_id = InputId::new();
1557 let handle = registry.register(input_id.clone());
1558 let observed = Arc::new(AtomicBool::new(false));
1559 let cleanup_observed = Arc::clone(&observed);
1560 let handle = handle.with_outcome_cleanup(move |observation| async move {
1561 if observation.observed_outcome()
1562 == crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome::CompletedWithoutResult
1563 {
1564 cleanup_observed.store(true, Ordering::Release);
1565 }
1566 });
1567
1568 registry.resolve_without_result_authorized(
1569 &input_id,
1570 authority(
1571 RuntimeCompletionResultClass::CompletedWithoutResult,
1572 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1573 ),
1574 );
1575 match handle.wait_authorized().await {
1576 CompletionOutcome::CompletedWithoutResult => {}
1577 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1578 }
1579 assert!(observed.load(Ordering::Acquire));
1580 }
1581
1582 #[tokio::test]
1583 async fn multi_waiter_terminated_on_reset() {
1584 let mut registry = CompletionRegistry::new();
1585 let input_id = InputId::new();
1586 let h1 = registry.register(input_id.clone());
1587 let h2 = registry.register(input_id);
1588
1589 registry.resolve_all_runtime_terminated(
1590 "runtime reset",
1591 authority(
1592 RuntimeCompletionResultClass::RuntimeTerminated,
1593 RuntimeCompletionObservedOutcome::RuntimeTerminated,
1594 ),
1595 );
1596
1597 for handle in [h1, h2] {
1598 match handle.wait_authorized().await {
1599 CompletionOutcome::RuntimeTerminated { reason, .. } => {
1600 assert_eq!(reason, "runtime reset");
1601 }
1602 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1603 }
1604 }
1605 }
1606
1607 #[tokio::test]
1608 async fn resolve_not_pending_keeps_pending_waiters() {
1609 let mut registry = CompletionRegistry::new();
1610 let keep_id = InputId::new();
1611 let drop_id = InputId::new();
1612
1613 let keep_handle = registry.register(keep_id.clone());
1614 let drop_handle = registry.register(drop_id.clone());
1615 registry.resolve_not_pending_runtime_terminated(
1616 |input_id| input_id == &keep_id,
1617 "runtime recycled",
1618 authority(
1619 RuntimeCompletionResultClass::RuntimeTerminated,
1620 RuntimeCompletionObservedOutcome::RuntimeTerminated,
1621 ),
1622 );
1623 assert_eq!(registry.debug_waiter_count(), 1);
1624
1625 match drop_handle.wait_authorized().await {
1626 CompletionOutcome::RuntimeTerminated { reason, .. } => {
1627 assert_eq!(reason, "runtime recycled");
1628 }
1629 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1630 }
1631
1632 registry.resolve_without_result_authorized(
1633 &keep_id,
1634 authority(
1635 RuntimeCompletionResultClass::CompletedWithoutResult,
1636 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1637 ),
1638 );
1639 match keep_handle.wait_authorized().await {
1640 CompletionOutcome::CompletedWithoutResult => {}
1641 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1642 }
1643 }
1644
1645 #[tokio::test]
1646 async fn mismatched_not_pending_runtime_terminated_authority_fails_selected_waiters_closed() {
1647 let mut registry = CompletionRegistry::new();
1648 let keep_id = InputId::new();
1649 let drop_id = InputId::new();
1650
1651 let keep_handle = registry.register(keep_id.clone());
1652 let drop_handle = registry.register(drop_id);
1653 registry.resolve_not_pending_runtime_terminated(
1654 |input_id| input_id == &keep_id,
1655 "runtime recycled",
1656 authority(
1657 RuntimeCompletionResultClass::Completed,
1658 RuntimeCompletionObservedOutcome::Completed,
1659 ),
1660 );
1661
1662 assert_eq!(registry.debug_waiter_count(), 1);
1663 match drop_handle.try_wait().await {
1664 Err(CompletionWaitError::AuthorityUnavailable(reason)) => {
1665 assert!(reason.contains("Completed"));
1666 assert!(reason.contains("RuntimeTerminated"));
1667 }
1668 other => panic!("Expected authority mismatch wait error, got {other:?}"),
1669 }
1670
1671 registry.resolve_without_result_authorized(
1672 &keep_id,
1673 authority(
1674 RuntimeCompletionResultClass::CompletedWithoutResult,
1675 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1676 ),
1677 );
1678 match keep_handle.wait_authorized().await {
1679 CompletionOutcome::CompletedWithoutResult => {}
1680 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1681 }
1682 }
1683
1684 #[tokio::test]
1685 async fn resolve_without_result_nonexistent_is_a_noop() {
1686 let mut registry = CompletionRegistry::new();
1687 registry.resolve_without_result_authorized(
1688 &InputId::new(),
1689 authority(
1690 RuntimeCompletionResultClass::CompletedWithoutResult,
1691 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1692 ),
1693 );
1694 assert!(!registry.debug_has_waiters());
1695 }
1696
1697 #[test]
1698 fn abandoned_carries_typed_error_metadata() {
1699 let error = TurnErrorMetadata::runtime_apply_failure("apply blew up");
1700 let outcome = CompletionOutcome::Abandoned {
1701 reason: "abandoned".into(),
1702 error: error.clone(),
1703 };
1704 assert_eq!(outcome.abandoned_reason(), Some("abandoned"));
1705 assert_eq!(outcome.error_metadata(), Some(&error));
1706 }
1707
1708 #[test]
1709 fn runtime_terminated_carries_typed_error_metadata() {
1710 let outcome = CompletionOutcome::runtime_terminated("runtime stopped");
1711 match &outcome {
1712 CompletionOutcome::RuntimeTerminated { reason, .. } => {
1713 assert_eq!(reason, "runtime stopped");
1714 }
1715 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1716 }
1717 let metadata = outcome
1718 .error_metadata()
1719 .expect("RuntimeTerminated must carry typed turn error metadata");
1720 assert_eq!(metadata.kind, TurnTerminalCauseKind::FatalFailure);
1721 assert_eq!(metadata.outcome, Some(TurnTerminalOutcome::Failed));
1722 assert!(metadata.terminal);
1723 assert_eq!(metadata.detail.as_deref(), Some("runtime stopped"));
1724 }
1725}