1use std::collections::BTreeSet;
4use std::sync::Arc;
5
6use meerkat_core::handles::{
7 DslTransitionError, ExternalToolSurfaceEffect, ExternalToolSurfaceHandle,
8 ExternalToolSurfaceInput, ExternalToolSurfaceTransition, SurfaceDiagnosticSnapshot,
9 SurfaceSnapshot,
10};
11use meerkat_core::tool_scope::{
12 ExternalToolSurfaceBaseState, ExternalToolSurfaceDeltaOperation, ExternalToolSurfaceDeltaPhase,
13 ExternalToolSurfaceFailureCause, ExternalToolSurfaceGlobalPhase, ExternalToolSurfacePendingOp,
14 ExternalToolSurfaceStagedOp,
15};
16
17use super::HandleDslAuthority;
18use crate::meerkat_machine::dsl as mm_dsl;
19
20#[derive(Debug)]
22pub struct RuntimeExternalToolSurfaceHandle {
23 dsl: Arc<HandleDslAuthority>,
24}
25
26impl RuntimeExternalToolSurfaceHandle {
27 pub fn new(dsl: Arc<HandleDslAuthority>) -> Self {
29 Self { dsl }
30 }
31
32 pub fn ephemeral() -> Self {
34 let state = mm_dsl::MeerkatMachineState {
35 lifecycle_phase: mm_dsl::MeerkatPhase::Attached,
36 ..Default::default()
37 };
38 let shared = Arc::new(std::sync::Mutex::new(
39 mm_dsl::MeerkatMachineAuthority::from_state(state),
40 ));
41 Self::new(Arc::new(HandleDslAuthority::from_shared(shared)))
42 }
43}
44
45impl RuntimeExternalToolSurfaceHandle {
46 fn snapshot_entry(
47 state: &mm_dsl::MeerkatMachineState,
48 surface_id: &str,
49 ) -> Option<SurfaceSnapshot> {
50 let key = surface_id.to_string();
51 if !state.known_surfaces.contains(&key)
52 && !state.surface_base_state.contains_key(&key)
53 && !state.surface_pending_op.contains_key(&key)
54 && !state.surface_staged_op.contains_key(&key)
55 {
56 return None;
57 }
58 Some(SurfaceSnapshot {
59 surface_id: key.clone(),
60 base_state: state
61 .surface_base_state
62 .get(&key)
63 .copied()
64 .map(ExternalToolSurfaceBaseState::from),
65 pending_op: state
66 .surface_pending_op
67 .get(&key)
68 .copied()
69 .map(map_pending_op)
70 .unwrap_or(ExternalToolSurfacePendingOp::None),
71 staged_op: state
72 .surface_staged_op
73 .get(&key)
74 .copied()
75 .map(map_staged_op)
76 .unwrap_or(ExternalToolSurfaceStagedOp::None),
77 staged_intent_sequence: state.surface_staged_intent_sequence.get(&key).copied(),
78 pending_task_sequence: state.surface_pending_task_sequence.get(&key).copied(),
79 pending_lineage_sequence: state.surface_pending_lineage_sequence.get(&key).copied(),
80 inflight_calls: state.surface_inflight_calls.get(&key).copied().unwrap_or(0),
81 last_delta_operation: state
82 .surface_last_delta_operation
83 .get(&key)
84 .copied()
85 .map(ExternalToolSurfaceDeltaOperation::from),
86 last_delta_phase: state
87 .surface_last_delta_phase
88 .get(&key)
89 .copied()
90 .map(ExternalToolSurfaceDeltaPhase::from),
91 removal_draining_since_ms: state.surface_draining_since_ms.get(&key).copied(),
92 removal_timeout_at_ms: state.surface_removal_timeout_at_ms.get(&key).copied(),
93 removal_applied_at_turn: state.surface_removal_applied_at_turn.get(&key).copied(),
94 })
95 }
96
97 fn apply_input_with_effects(
98 &self,
99 input: mm_dsl::MeerkatMachineInput,
100 context: &'static str,
101 ) -> Result<ExternalToolSurfaceTransition, DslTransitionError> {
102 let effects = self.dsl.apply_input_with_effects(input, context)?;
103 let state = self.dsl.snapshot_state();
104 Ok(ExternalToolSurfaceTransition {
105 phase: map_surface_phase(state.surface_phase),
106 effects: effects
107 .into_iter()
108 .filter_map(|effect| map_surface_effect(effect, state.snapshot_epoch))
109 .collect(),
110 })
111 }
112}
113
114impl ExternalToolSurfaceHandle for RuntimeExternalToolSurfaceHandle {
115 fn apply_surface_input(
116 &self,
117 input: ExternalToolSurfaceInput,
118 ) -> Result<ExternalToolSurfaceTransition, DslTransitionError> {
119 match input {
120 ExternalToolSurfaceInput::StageAdd { surface_id, now_ms } => self
121 .apply_input_with_effects(
122 mm_dsl::MeerkatMachineInput::SurfaceStageAdd { surface_id, now_ms },
123 "ExternalToolSurfaceHandle::stage_add",
124 ),
125 ExternalToolSurfaceInput::StageRemove { surface_id, now_ms } => self
126 .apply_input_with_effects(
127 mm_dsl::MeerkatMachineInput::SurfaceStageRemove { surface_id, now_ms },
128 "ExternalToolSurfaceHandle::stage_remove",
129 ),
130 ExternalToolSurfaceInput::StageReload { surface_id, now_ms } => self
131 .apply_input_with_effects(
132 mm_dsl::MeerkatMachineInput::SurfaceStageReload { surface_id, now_ms },
133 "ExternalToolSurfaceHandle::stage_reload",
134 ),
135 ExternalToolSurfaceInput::ApplyBoundary {
136 surface_id,
137 now_ms,
138 staged_intent_sequence,
139 applied_at_turn,
140 } => self.apply_input_with_effects(
141 mm_dsl::MeerkatMachineInput::SurfaceApplyBoundary {
142 surface_id,
143 now_ms,
144 staged_intent_sequence,
145 applied_at_turn,
146 },
147 "ExternalToolSurfaceHandle::apply_boundary",
148 ),
149 ExternalToolSurfaceInput::MarkPendingSucceeded {
150 surface_id,
151 pending_task_sequence,
152 staged_intent_sequence,
153 } => self.apply_input_with_effects(
154 mm_dsl::MeerkatMachineInput::SurfaceMarkPendingSucceeded {
155 surface_id,
156 pending_task_sequence,
157 staged_intent_sequence,
158 },
159 "ExternalToolSurfaceHandle::mark_pending_succeeded",
160 ),
161 ExternalToolSurfaceInput::MarkPendingFailed {
162 surface_id,
163 pending_task_sequence,
164 staged_intent_sequence,
165 cause,
166 } => self.apply_input_with_effects(
167 mm_dsl::MeerkatMachineInput::SurfaceMarkPendingFailed {
168 surface_id,
169 pending_task_sequence,
170 staged_intent_sequence,
171 cause: mm_dsl::ExternalToolSurfaceFailureCause::from(cause),
172 },
173 "ExternalToolSurfaceHandle::mark_pending_failed",
174 ),
175 ExternalToolSurfaceInput::CallStarted { surface_id } => self.apply_input_with_effects(
176 mm_dsl::MeerkatMachineInput::SurfaceCallStarted { surface_id },
177 "ExternalToolSurfaceHandle::call_started",
178 ),
179 ExternalToolSurfaceInput::CallFinished { surface_id } => self.apply_input_with_effects(
180 mm_dsl::MeerkatMachineInput::SurfaceCallFinished { surface_id },
181 "ExternalToolSurfaceHandle::call_finished",
182 ),
183 ExternalToolSurfaceInput::FinalizeRemovalClean { surface_id } => self
184 .apply_input_with_effects(
185 mm_dsl::MeerkatMachineInput::SurfaceFinalizeRemovalClean { surface_id },
186 "ExternalToolSurfaceHandle::finalize_removal_clean",
187 ),
188 ExternalToolSurfaceInput::FinalizeRemovalForced { surface_id } => self
189 .apply_input_with_effects(
190 mm_dsl::MeerkatMachineInput::SurfaceFinalizeRemovalForced { surface_id },
191 "ExternalToolSurfaceHandle::finalize_removal_forced",
192 ),
193 ExternalToolSurfaceInput::SnapshotAligned { epoch } => self.apply_input_with_effects(
194 mm_dsl::MeerkatMachineInput::SurfaceSnapshotAligned { epoch },
195 "ExternalToolSurfaceHandle::snapshot_aligned",
196 ),
197 ExternalToolSurfaceInput::Shutdown => self.apply_input_with_effects(
198 mm_dsl::MeerkatMachineInput::SurfaceShutdown,
199 "ExternalToolSurfaceHandle::shutdown_surface",
200 ),
201 }
202 }
203
204 fn register(&self, surface_id: String) -> Result<(), DslTransitionError> {
205 self.dsl.apply_input(
207 mm_dsl::MeerkatMachineInput::SurfaceRegister { surface_id },
208 "ExternalToolSurfaceHandle::register",
209 )
210 }
211
212 fn stage_add(&self, surface_id: String, now_ms: u64) -> Result<(), DslTransitionError> {
213 self.dsl.apply_input(
215 mm_dsl::MeerkatMachineInput::SurfaceStageAdd { surface_id, now_ms },
216 "ExternalToolSurfaceHandle::stage_add",
217 )
218 }
219
220 fn stage_remove(&self, surface_id: String, now_ms: u64) -> Result<(), DslTransitionError> {
221 self.dsl.apply_input(
223 mm_dsl::MeerkatMachineInput::SurfaceStageRemove { surface_id, now_ms },
224 "ExternalToolSurfaceHandle::stage_remove",
225 )
226 }
227
228 fn stage_reload(&self, surface_id: String, now_ms: u64) -> Result<(), DslTransitionError> {
229 self.dsl.apply_input(
231 mm_dsl::MeerkatMachineInput::SurfaceStageReload { surface_id, now_ms },
232 "ExternalToolSurfaceHandle::stage_reload",
233 )
234 }
235
236 fn apply_boundary(
237 &self,
238 surface_id: String,
239 now_ms: u64,
240 staged_intent_sequence: u64,
241 applied_at_turn: u64,
242 ) -> Result<(), DslTransitionError> {
243 self.dsl.apply_input(
245 mm_dsl::MeerkatMachineInput::SurfaceApplyBoundary {
246 surface_id,
247 now_ms,
248 staged_intent_sequence,
249 applied_at_turn,
250 },
251 "ExternalToolSurfaceHandle::apply_boundary",
252 )
253 }
254
255 fn mark_pending_succeeded(
256 &self,
257 surface_id: String,
258 pending_task_sequence: u64,
259 staged_intent_sequence: u64,
260 ) -> Result<(), DslTransitionError> {
261 self.dsl.apply_input(
263 mm_dsl::MeerkatMachineInput::SurfaceMarkPendingSucceeded {
264 surface_id,
265 pending_task_sequence,
266 staged_intent_sequence,
267 },
268 "ExternalToolSurfaceHandle::mark_pending_succeeded",
269 )
270 }
271
272 fn mark_pending_failed(
273 &self,
274 surface_id: String,
275 pending_task_sequence: u64,
276 staged_intent_sequence: u64,
277 cause: ExternalToolSurfaceFailureCause,
278 ) -> Result<(), DslTransitionError> {
279 self.dsl.apply_input(
281 mm_dsl::MeerkatMachineInput::SurfaceMarkPendingFailed {
282 surface_id,
283 pending_task_sequence,
284 staged_intent_sequence,
285 cause: mm_dsl::ExternalToolSurfaceFailureCause::from(cause),
286 },
287 "ExternalToolSurfaceHandle::mark_pending_failed",
288 )
289 }
290
291 fn call_started(&self, surface_id: String) -> Result<(), DslTransitionError> {
292 self.dsl.apply_input(
294 mm_dsl::MeerkatMachineInput::SurfaceCallStarted { surface_id },
295 "ExternalToolSurfaceHandle::call_started",
296 )
297 }
298
299 fn call_finished(&self, surface_id: String) -> Result<(), DslTransitionError> {
300 self.dsl.apply_input(
302 mm_dsl::MeerkatMachineInput::SurfaceCallFinished { surface_id },
303 "ExternalToolSurfaceHandle::call_finished",
304 )
305 }
306
307 fn finalize_removal_clean(&self, surface_id: String) -> Result<(), DslTransitionError> {
308 self.dsl.apply_input(
310 mm_dsl::MeerkatMachineInput::SurfaceFinalizeRemovalClean { surface_id },
311 "ExternalToolSurfaceHandle::finalize_removal_clean",
312 )
313 }
314
315 fn finalize_removal_forced(&self, surface_id: String) -> Result<(), DslTransitionError> {
316 self.dsl.apply_input(
318 mm_dsl::MeerkatMachineInput::SurfaceFinalizeRemovalForced { surface_id },
319 "ExternalToolSurfaceHandle::finalize_removal_forced",
320 )
321 }
322
323 fn snapshot_aligned(&self, epoch: u64) -> Result<(), DslTransitionError> {
324 self.dsl.apply_input(
326 mm_dsl::MeerkatMachineInput::SurfaceSnapshotAligned { epoch },
327 "ExternalToolSurfaceHandle::snapshot_aligned",
328 )
329 }
330
331 fn shutdown_surface(&self) -> Result<(), DslTransitionError> {
332 self.dsl.apply_input(
334 mm_dsl::MeerkatMachineInput::SurfaceShutdown,
335 "ExternalToolSurfaceHandle::shutdown_surface",
336 )
337 }
338
339 fn surface_snapshot(&self, surface_id: &str) -> Option<SurfaceSnapshot> {
340 let state = self.dsl.snapshot_state();
341 Self::snapshot_entry(&state, surface_id)
342 }
343
344 fn diagnostic_snapshot(&self) -> SurfaceDiagnosticSnapshot {
345 let state = self.dsl.snapshot_state();
346 let mut entries: Vec<SurfaceSnapshot> = state
347 .known_surfaces
348 .iter()
349 .filter_map(|surface_id| Self::snapshot_entry(&state, surface_id))
350 .collect();
351 entries.sort_by(|a, b| a.surface_id.cmp(&b.surface_id));
352 SurfaceDiagnosticSnapshot {
353 surface_phase: map_surface_phase(state.surface_phase),
354 known_surfaces: state.known_surfaces.clone(),
355 visible_surfaces: state.visible_surfaces.clone(),
356 snapshot_epoch: state.snapshot_epoch,
357 snapshot_aligned_epoch: state.snapshot_aligned_epoch,
358 has_pending_or_staged: entries.iter().any(|entry| {
359 entry.pending_op != ExternalToolSurfacePendingOp::None
360 || entry.staged_op != ExternalToolSurfaceStagedOp::None
361 }),
362 entries,
363 }
364 }
365
366 fn visible_surfaces(&self) -> BTreeSet<String> {
367 self.dsl.snapshot_state().visible_surfaces
368 }
369
370 fn removing_surfaces(&self) -> BTreeSet<String> {
371 self.dsl
372 .snapshot_state()
373 .surface_base_state
374 .into_iter()
375 .filter_map(|(surface_id, base_state)| {
376 if base_state == mm_dsl::ExternalToolSurfaceBaseState::Removing {
377 Some(surface_id)
378 } else {
379 None
380 }
381 })
382 .collect()
383 }
384
385 fn pending_surfaces(&self) -> BTreeSet<String> {
386 self.dsl
387 .snapshot_state()
388 .surface_pending_op
389 .into_iter()
390 .filter_map(|(surface_id, pending_op)| {
391 if pending_op == mm_dsl::SurfacePendingOp::None {
392 None
393 } else {
394 Some(surface_id)
395 }
396 })
397 .collect()
398 }
399
400 fn has_pending_or_staged(&self) -> bool {
401 let state = self.dsl.snapshot_state();
402 state
403 .surface_pending_op
404 .values()
405 .any(|pending_op| *pending_op != mm_dsl::SurfacePendingOp::None)
406 || state
407 .surface_staged_op
408 .values()
409 .any(|staged_op| *staged_op != mm_dsl::SurfaceStagedOp::None)
410 }
411
412 fn snapshot_epoch(&self) -> u64 {
413 self.dsl.snapshot_state().snapshot_epoch
414 }
415
416 fn snapshot_aligned_epoch(&self) -> u64 {
417 self.dsl.snapshot_state().snapshot_aligned_epoch
418 }
419}
420
421fn map_surface_phase(phase: mm_dsl::SurfacePhase) -> ExternalToolSurfaceGlobalPhase {
424 match phase {
425 mm_dsl::SurfacePhase::Operating => ExternalToolSurfaceGlobalPhase::Operating,
426 mm_dsl::SurfacePhase::Shutdown => ExternalToolSurfaceGlobalPhase::Shutdown,
427 }
428}
429
430fn map_pending_op(op: mm_dsl::SurfacePendingOp) -> ExternalToolSurfacePendingOp {
431 match op {
432 mm_dsl::SurfacePendingOp::None => ExternalToolSurfacePendingOp::None,
433 mm_dsl::SurfacePendingOp::Add => ExternalToolSurfacePendingOp::Add,
434 mm_dsl::SurfacePendingOp::Reload => ExternalToolSurfacePendingOp::Reload,
435 }
436}
437
438fn map_staged_op(op: mm_dsl::SurfaceStagedOp) -> ExternalToolSurfaceStagedOp {
439 match op {
440 mm_dsl::SurfaceStagedOp::None => ExternalToolSurfaceStagedOp::None,
441 mm_dsl::SurfaceStagedOp::Add => ExternalToolSurfaceStagedOp::Add,
442 mm_dsl::SurfaceStagedOp::Remove => ExternalToolSurfaceStagedOp::Remove,
443 mm_dsl::SurfaceStagedOp::Reload => ExternalToolSurfaceStagedOp::Reload,
444 }
445}
446
447fn map_surface_effect(
448 effect: mm_dsl::MeerkatMachineEffect,
449 _snapshot_epoch: u64,
450) -> Option<ExternalToolSurfaceEffect> {
451 match effect {
452 mm_dsl::MeerkatMachineEffect::ScheduleSurfaceCompletion {
453 surface_id,
454 operation,
455 pending_task_sequence,
456 staged_intent_sequence,
457 applied_at_turn,
458 } => Some(ExternalToolSurfaceEffect::ScheduleSurfaceCompletion {
459 surface_id,
460 operation: ExternalToolSurfaceDeltaOperation::from(operation),
461 pending_task_sequence,
462 staged_intent_sequence,
463 applied_at_turn,
464 }),
465 mm_dsl::MeerkatMachineEffect::RefreshVisibleSurfaceSet { snapshot_epoch } => {
466 Some(ExternalToolSurfaceEffect::RefreshVisibleSurfaceSet { snapshot_epoch })
467 }
468 mm_dsl::MeerkatMachineEffect::EmitExternalToolDelta {
469 surface_id,
470 operation,
471 phase,
472 cause,
473 } => Some(ExternalToolSurfaceEffect::EmitExternalToolDelta {
474 surface_id,
475 operation: ExternalToolSurfaceDeltaOperation::from(operation),
476 phase: ExternalToolSurfaceDeltaPhase::from(phase),
477 cause: cause.map(ExternalToolSurfaceFailureCause::from),
478 }),
479 mm_dsl::MeerkatMachineEffect::CloseSurfaceConnection { surface_id } => {
480 Some(ExternalToolSurfaceEffect::CloseSurfaceConnection { surface_id })
481 }
482 mm_dsl::MeerkatMachineEffect::RejectSurfaceCall { surface_id, cause } => {
483 Some(ExternalToolSurfaceEffect::RejectSurfaceCall {
484 surface_id,
485 cause: ExternalToolSurfaceFailureCause::from(cause),
486 })
487 }
488 _ => None,
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use std::sync::{Arc, Mutex};
495
496 use super::*;
497 use meerkat_core::ExternalToolSurfaceFailureCause;
498
499 fn handle_in_phase(phase: mm_dsl::MeerkatPhase) -> RuntimeExternalToolSurfaceHandle {
500 let state = mm_dsl::MeerkatMachineState {
501 lifecycle_phase: phase,
502 ..Default::default()
503 };
504 let authority = mm_dsl::MeerkatMachineAuthority::from_state(state);
505 let shared = Arc::new(Mutex::new(authority));
506 RuntimeExternalToolSurfaceHandle::new(Arc::new(HandleDslAuthority::from_shared(shared)))
507 }
508
509 fn handle_with_active_surface(surface_id: &str) -> RuntimeExternalToolSurfaceHandle {
510 let mut state = mm_dsl::MeerkatMachineState {
511 lifecycle_phase: mm_dsl::MeerkatPhase::Attached,
512 ..Default::default()
513 };
514 state.known_surfaces.insert(surface_id.to_owned());
515 state.active_surfaces.insert(surface_id.to_owned());
516 state.surface_base_state.insert(
517 surface_id.to_owned(),
518 mm_dsl::ExternalToolSurfaceBaseState::Active,
519 );
520 let authority = mm_dsl::MeerkatMachineAuthority::from_state(state);
521 let shared = Arc::new(Mutex::new(authority));
522 RuntimeExternalToolSurfaceHandle::new(Arc::new(HandleDslAuthority::from_shared(shared)))
523 }
524
525 #[test]
526 fn staging_inputs_mint_sequences_and_project_snapshot() {
527 let handle = handle_in_phase(mm_dsl::MeerkatPhase::Attached);
528
529 handle.stage_add("alpha".to_owned(), 10).expect("stage add");
530 let add = handle.surface_snapshot("alpha").expect("add snapshot");
531 assert_eq!(add.staged_op, ExternalToolSurfaceStagedOp::Add);
532 assert_eq!(add.staged_intent_sequence, Some(1));
533 assert!(
534 handle
535 .diagnostic_snapshot()
536 .known_surfaces
537 .contains("alpha")
538 );
539
540 handle
541 .stage_remove("alpha".to_owned(), 20)
542 .expect("stage remove");
543 let remove = handle.surface_snapshot("alpha").expect("remove snapshot");
544 assert_eq!(remove.staged_op, ExternalToolSurfaceStagedOp::Remove);
545 assert_eq!(remove.staged_intent_sequence, Some(2));
546
547 handle.stage_add("beta".to_owned(), 30).expect("stage add");
548 let beta = handle.surface_snapshot("beta").expect("beta snapshot");
549 assert_eq!(beta.staged_op, ExternalToolSurfaceStagedOp::Add);
550 assert_eq!(beta.staged_intent_sequence, Some(3));
551 }
552
553 #[test]
554 fn staging_inputs_reject_after_surface_shutdown() {
555 let handle = handle_in_phase(mm_dsl::MeerkatPhase::Attached);
556
557 handle.shutdown_surface().expect("shutdown surface");
558
559 assert!(handle.stage_add("alpha".to_owned(), 10).is_err());
560 assert_eq!(
561 handle.diagnostic_snapshot().surface_phase,
562 ExternalToolSurfaceGlobalPhase::Shutdown
563 );
564 }
565
566 #[test]
567 fn stage_reload_requires_active_base_state() {
568 let handle = handle_in_phase(mm_dsl::MeerkatPhase::Attached);
569
570 assert!(handle.stage_reload("alpha".to_owned(), 10).is_err());
571 assert!(handle.surface_snapshot("alpha").is_none());
572 }
573
574 #[test]
575 fn stage_reload_mints_sequence_for_active_surface() {
576 let handle = handle_with_active_surface("alpha");
577
578 handle
579 .stage_reload("alpha".to_owned(), 10)
580 .expect("stage reload");
581
582 let snapshot = handle.surface_snapshot("alpha").expect("reload snapshot");
583 assert_eq!(
584 snapshot.base_state,
585 Some(ExternalToolSurfaceBaseState::Active)
586 );
587 assert_eq!(snapshot.staged_op, ExternalToolSurfaceStagedOp::Reload);
588 assert_eq!(snapshot.staged_intent_sequence, Some(1));
589 }
590
591 #[test]
592 fn runtime_surface_lifecycle_keeps_pending_lineage_on_staged_sequence() {
593 let handle = handle_in_phase(mm_dsl::MeerkatPhase::Attached);
594
595 assert!(handle.stage_reload("alpha".to_owned(), 10).is_err());
596
597 handle.stage_add("alpha".to_owned(), 10).expect("stage add");
598 let staged_add = handle.surface_snapshot("alpha").expect("staged add");
599 let add_lineage = staged_add
600 .staged_intent_sequence
601 .expect("staged add sequence");
602
603 assert!(
604 handle
605 .apply_boundary("alpha".to_owned(), 20, add_lineage + 1, 99)
606 .is_err(),
607 "apply boundary must reject a lineage that is not the staged intent"
608 );
609
610 handle
611 .apply_boundary("alpha".to_owned(), 20, add_lineage, 99)
612 .expect("apply add boundary");
613 let pending_add = handle.surface_snapshot("alpha").expect("pending add");
614 assert_eq!(pending_add.pending_op, ExternalToolSurfacePendingOp::Add);
615 assert_eq!(pending_add.pending_task_sequence, Some(1));
616 assert_eq!(pending_add.pending_lineage_sequence, Some(add_lineage));
617 assert_eq!(pending_add.staged_op, ExternalToolSurfaceStagedOp::None);
618
619 handle
620 .mark_pending_succeeded("alpha".to_owned(), 1, add_lineage)
621 .expect("add success");
622 let active = handle.surface_snapshot("alpha").expect("active alpha");
623 assert_eq!(
624 active.base_state,
625 Some(ExternalToolSurfaceBaseState::Active)
626 );
627 assert!(handle.visible_surfaces().contains("alpha"));
628
629 handle
630 .stage_reload("alpha".to_owned(), 30)
631 .expect("stage reload after active");
632 let staged_reload = handle.surface_snapshot("alpha").expect("staged reload");
633 let reload_lineage = staged_reload
634 .staged_intent_sequence
635 .expect("staged reload sequence");
636 handle
637 .apply_boundary("alpha".to_owned(), 40, reload_lineage, 100)
638 .expect("apply reload boundary");
639 let pending_reload = handle.surface_snapshot("alpha").expect("pending reload");
640 assert_eq!(
641 pending_reload.pending_op,
642 ExternalToolSurfacePendingOp::Reload
643 );
644 assert_eq!(
645 pending_reload.pending_lineage_sequence,
646 Some(reload_lineage)
647 );
648 handle
649 .mark_pending_succeeded("alpha".to_owned(), 2, reload_lineage)
650 .expect("reload success");
651
652 handle
653 .stage_remove("alpha".to_owned(), 50)
654 .expect("stage remove");
655 let remove_lineage = handle
656 .surface_snapshot("alpha")
657 .and_then(|entry| entry.staged_intent_sequence)
658 .expect("staged remove sequence");
659 handle
660 .apply_boundary("alpha".to_owned(), 60, remove_lineage, 101)
661 .expect("apply remove boundary");
662 let removing = handle.surface_snapshot("alpha").expect("removing alpha");
663 assert_eq!(
664 removing.base_state,
665 Some(ExternalToolSurfaceBaseState::Removing)
666 );
667 assert!(!handle.visible_surfaces().contains("alpha"));
668
669 handle
670 .finalize_removal_clean("alpha".to_owned())
671 .expect("finalize removal");
672 let removed = handle.surface_snapshot("alpha").expect("removed alpha");
673 assert_eq!(
674 removed.base_state,
675 Some(ExternalToolSurfaceBaseState::Removed)
676 );
677 assert!(!handle.visible_surfaces().contains("alpha"));
678 }
679
680 #[test]
681 fn runtime_mark_pending_failed_accepts_typed_failure_cause() {
682 let handle = handle_in_phase(mm_dsl::MeerkatPhase::Attached);
683
684 handle.stage_add("alpha".to_owned(), 10).expect("stage add");
685 let staged_sequence = handle
686 .surface_snapshot("alpha")
687 .and_then(|entry| entry.staged_intent_sequence)
688 .expect("staged add sequence");
689 handle
690 .apply_boundary("alpha".to_owned(), 20, staged_sequence, 99)
691 .expect("apply add boundary");
692
693 let transition = handle
694 .apply_surface_input(ExternalToolSurfaceInput::MarkPendingFailed {
695 surface_id: "alpha".to_owned(),
696 pending_task_sequence: 1,
697 staged_intent_sequence: staged_sequence,
698 cause: ExternalToolSurfaceFailureCause::PendingFailed,
699 })
700 .expect("typed pending failure");
701 assert!(transition.effects.iter().any(|effect| matches!(
702 effect,
703 ExternalToolSurfaceEffect::EmitExternalToolDelta {
704 phase: ExternalToolSurfaceDeltaPhase::Failed,
705 cause: Some(ExternalToolSurfaceFailureCause::PendingFailed),
706 ..
707 }
708 )));
709
710 let failed = handle.surface_snapshot("alpha").expect("failed alpha");
711 assert_eq!(
712 failed.last_delta_phase,
713 Some(ExternalToolSurfaceDeltaPhase::Failed)
714 );
715 assert_eq!(failed.pending_op, ExternalToolSurfacePendingOp::None);
716 }
717}