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