Skip to main content

runtara_core/
instance_handlers.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Instance protocol handlers for runtara-core.
4//!
5//! These handlers process requests from instances (registration, checkpoints, events, signals, etc.)
6
7use std::{sync::Arc, time::Duration};
8
9use anyhow::Result;
10use chrono::{DateTime, Utc};
11use tracing::{debug, info, instrument, warn};
12
13// ============================================================================
14// Plain Rust types (replacing protobuf/prost types from runtara-protocol)
15// ============================================================================
16
17/// Signal type for instance-wide signals.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum SignalType {
20    /// Cancel execution.
21    SignalCancel = 0,
22    /// Pause execution (checkpoint and wait).
23    SignalPause = 1,
24    /// Resume paused execution.
25    SignalResume = 2,
26}
27
28impl From<SignalType> for i32 {
29    fn from(s: SignalType) -> i32 {
30        s as i32
31    }
32}
33
34/// Instance event type.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum InstanceEventType {
37    /// Simple "I'm alive" signal.
38    EventHeartbeat = 0,
39    /// Instance finished successfully, payload = output.
40    EventCompleted = 2,
41    /// Instance failed, payload = error details.
42    EventFailed = 3,
43    /// Instance suspended (waiting for wake).
44    EventSuspended = 4,
45    /// Generic custom event with arbitrary subtype.
46    EventCustom = 5,
47}
48
49impl From<InstanceEventType> for i32 {
50    fn from(e: InstanceEventType) -> i32 {
51        e as i32
52    }
53}
54
55/// Instance status.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum InstanceStatus {
58    /// Unknown status.
59    StatusUnknown = 0,
60    /// Queued, not yet started.
61    StatusPending = 1,
62    /// Currently executing.
63    StatusRunning = 2,
64    /// Sleeping / waiting for wake.
65    StatusSuspended = 3,
66    /// Finished successfully.
67    StatusCompleted = 4,
68    /// Finished with error.
69    StatusFailed = 5,
70    /// Cancelled by signal.
71    StatusCancelled = 6,
72}
73
74impl From<InstanceStatus> for i32 {
75    fn from(s: InstanceStatus) -> i32 {
76        s as i32
77    }
78}
79
80impl InstanceStatus {
81    /// Try to convert from an i32 value.
82    pub fn try_from_i32(value: i32) -> Option<Self> {
83        match value {
84            0 => Some(Self::StatusUnknown),
85            1 => Some(Self::StatusPending),
86            2 => Some(Self::StatusRunning),
87            3 => Some(Self::StatusSuspended),
88            4 => Some(Self::StatusCompleted),
89            5 => Some(Self::StatusFailed),
90            6 => Some(Self::StatusCancelled),
91            _ => None,
92        }
93    }
94}
95
96/// Register instance request.
97pub struct RegisterInstanceRequest {
98    /// Instance identifier.
99    pub instance_id: String,
100    /// Tenant identifier.
101    pub tenant_id: String,
102    /// Optional checkpoint ID to resume from.
103    pub checkpoint_id: Option<String>,
104}
105
106/// Register instance response.
107pub struct RegisterInstanceResponse {
108    /// Whether registration succeeded.
109    pub success: bool,
110    /// Error message if registration failed.
111    pub error: String,
112}
113
114/// Checkpoint request.
115pub struct CheckpointRequest {
116    /// Instance identifier.
117    pub instance_id: String,
118    /// Checkpoint identifier (unique per durable function call).
119    pub checkpoint_id: String,
120    /// Serialized workflow state.
121    pub state: Vec<u8>,
122}
123
124/// Signal forwarded from core to instance.
125#[derive(Debug, Clone)]
126pub struct Signal {
127    /// Instance identifier.
128    pub instance_id: String,
129    /// Signal type as integer (see `SignalType` enum values).
130    pub signal_type: i32,
131    /// Signal payload bytes.
132    pub payload: Vec<u8>,
133}
134
135/// Custom signal targeted at a specific checkpoint_id.
136#[derive(Debug, Clone)]
137pub struct CustomSignal {
138    /// Checkpoint ID this signal targets.
139    pub checkpoint_id: String,
140    /// Signal payload bytes.
141    pub payload: Vec<u8>,
142}
143
144/// Error info returned with checkpoint responses.
145#[derive(Debug, Clone)]
146pub struct CheckpointErrorInfo {
147    /// Machine-readable error code.
148    pub code: String,
149    /// Human-readable error message.
150    pub message: String,
151}
152
153/// Checkpoint response.
154pub struct CheckpointResponse {
155    /// True if checkpoint already existed (resume case).
156    pub found: bool,
157    /// Existing checkpoint state if found, empty if new.
158    pub state: Vec<u8>,
159    /// Pending instance-wide signal (cancel/pause/resume).
160    pub pending_signal: Option<Signal>,
161    /// Pending checkpoint-scoped custom signal.
162    pub custom_signal: Option<CustomSignal>,
163    /// Last error from a previous checkpoint attempt.
164    pub last_error: Option<CheckpointErrorInfo>,
165}
166
167/// Get checkpoint request (read-only lookup).
168pub struct GetCheckpointRequest {
169    /// Instance identifier.
170    pub instance_id: String,
171    /// Checkpoint ID to look up.
172    pub checkpoint_id: String,
173}
174
175/// Get checkpoint response.
176pub struct GetCheckpointResponse {
177    /// True if checkpoint exists.
178    pub found: bool,
179    /// Checkpoint state if found.
180    pub state: Vec<u8>,
181}
182
183/// Sleep request.
184pub struct SleepRequest {
185    /// Instance identifier.
186    pub instance_id: String,
187    /// Sleep duration in milliseconds.
188    pub duration_ms: u64,
189    /// Checkpoint ID for resume after wake.
190    pub checkpoint_id: String,
191    /// State to restore on wake.
192    pub state: Vec<u8>,
193}
194
195/// Sleep response (empty - sleep completes in-process).
196pub struct SleepResponse {}
197
198/// Instance event.
199pub struct InstanceEvent {
200    /// Instance identifier.
201    pub instance_id: String,
202    /// Event type as integer (see `InstanceEventType` enum values).
203    pub event_type: i32,
204    /// Current checkpoint position.
205    pub checkpoint_id: Option<String>,
206    /// Event-specific data (output, error, etc.).
207    pub payload: Vec<u8>,
208    /// Event timestamp in milliseconds since epoch.
209    pub timestamp_ms: i64,
210    /// Arbitrary subtype for custom events.
211    pub subtype: Option<String>,
212}
213
214impl InstanceEvent {
215    /// Get the event type as an enum.
216    pub fn event_type(&self) -> InstanceEventType {
217        match self.event_type {
218            0 => InstanceEventType::EventHeartbeat,
219            2 => InstanceEventType::EventCompleted,
220            3 => InstanceEventType::EventFailed,
221            4 => InstanceEventType::EventSuspended,
222            5 => InstanceEventType::EventCustom,
223            _ => InstanceEventType::EventCustom,
224        }
225    }
226}
227
228/// Instance event response.
229pub struct InstanceEventResponse {
230    /// Whether the event was persisted successfully.
231    pub success: bool,
232    /// Error message if persistence failed.
233    pub error: Option<String>,
234}
235
236/// Get instance status request.
237pub struct GetInstanceStatusRequest {
238    /// Instance identifier.
239    pub instance_id: String,
240}
241
242/// Get instance status response.
243pub struct GetInstanceStatusResponse {
244    /// Instance identifier.
245    pub instance_id: String,
246    /// Instance status as integer (see `InstanceStatus` enum values).
247    pub status: i32,
248    /// Last known checkpoint ID.
249    pub checkpoint_id: Option<String>,
250    /// Instance start timestamp in milliseconds since epoch.
251    pub started_at_ms: i64,
252    /// Instance finish timestamp in milliseconds since epoch.
253    pub finished_at_ms: Option<i64>,
254    /// Output data if completed.
255    pub output: Option<Vec<u8>>,
256    /// Error message if failed.
257    pub error: Option<String>,
258}
259
260/// Poll signals request.
261pub struct PollSignalsRequest {
262    /// Instance identifier.
263    pub instance_id: String,
264    /// Optional checkpoint ID for custom signal polling.
265    pub checkpoint_id: Option<String>,
266}
267
268/// Poll signals response.
269pub struct PollSignalsResponse {
270    /// Pending instance-wide signal if any.
271    pub signal: Option<Signal>,
272    /// Pending checkpoint-scoped custom signal if any.
273    pub custom_signal: Option<CustomSignal>,
274}
275
276/// Signal acknowledgement.
277pub struct SignalAck {
278    /// Instance identifier.
279    pub instance_id: String,
280    /// Signal type as integer (see `SignalType` enum values).
281    pub signal_type: i32,
282    /// Whether the signal was acknowledged.
283    pub acknowledged: bool,
284}
285
286impl SignalAck {
287    /// Get the signal type as an enum.
288    pub fn signal_type(&self) -> SignalType {
289        match self.signal_type {
290            0 => SignalType::SignalCancel,
291            1 => SignalType::SignalPause,
292            2 => SignalType::SignalResume,
293            _ => SignalType::SignalCancel,
294        }
295    }
296}
297
298/// Error metadata for retry decisions.
299pub struct ErrorMetadata {
300    /// Error category (0=unknown, 1=transient, 2=permanent, 3=business).
301    pub category: i32,
302    /// Error severity (0=info, 1=warning, 2=error, 3=critical).
303    pub severity: i32,
304    /// Retry hint (0=unknown, 1=immediately, 2=backoff, 3=after, 4=do_not_retry).
305    pub retry_hint: i32,
306    /// Retry delay in milliseconds for `retry_after` hint.
307    pub retry_after_ms: Option<u64>,
308    /// Machine-readable error code.
309    pub error_code: Option<String>,
310}
311
312impl ErrorMetadata {
313    /// Get the error category name.
314    pub fn category(&self) -> &'static str {
315        match self.category {
316            1 => "transient",
317            2 => "permanent",
318            3 => "business",
319            _ => "unknown",
320        }
321    }
322
323    /// Get the error severity name.
324    pub fn severity(&self) -> &'static str {
325        match self.severity {
326            0 => "info",
327            1 => "warning",
328            2 => "error",
329            3 => "critical",
330            _ => "unknown",
331        }
332    }
333
334    /// Get the retry hint name.
335    pub fn retry_hint(&self) -> &'static str {
336        match self.retry_hint {
337            1 => "retry_immediately",
338            2 => "retry_with_backoff",
339            3 => "retry_after",
340            4 => "do_not_retry",
341            _ => "unknown",
342        }
343    }
344}
345
346/// Retry attempt event.
347pub struct RetryAttemptEvent {
348    /// Instance identifier.
349    pub instance_id: String,
350    /// Durable function cache key.
351    pub checkpoint_id: String,
352    /// 1-indexed retry attempt number.
353    pub attempt_number: u32,
354    /// Timestamp in milliseconds since epoch.
355    pub timestamp_ms: i64,
356    /// Error from previous attempt.
357    pub error_message: Option<String>,
358    /// Structured error metadata for retry decisions.
359    pub error_metadata: Option<ErrorMetadata>,
360}
361
362use crate::error::CoreError;
363use crate::persistence::{EventRecord, Persistence};
364
365/// Shared state for instance handlers.
366///
367/// Contains the persistence implementation shared across all handlers.
368pub struct InstanceHandlerState {
369    /// Persistence implementation.
370    pub persistence: Arc<dyn Persistence>,
371}
372
373impl InstanceHandlerState {
374    /// Create a new instance handler state with the given persistence backend.
375    pub fn new(persistence: Arc<dyn Persistence>) -> Self {
376        Self { persistence }
377    }
378}
379
380// ============================================================================
381// Instance Registration
382// ============================================================================
383
384/// Handle instance registration request.
385///
386/// Registers an instance with Core, optionally resuming from a checkpoint.
387/// If the instance doesn't exist, it's created (self-registration).
388///
389/// # Errors
390///
391/// Returns an error response if:
392/// - `instance_id` or `tenant_id` is empty
393/// - A specified `checkpoint_id` doesn't exist
394#[instrument(skip(state, request), fields(instance_id = %request.instance_id))]
395pub async fn handle_register_instance(
396    state: &InstanceHandlerState,
397    request: RegisterInstanceRequest,
398) -> Result<RegisterInstanceResponse> {
399    info!(
400        tenant_id = %request.tenant_id,
401        resuming_from = ?request.checkpoint_id,
402        "Instance registering"
403    );
404
405    // 1. Validate instance_id is not empty
406    if request.instance_id.is_empty() {
407        return Ok(RegisterInstanceResponse {
408            success: false,
409            error: "instance_id is required".to_string(),
410        });
411    }
412
413    // 2. Validate tenant_id is not empty
414    if request.tenant_id.is_empty() {
415        return Ok(RegisterInstanceResponse {
416            success: false,
417            error: "tenant_id is required".to_string(),
418        });
419    }
420
421    // 3. If checkpoint_id provided, verify it exists
422    if let Some(ref cp_id) = request.checkpoint_id {
423        let checkpoint = state
424            .persistence
425            .load_checkpoint(&request.instance_id, cp_id)
426            .await;
427        match checkpoint {
428            Ok(Some(_)) => {
429                debug!(checkpoint_id = %cp_id, "Checkpoint found for resume");
430            }
431            Ok(None) => {
432                return Ok(RegisterInstanceResponse {
433                    success: false,
434                    error: format!("Checkpoint '{}' not found", cp_id),
435                });
436            }
437            Err(e) => {
438                return Ok(RegisterInstanceResponse {
439                    success: false,
440                    error: format!("Failed to verify checkpoint: {}", e),
441                });
442            }
443        }
444    }
445
446    // 4. Check if instance exists, create if not (self-registration)
447    let instance_exists = state
448        .persistence
449        .get_instance(&request.instance_id)
450        .await
451        .map(|opt| opt.is_some())
452        .unwrap_or(false);
453
454    if !instance_exists {
455        // Self-registration: create instance record
456        info!("Instance not found, creating self-registered instance");
457        if let Err(e) = state
458            .persistence
459            .register_instance(&request.instance_id, &request.tenant_id)
460            .await
461        {
462            return Ok(RegisterInstanceResponse {
463                success: false,
464                error: format!("Failed to create instance: {}", e),
465            });
466        }
467    }
468
469    // 5. Update instance status to RUNNING
470    let started_at = Utc::now();
471    if let Err(e) = state
472        .persistence
473        .update_instance_status(&request.instance_id, "running", Some(started_at))
474        .await
475    {
476        return Ok(RegisterInstanceResponse {
477            success: false,
478            error: format!("Failed to update instance status: {}", e),
479        });
480    }
481
482    // 6. Insert started event
483    let event = EventRecord {
484        id: None,
485        instance_id: request.instance_id.clone(),
486        event_type: "started".to_string(),
487        checkpoint_id: request.checkpoint_id.clone(),
488        payload: None,
489        created_at: started_at,
490        subtype: None,
491    };
492    if let Err(e) = state.persistence.insert_event(&event).await {
493        warn!("Failed to insert started event: {}", e);
494        // Don't fail registration just because event logging failed
495    }
496
497    info!("Instance registered successfully");
498
499    Ok(RegisterInstanceResponse {
500        success: true,
501        error: String::new(),
502    })
503}
504
505// ============================================================================
506// Checkpointing (append-only log)
507// ============================================================================
508
509/// Checkpoint handler - combines save and load semantics.
510///
511/// - If checkpoint with this ID exists, returns the existing state (for resume)
512/// - If checkpoint doesn't exist, saves the state and returns empty (fresh execution)
513///
514/// Also serves as heartbeat - updates instance's last activity timestamp.
515/// Includes pending signal information so instance can react to cancel/pause.
516#[instrument(skip(state, request), fields(instance_id = %request.instance_id, checkpoint_id = %request.checkpoint_id))]
517pub async fn handle_checkpoint(
518    state: &InstanceHandlerState,
519    request: CheckpointRequest,
520) -> Result<CheckpointResponse> {
521    debug!(
522        state_size = request.state.len(),
523        "Processing checkpoint request"
524    );
525
526    // 1. Validate instance exists and is running
527    let instance = state.persistence.get_instance(&request.instance_id).await?;
528    match instance {
529        Some(inst) => {
530            if inst.status != "running" {
531                return Err(CoreError::InvalidInstanceState {
532                    instance_id: request.instance_id.clone(),
533                    expected: "running".to_string(),
534                    actual: inst.status,
535                }
536                .into());
537            }
538        }
539        None => {
540            return Err(CoreError::InstanceNotFound {
541                instance_id: request.instance_id.clone(),
542            }
543            .into());
544        }
545    }
546
547    // 2. Check if checkpoint already exists
548    if let Some(existing) = state
549        .persistence
550        .load_checkpoint(&request.instance_id, &request.checkpoint_id)
551        .await?
552    {
553        debug!(
554            checkpoint_id = %request.checkpoint_id,
555            state_size = existing.state.len(),
556            "Found existing checkpoint - returning for resume"
557        );
558
559        // Check for pending signal even when returning existing checkpoint
560        let pending_signal =
561            get_pending_signal(state.persistence.as_ref(), &request.instance_id).await;
562        let custom_signal = state
563            .persistence
564            .take_pending_custom_signal(&request.instance_id, &request.checkpoint_id)
565            .await?
566            .map(|sig| CustomSignal {
567                checkpoint_id: request.checkpoint_id.clone(),
568                payload: sig.payload.unwrap_or_default(),
569            });
570
571        return Ok(CheckpointResponse {
572            found: true,
573            state: existing.state,
574            pending_signal,
575            custom_signal,
576            last_error: None, // TODO: Fetch last error from error_history when available
577        });
578    }
579
580    // 3. Checkpoint doesn't exist
581    // Only save if state is non-empty. The SDK's get_checkpoint() calls this
582    // endpoint with empty state as a read-only probe. If we saved empty state,
583    // subsequent save attempts with real state would find the empty checkpoint
584    // and return it instead of overwriting — corrupting the checkpoint permanently.
585    if request.state.is_empty() {
586        return Ok(CheckpointResponse {
587            found: false,
588            state: vec![],
589            pending_signal: get_pending_signal(state.persistence.as_ref(), &request.instance_id)
590                .await,
591            custom_signal: None,
592            last_error: None,
593        });
594    }
595
596    state
597        .persistence
598        .save_checkpoint(&request.instance_id, &request.checkpoint_id, &request.state)
599        .await?;
600
601    // 4. Update instance's current checkpoint_id
602    state
603        .persistence
604        .update_instance_checkpoint(&request.instance_id, &request.checkpoint_id)
605        .await?;
606
607    // 5. Check for pending signals to include in response
608    let pending_signal = get_pending_signal(state.persistence.as_ref(), &request.instance_id).await;
609    let custom_signal = state
610        .persistence
611        .take_pending_custom_signal(&request.instance_id, &request.checkpoint_id)
612        .await?
613        .map(|sig| CustomSignal {
614            checkpoint_id: request.checkpoint_id.clone(),
615            payload: sig.payload.unwrap_or_default(),
616        });
617
618    if pending_signal.is_some() || custom_signal.is_some() {
619        debug!(
620            ?pending_signal,
621            has_custom = custom_signal.is_some(),
622            "Checkpoint saved with pending signal"
623        );
624    } else {
625        debug!("New checkpoint saved successfully");
626    }
627
628    Ok(CheckpointResponse {
629        found: false,
630        state: Vec::new(),
631        pending_signal,
632        custom_signal,
633        last_error: None,
634    })
635}
636
637/// Helper to get the pending instance-wide signal for an instance.
638async fn get_pending_signal(persistence: &dyn Persistence, instance_id: &str) -> Option<Signal> {
639    match persistence.get_pending_signal(instance_id).await {
640        Ok(Some(signal)) => {
641            let signal_type = match signal.signal_type.as_str() {
642                "cancel" => SignalType::SignalCancel,
643                "pause" => SignalType::SignalPause,
644                "resume" => SignalType::SignalResume,
645                _ => return None,
646            };
647            Some(Signal {
648                instance_id: instance_id.to_string(),
649                signal_type: signal_type.into(),
650                payload: signal.payload.unwrap_or_default(),
651            })
652        }
653        _ => None,
654    }
655}
656
657/// Get checkpoint handler - read-only lookup without saving.
658///
659/// Returns the checkpoint state if found, or empty if not found.
660#[instrument(skip(state, request), fields(instance_id = %request.instance_id, checkpoint_id = %request.checkpoint_id))]
661pub async fn handle_get_checkpoint(
662    state: &InstanceHandlerState,
663    request: GetCheckpointRequest,
664) -> Result<GetCheckpointResponse> {
665    debug!("Looking up checkpoint (read-only)");
666
667    // 1. Validate instance exists
668    let instance = state.persistence.get_instance(&request.instance_id).await?;
669    if instance.is_none() {
670        return Err(CoreError::InstanceNotFound {
671            instance_id: request.instance_id.clone(),
672        }
673        .into());
674    }
675
676    // 2. Look up checkpoint
677    if let Some(checkpoint) = state
678        .persistence
679        .load_checkpoint(&request.instance_id, &request.checkpoint_id)
680        .await?
681    {
682        debug!(
683            checkpoint_id = %request.checkpoint_id,
684            state_size = checkpoint.state.len(),
685            "Checkpoint found"
686        );
687        return Ok(GetCheckpointResponse {
688            found: true,
689            state: checkpoint.state,
690        });
691    }
692
693    debug!(checkpoint_id = %request.checkpoint_id, "Checkpoint not found");
694    Ok(GetCheckpointResponse {
695        found: false,
696        state: Vec::new(),
697    })
698}
699
700// ============================================================================
701// Sleep/Wake
702// ============================================================================
703
704/// Handle durable sleep request.
705///
706/// Saves the checkpoint state before sleeping, then sleeps in-process.
707/// This ensures the state is durable and can be restored if the process
708/// is killed during the sleep.
709#[instrument(skip(state, request), fields(instance_id = %request.instance_id, checkpoint_id = %request.checkpoint_id))]
710pub async fn handle_sleep(
711    state: &InstanceHandlerState,
712    request: SleepRequest,
713) -> Result<SleepResponse> {
714    debug!(
715        duration_ms = request.duration_ms,
716        state_size = request.state.len(),
717        "Processing sleep request"
718    );
719
720    // 1. Save checkpoint before sleeping (for durability)
721    if !request.checkpoint_id.is_empty() {
722        state
723            .persistence
724            .save_checkpoint(&request.instance_id, &request.checkpoint_id, &request.state)
725            .await?;
726
727        // Update instance's current checkpoint_id
728        state
729            .persistence
730            .update_instance_checkpoint(&request.instance_id, &request.checkpoint_id)
731            .await?;
732
733        debug!(checkpoint_id = %request.checkpoint_id, "Sleep checkpoint saved");
734    }
735
736    // 2. Sleep in-process; environment may hibernate managed instances separately.
737    tokio::time::sleep(Duration::from_millis(request.duration_ms)).await;
738    Ok(SleepResponse {})
739}
740
741// ============================================================================
742// Instance Events
743// ============================================================================
744
745/// Handle instance event.
746///
747/// Processes events from instances:
748/// - **Heartbeat**: Update activity timestamp
749/// - **Completed**: Mark instance as completed, store output
750/// - **Failed**: Mark instance as failed, store error
751/// - **Suspended**: Mark instance as suspended
752/// - **Custom**: Store custom event for telemetry (debug events, etc.)
753///
754/// All events return `InstanceEventResponse` to acknowledge persistence.
755/// This ensures no events are lost due to race conditions when the process exits.
756#[instrument(skip(state, event), fields(instance_id = %event.instance_id, event_type = ?event.event_type()))]
757pub async fn handle_instance_event(
758    state: &InstanceHandlerState,
759    event: InstanceEvent,
760) -> Result<InstanceEventResponse> {
761    debug!(
762        event_type = ?event.event_type,
763        checkpoint_id = ?event.checkpoint_id,
764        payload_size = event.payload.len(),
765        timestamp_ms = event.timestamp_ms,
766        "Received instance event"
767    );
768
769    // 1. Map proto event type to DB enum
770    let event_type = map_event_type(event.event_type());
771
772    // 2. Validate instance_id is not empty
773    if event.instance_id.is_empty() {
774        return Err(CoreError::ValidationError {
775            field: "instance_id".to_string(),
776            message: "instance_id is required".to_string(),
777        }
778        .into());
779    }
780
781    // 3. Determine timestamp
782    let created_at = DateTime::from_timestamp_millis(event.timestamp_ms).unwrap_or_else(Utc::now);
783
784    // 4. Insert event record
785    let event_record = EventRecord {
786        id: None,
787        instance_id: event.instance_id.clone(),
788        event_type: event_type.to_string(),
789        checkpoint_id: event.checkpoint_id.clone(),
790        payload: if event.payload.is_empty() {
791            None
792        } else {
793            Some(event.payload.clone())
794        },
795        created_at,
796        subtype: event.subtype.clone(),
797    };
798    state.persistence.insert_event(&event_record).await?;
799
800    // 5. Update instance status based on event type
801    // All events return a response to acknowledge persistence
802    match event.event_type() {
803        InstanceEventType::EventHeartbeat => {
804            // Heartbeat is just an "I'm alive" signal - no state changes needed
805            // The event was already logged above
806            debug!("Heartbeat received");
807        }
808        InstanceEventType::EventCompleted => {
809            let output = if event.payload.is_empty() {
810                None
811            } else {
812                Some(event.payload.as_slice())
813            };
814            // Use _if_running to prevent race condition with PID monitor:
815            // If process crashed and PID monitor already set status to "failed",
816            // we should not overwrite it with "completed" from queued SDK event.
817            let applied = state
818                .persistence
819                .complete_instance_if_running(
820                    &event.instance_id,
821                    "completed",
822                    output,
823                    None,
824                    None,
825                    None,
826                )
827                .await?;
828            if applied {
829                info!("Instance completed successfully");
830            } else {
831                warn!("Instance completion skipped (already in terminal state)");
832            }
833        }
834        InstanceEventType::EventFailed => {
835            let error = if event.payload.is_empty() {
836                "Unknown error"
837            } else {
838                std::str::from_utf8(&event.payload).unwrap_or("Unknown error (binary payload)")
839            };
840            // Use _if_running to prevent race condition with PID monitor:
841            // If PID monitor already set status to "failed", don't overwrite with SDK event.
842            let applied = state
843                .persistence
844                .complete_instance_if_running(
845                    &event.instance_id,
846                    "failed",
847                    None,
848                    Some(error),
849                    None,
850                    None,
851                )
852                .await?;
853            if applied {
854                warn!(error = %error, "Instance failed");
855            } else {
856                warn!(error = %error, "Instance failure event skipped (already in terminal state)");
857            }
858        }
859        InstanceEventType::EventSuspended => {
860            // Check if this is a suspended-with-sleep event (has sleep data in payload)
861            if let (false, Some(checkpoint_id)) = (event.payload.is_empty(), &event.checkpoint_id) {
862                if let Some(sleep_data) = parse_sleep_payload(&event.payload) {
863                    // Save checkpoint with state from payload
864                    state
865                        .persistence
866                        .save_checkpoint(&event.instance_id, checkpoint_id, &sleep_data.state)
867                        .await?;
868
869                    // Update instance checkpoint reference
870                    state
871                        .persistence
872                        .update_instance_checkpoint(&event.instance_id, checkpoint_id)
873                        .await?;
874
875                    // Set sleep_until for wake scheduler
876                    if let Some(wake_at) = sleep_data.wake_at {
877                        state
878                            .persistence
879                            .set_instance_sleep(&event.instance_id, wake_at)
880                            .await?;
881                    }
882
883                    // Mark as suspended with termination_reason "sleeping"
884                    // Use _if_running to prevent race condition with PID monitor.
885                    let applied = state
886                        .persistence
887                        .complete_instance_with_termination_if_running(
888                            &event.instance_id,
889                            "suspended",
890                            Some("sleeping"),
891                            None, // exit_code
892                            None, // output
893                            None, // error
894                            None, // stderr
895                            Some(checkpoint_id),
896                        )
897                        .await?;
898
899                    if applied {
900                        info!(
901                            checkpoint_id = %checkpoint_id,
902                            wake_at = ?sleep_data.wake_at,
903                            "Instance sleeping until scheduled wake"
904                        );
905                    } else {
906                        warn!(
907                            checkpoint_id = %checkpoint_id,
908                            "Instance sleep event skipped (already in terminal state)"
909                        );
910                    }
911                } else {
912                    // Payload present but not valid sleep data - just suspend
913                    // Use _if_running to prevent race condition with PID monitor.
914                    let applied = state
915                        .persistence
916                        .complete_instance_if_running(
917                            &event.instance_id,
918                            "suspended",
919                            None,
920                            None,
921                            None,
922                            None,
923                        )
924                        .await?;
925                    if applied {
926                        info!("Instance suspended (with payload but no valid sleep data)");
927                    } else {
928                        warn!("Instance suspend event skipped (already in terminal state)");
929                    }
930                }
931            } else {
932                // No payload or no checkpoint_id - simple suspend
933                // Use _if_running to prevent race condition with PID monitor.
934                let applied = state
935                    .persistence
936                    .complete_instance_if_running(
937                        &event.instance_id,
938                        "suspended",
939                        None,
940                        None,
941                        None,
942                        None,
943                    )
944                    .await?;
945                if applied {
946                    info!("Instance suspended");
947                } else {
948                    warn!("Instance suspend event skipped (already in terminal state)");
949                }
950            }
951        }
952        InstanceEventType::EventCustom => {
953            // Custom events are just stored for telemetry - no state changes needed
954            // The event was already logged above with its subtype
955            debug!(subtype = ?event.subtype, "Custom event received");
956        }
957    }
958
959    Ok(InstanceEventResponse {
960        success: true,
961        error: None,
962    })
963}
964
965// ============================================================================
966// Instance Status
967// ============================================================================
968
969/// Handle instance status query.
970///
971/// Returns the current status of an instance including:
972/// - Current status (pending, running, suspended, completed, failed, cancelled)
973/// - Last checkpoint ID
974/// - Start/finish timestamps
975/// - Output data (if completed) or error message (if failed)
976#[instrument(skip(state, request), fields(instance_id = %request.instance_id))]
977pub async fn handle_get_instance_status(
978    state: &InstanceHandlerState,
979    request: GetInstanceStatusRequest,
980) -> Result<GetInstanceStatusResponse> {
981    debug!("Getting instance status");
982
983    let instance = state.persistence.get_instance(&request.instance_id).await?;
984
985    match instance {
986        Some(inst) => {
987            let status = map_status(&inst.status);
988
989            Ok(GetInstanceStatusResponse {
990                instance_id: request.instance_id,
991                status: status.into(),
992                checkpoint_id: inst.checkpoint_id,
993                started_at_ms: inst.started_at.map(|t| t.timestamp_millis()).unwrap_or(0),
994                finished_at_ms: inst.finished_at.map(|t| t.timestamp_millis()),
995                output: inst.output,
996                error: inst.error,
997            })
998        }
999        None => Ok(GetInstanceStatusResponse {
1000            instance_id: request.instance_id,
1001            status: InstanceStatus::StatusUnknown.into(),
1002            checkpoint_id: None,
1003            started_at_ms: 0,
1004            finished_at_ms: None,
1005            output: None,
1006            error: Some("Instance not found".to_string()),
1007        }),
1008    }
1009}
1010
1011// ============================================================================
1012// Signal Polling (instance → core)
1013// ============================================================================
1014
1015/// Handle signal polling request.
1016///
1017/// Returns the oldest pending signal for the instance, if any.
1018/// Signals are: cancel, pause, resume.
1019///
1020/// Note: The checkpoint response also includes pending signals for efficiency.
1021/// This endpoint is for explicit polling when not checkpointing.
1022#[instrument(skip(state, request), fields(instance_id = %request.instance_id))]
1023pub async fn handle_poll_signals(
1024    state: &InstanceHandlerState,
1025    request: PollSignalsRequest,
1026) -> Result<PollSignalsResponse> {
1027    debug!("Instance polling for signals");
1028
1029    let pending = state
1030        .persistence
1031        .get_pending_signal(&request.instance_id)
1032        .await?;
1033    let custom = if let Some(checkpoint_id) = request.checkpoint_id.as_deref() {
1034        state
1035            .persistence
1036            .take_pending_custom_signal(&request.instance_id, checkpoint_id)
1037            .await?
1038    } else {
1039        None
1040    };
1041
1042    let signal = pending.map(|sig| {
1043        let signal_type = match sig.signal_type.as_str() {
1044            "cancel" => SignalType::SignalCancel,
1045            "pause" => SignalType::SignalPause,
1046            "resume" => SignalType::SignalResume,
1047            _ => {
1048                warn!(signal_type = %sig.signal_type, "Unknown signal type");
1049                SignalType::SignalCancel
1050            }
1051        };
1052
1053        Signal {
1054            instance_id: request.instance_id.clone(),
1055            signal_type: signal_type.into(),
1056            payload: sig.payload.unwrap_or_default(),
1057        }
1058    });
1059
1060    let custom_signal = custom.map(|sig| CustomSignal {
1061        checkpoint_id: sig.checkpoint_id,
1062        payload: sig.payload.unwrap_or_default(),
1063    });
1064
1065    if signal.is_some() || custom_signal.is_some() {
1066        debug!(
1067            has_global = signal.is_some(),
1068            has_custom = custom_signal.is_some(),
1069            "Returning pending signals"
1070        );
1071    }
1072
1073    Ok(PollSignalsResponse {
1074        signal,
1075        custom_signal,
1076    })
1077}
1078
1079/// Handle signal acknowledgement (fire-and-forget).
1080///
1081/// Marks a signal as acknowledged by the instance.
1082/// If acknowledging a cancel signal, also updates instance status to cancelled.
1083#[instrument(skip(state, ack), fields(instance_id = %ack.instance_id))]
1084pub async fn handle_signal_ack(state: &InstanceHandlerState, ack: SignalAck) -> Result<()> {
1085    debug!(
1086        signal_type = ?ack.signal_type,
1087        acknowledged = ack.acknowledged,
1088        "Received signal acknowledgement"
1089    );
1090
1091    if ack.acknowledged {
1092        // Mark signal as acknowledged
1093        state
1094            .persistence
1095            .acknowledge_signal(&ack.instance_id)
1096            .await?;
1097
1098        // Handle signal-specific side effects
1099        match ack.signal_type() {
1100            SignalType::SignalCancel => {
1101                // Update instance status to cancelled with finished_at
1102                state
1103                    .persistence
1104                    .complete_instance_extended(
1105                        &ack.instance_id,
1106                        "cancelled",
1107                        None, // output
1108                        None, // error
1109                        None, // stderr
1110                        None, // checkpoint_id
1111                    )
1112                    .await?;
1113                info!("Instance cancelled");
1114            }
1115            SignalType::SignalPause => {
1116                // Update instance status to suspended
1117                state
1118                    .persistence
1119                    .update_instance_status(&ack.instance_id, "suspended", None)
1120                    .await?;
1121                info!("Instance paused/suspended");
1122            }
1123            SignalType::SignalResume => {
1124                // Instance should resume execution
1125                debug!("Resume signal acknowledged");
1126            }
1127        }
1128    } else {
1129        warn!("Signal was not acknowledged by instance");
1130    }
1131
1132    Ok(())
1133}
1134
1135// ============================================================================
1136// Retry Tracking
1137// ============================================================================
1138
1139/// Handle retry attempt event (fire-and-forget).
1140///
1141/// Records a retry attempt for audit trail. Retry attempts are stored
1142/// in the checkpoints table with `is_retry_attempt=true`.
1143///
1144/// This is sent by the SDK when a durable function fails and is about
1145/// to be retried (before the backoff delay).
1146#[instrument(skip(state, event), fields(
1147    instance_id = %event.instance_id,
1148    checkpoint_id = %event.checkpoint_id,
1149    attempt = event.attempt_number,
1150    error_message = ?event.error_message,
1151))]
1152pub async fn handle_retry_attempt(
1153    state: &InstanceHandlerState,
1154    event: RetryAttemptEvent,
1155) -> Result<()> {
1156    debug!(timestamp_ms = event.timestamp_ms, "Recording retry attempt");
1157
1158    // Save retry attempt record for audit trail
1159    state
1160        .persistence
1161        .save_retry_attempt(
1162            &event.instance_id,
1163            &event.checkpoint_id,
1164            event.attempt_number as i32,
1165            event.error_message.as_deref(),
1166        )
1167        .await?;
1168
1169    if let Some(ref meta) = event.error_metadata {
1170        info!(
1171            error_category = ?meta.category(),
1172            error_severity = ?meta.severity(),
1173            retry_hint = ?meta.retry_hint(),
1174            error_code = ?meta.error_code,
1175            retry_after_ms = ?meta.retry_after_ms,
1176            "Retry attempt with error metadata"
1177        );
1178    }
1179
1180    debug!("Retry attempt recorded");
1181
1182    Ok(())
1183}
1184
1185// ============================================================================
1186// Helper Functions
1187// ============================================================================
1188
1189/// Map proto event type to database enum string.
1190pub fn map_event_type(event_type: InstanceEventType) -> &'static str {
1191    match event_type {
1192        InstanceEventType::EventHeartbeat => "heartbeat",
1193        InstanceEventType::EventCompleted => "completed",
1194        InstanceEventType::EventFailed => "failed",
1195        InstanceEventType::EventSuspended => "suspended",
1196        InstanceEventType::EventCustom => "custom",
1197    }
1198}
1199
1200/// Map database status string to InstanceStatus enum.
1201pub fn map_status(status: &str) -> InstanceStatus {
1202    match status {
1203        "pending" => InstanceStatus::StatusPending,
1204        "running" => InstanceStatus::StatusRunning,
1205        "suspended" => InstanceStatus::StatusSuspended,
1206        "completed" => InstanceStatus::StatusCompleted,
1207        "failed" => InstanceStatus::StatusFailed,
1208        "cancelled" => InstanceStatus::StatusCancelled,
1209        _ => InstanceStatus::StatusUnknown,
1210    }
1211}
1212
1213/// Map SignalType enum to database enum string.
1214pub fn map_signal_type(signal_type: SignalType) -> &'static str {
1215    match signal_type {
1216        SignalType::SignalCancel => "cancel",
1217        SignalType::SignalPause => "pause",
1218        SignalType::SignalResume => "resume",
1219    }
1220}
1221
1222/// Parsed sleep data from a suspended event payload.
1223struct SleepPayload {
1224    wake_at: Option<DateTime<Utc>>,
1225    state: Vec<u8>,
1226}
1227
1228/// Parse sleep data from a suspended event payload.
1229///
1230/// The SDK sends JSON with:
1231/// - `wake_at_ms`: Unix timestamp in milliseconds for when to wake
1232/// - `state`: Base64-encoded checkpoint state
1233fn parse_sleep_payload(payload: &[u8]) -> Option<SleepPayload> {
1234    use base64::Engine;
1235
1236    // Try to parse as JSON
1237    let json: serde_json::Value = serde_json::from_slice(payload).ok()?;
1238
1239    // Extract wake_at_ms
1240    let wake_at_ms = json.get("wake_at_ms")?.as_i64()?;
1241    let wake_at = DateTime::from_timestamp_millis(wake_at_ms);
1242
1243    // Extract and decode state
1244    let state_b64 = json.get("state")?.as_str()?;
1245    let state = base64::engine::general_purpose::STANDARD
1246        .decode(state_b64)
1247        .ok()?;
1248
1249    Some(SleepPayload { wake_at, state })
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254    use super::*;
1255    use crate::persistence::{
1256        CheckpointRecord, CustomSignalRecord, InstanceRecord, ListEventsFilter,
1257        ListStepSummariesFilter, SignalRecord, StepSummaryRecord,
1258    };
1259    use async_trait::async_trait;
1260    use std::collections::HashMap;
1261    use std::sync::Mutex;
1262
1263    /// Mock persistence for handler unit tests.
1264    struct MockPersistence {
1265        instances: Mutex<HashMap<String, InstanceRecord>>,
1266        checkpoints: Mutex<HashMap<(String, String), CheckpointRecord>>,
1267        signals: Mutex<HashMap<String, SignalRecord>>,
1268        events: Mutex<Vec<EventRecord>>,
1269        custom_signals: Mutex<HashMap<(String, String), CustomSignalRecord>>,
1270        fail_register: Mutex<bool>,
1271        fail_status_update: Mutex<bool>,
1272    }
1273
1274    impl MockPersistence {
1275        fn new() -> Self {
1276            Self {
1277                instances: Mutex::new(HashMap::new()),
1278                checkpoints: Mutex::new(HashMap::new()),
1279                signals: Mutex::new(HashMap::new()),
1280                events: Mutex::new(Vec::new()),
1281                custom_signals: Mutex::new(HashMap::new()),
1282                fail_register: Mutex::new(false),
1283                fail_status_update: Mutex::new(false),
1284            }
1285        }
1286
1287        fn with_instance(self, instance: InstanceRecord) -> Self {
1288            self.instances
1289                .lock()
1290                .unwrap()
1291                .insert(instance.instance_id.clone(), instance);
1292            self
1293        }
1294
1295        fn with_checkpoint(self, checkpoint: CheckpointRecord) -> Self {
1296            self.checkpoints.lock().unwrap().insert(
1297                (
1298                    checkpoint.instance_id.clone(),
1299                    checkpoint.checkpoint_id.clone(),
1300                ),
1301                checkpoint,
1302            );
1303            self
1304        }
1305
1306        fn with_signal(self, signal: SignalRecord) -> Self {
1307            self.signals
1308                .lock()
1309                .unwrap()
1310                .insert(signal.instance_id.clone(), signal);
1311            self
1312        }
1313
1314        fn with_custom_signal(self, signal: CustomSignalRecord) -> Self {
1315            self.custom_signals.lock().unwrap().insert(
1316                (signal.instance_id.clone(), signal.checkpoint_id.clone()),
1317                signal,
1318            );
1319            self
1320        }
1321
1322        #[allow(dead_code)]
1323        fn set_fail_register(&self) {
1324            *self.fail_register.lock().unwrap() = true;
1325        }
1326
1327        #[allow(dead_code)]
1328        fn set_fail_status_update(&self) {
1329            *self.fail_status_update.lock().unwrap() = true;
1330        }
1331
1332        fn get_events(&self) -> Vec<EventRecord> {
1333            self.events.lock().unwrap().clone()
1334        }
1335    }
1336
1337    fn make_instance(instance_id: &str, tenant_id: &str, status: &str) -> InstanceRecord {
1338        InstanceRecord {
1339            instance_id: instance_id.to_string(),
1340            tenant_id: tenant_id.to_string(),
1341            definition_version: 1,
1342            status: status.to_string(),
1343            checkpoint_id: None,
1344            attempt: 1,
1345            max_attempts: 3,
1346            created_at: Utc::now(),
1347            started_at: None,
1348            finished_at: None,
1349            input: None,
1350            output: None,
1351            error: None,
1352            sleep_until: None,
1353            termination_reason: None,
1354            exit_code: None,
1355        }
1356    }
1357
1358    fn make_checkpoint(instance_id: &str, checkpoint_id: &str, state: &[u8]) -> CheckpointRecord {
1359        CheckpointRecord {
1360            id: 1,
1361            instance_id: instance_id.to_string(),
1362            checkpoint_id: checkpoint_id.to_string(),
1363            state: state.to_vec(),
1364            created_at: Utc::now(),
1365            is_compensatable: false,
1366            compensation_step_id: None,
1367            compensation_data: None,
1368            compensation_state: None,
1369            compensation_order: 0,
1370        }
1371    }
1372
1373    fn make_signal(instance_id: &str, signal_type: &str) -> SignalRecord {
1374        SignalRecord {
1375            instance_id: instance_id.to_string(),
1376            signal_type: signal_type.to_string(),
1377            payload: None,
1378            created_at: Utc::now(),
1379            acknowledged_at: None,
1380        }
1381    }
1382
1383    #[async_trait]
1384    impl Persistence for MockPersistence {
1385        async fn register_instance(
1386            &self,
1387            instance_id: &str,
1388            tenant_id: &str,
1389        ) -> std::result::Result<(), CoreError> {
1390            if *self.fail_register.lock().unwrap() {
1391                return Err(CoreError::DatabaseError {
1392                    operation: "register_instance".to_string(),
1393                    details: "Mock register failure".to_string(),
1394                });
1395            }
1396            let instance = make_instance(instance_id, tenant_id, "pending");
1397            self.instances
1398                .lock()
1399                .unwrap()
1400                .insert(instance_id.to_string(), instance);
1401            Ok(())
1402        }
1403
1404        async fn get_instance(
1405            &self,
1406            instance_id: &str,
1407        ) -> std::result::Result<Option<InstanceRecord>, CoreError> {
1408            Ok(self.instances.lock().unwrap().get(instance_id).cloned())
1409        }
1410
1411        async fn update_instance_status(
1412            &self,
1413            instance_id: &str,
1414            status: &str,
1415            started_at: Option<DateTime<Utc>>,
1416        ) -> std::result::Result<(), CoreError> {
1417            if *self.fail_status_update.lock().unwrap() {
1418                return Err(CoreError::DatabaseError {
1419                    operation: "update_instance_status".to_string(),
1420                    details: "Mock status update failure".to_string(),
1421                });
1422            }
1423            if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1424                inst.status = status.to_string();
1425                inst.started_at = started_at;
1426            }
1427            Ok(())
1428        }
1429
1430        async fn update_instance_checkpoint(
1431            &self,
1432            instance_id: &str,
1433            checkpoint_id: &str,
1434        ) -> std::result::Result<(), CoreError> {
1435            if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1436                inst.checkpoint_id = Some(checkpoint_id.to_string());
1437            }
1438            Ok(())
1439        }
1440
1441        async fn complete_instance(
1442            &self,
1443            instance_id: &str,
1444            output: Option<&[u8]>,
1445            error: Option<&str>,
1446        ) -> std::result::Result<(), CoreError> {
1447            if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1448                inst.status = if error.is_some() {
1449                    "failed".to_string()
1450                } else {
1451                    "completed".to_string()
1452                };
1453                inst.output = output.map(|o| o.to_vec());
1454                inst.error = error.map(|e| e.to_string());
1455                inst.finished_at = Some(Utc::now());
1456            }
1457            Ok(())
1458        }
1459
1460        async fn complete_instance_if_running(
1461            &self,
1462            instance_id: &str,
1463            status: &str,
1464            output: Option<&[u8]>,
1465            error: Option<&str>,
1466            _stderr: Option<&str>,
1467            checkpoint_id: Option<&str>,
1468        ) -> std::result::Result<bool, CoreError> {
1469            let mut instances = self.instances.lock().unwrap();
1470            if let Some(inst) = instances.get_mut(instance_id)
1471                && inst.status == "running"
1472            {
1473                inst.status = status.to_string();
1474                inst.output = output.map(|o| o.to_vec());
1475                inst.error = error.map(|e| e.to_string());
1476                inst.checkpoint_id = checkpoint_id.map(|s| s.to_string());
1477                if status == "completed"
1478                    || status == "failed"
1479                    || status == "cancelled"
1480                    || status == "suspended"
1481                {
1482                    inst.finished_at = Some(Utc::now());
1483                }
1484                return Ok(true);
1485            }
1486            Ok(false)
1487        }
1488
1489        async fn complete_instance_with_termination(
1490            &self,
1491            instance_id: &str,
1492            status: &str,
1493            termination_reason: Option<&str>,
1494            exit_code: Option<i32>,
1495            output: Option<&[u8]>,
1496            error: Option<&str>,
1497            _stderr: Option<&str>,
1498            checkpoint_id: Option<&str>,
1499        ) -> std::result::Result<(), CoreError> {
1500            if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1501                inst.status = status.to_string();
1502                inst.termination_reason = termination_reason.map(|s| s.to_string());
1503                inst.exit_code = exit_code;
1504                inst.output = output.map(|o| o.to_vec());
1505                inst.error = error.map(|e| e.to_string());
1506                inst.checkpoint_id = checkpoint_id.map(|s| s.to_string());
1507                if status == "completed" || status == "failed" || status == "cancelled" {
1508                    inst.finished_at = Some(Utc::now());
1509                }
1510            }
1511            Ok(())
1512        }
1513
1514        async fn complete_instance_with_termination_if_running(
1515            &self,
1516            instance_id: &str,
1517            status: &str,
1518            termination_reason: Option<&str>,
1519            exit_code: Option<i32>,
1520            output: Option<&[u8]>,
1521            error: Option<&str>,
1522            _stderr: Option<&str>,
1523            checkpoint_id: Option<&str>,
1524        ) -> std::result::Result<bool, CoreError> {
1525            let mut instances = self.instances.lock().unwrap();
1526            if let Some(inst) = instances.get_mut(instance_id)
1527                && inst.status == "running"
1528            {
1529                inst.status = status.to_string();
1530                inst.termination_reason = termination_reason.map(|s| s.to_string());
1531                inst.exit_code = exit_code;
1532                inst.output = output.map(|o| o.to_vec());
1533                inst.error = error.map(|e| e.to_string());
1534                inst.checkpoint_id = checkpoint_id.map(|s| s.to_string());
1535                if status == "completed" || status == "failed" || status == "cancelled" {
1536                    inst.finished_at = Some(Utc::now());
1537                }
1538                return Ok(true);
1539            }
1540            Ok(false)
1541        }
1542
1543        async fn save_checkpoint(
1544            &self,
1545            instance_id: &str,
1546            checkpoint_id: &str,
1547            state: &[u8],
1548        ) -> std::result::Result<(), CoreError> {
1549            let cp = make_checkpoint(instance_id, checkpoint_id, state);
1550            self.checkpoints
1551                .lock()
1552                .unwrap()
1553                .insert((instance_id.to_string(), checkpoint_id.to_string()), cp);
1554            Ok(())
1555        }
1556
1557        async fn load_checkpoint(
1558            &self,
1559            instance_id: &str,
1560            checkpoint_id: &str,
1561        ) -> std::result::Result<Option<CheckpointRecord>, CoreError> {
1562            Ok(self
1563                .checkpoints
1564                .lock()
1565                .unwrap()
1566                .get(&(instance_id.to_string(), checkpoint_id.to_string()))
1567                .cloned())
1568        }
1569
1570        async fn list_checkpoints(
1571            &self,
1572            _instance_id: &str,
1573            _checkpoint_id: Option<&str>,
1574            _limit: i64,
1575            _offset: i64,
1576            _created_after: Option<DateTime<Utc>>,
1577            _created_before: Option<DateTime<Utc>>,
1578        ) -> std::result::Result<Vec<CheckpointRecord>, CoreError> {
1579            Ok(Vec::new())
1580        }
1581
1582        async fn count_checkpoints(
1583            &self,
1584            _instance_id: &str,
1585            _checkpoint_id: Option<&str>,
1586            _created_after: Option<DateTime<Utc>>,
1587            _created_before: Option<DateTime<Utc>>,
1588        ) -> std::result::Result<i64, CoreError> {
1589            Ok(0)
1590        }
1591
1592        async fn insert_event(&self, event: &EventRecord) -> std::result::Result<(), CoreError> {
1593            self.events.lock().unwrap().push(event.clone());
1594            Ok(())
1595        }
1596
1597        async fn insert_signal(
1598            &self,
1599            instance_id: &str,
1600            signal_type: &str,
1601            _payload: &[u8],
1602        ) -> std::result::Result<(), CoreError> {
1603            let signal = make_signal(instance_id, signal_type);
1604            self.signals
1605                .lock()
1606                .unwrap()
1607                .insert(instance_id.to_string(), signal);
1608            Ok(())
1609        }
1610
1611        async fn get_pending_signal(
1612            &self,
1613            instance_id: &str,
1614        ) -> std::result::Result<Option<SignalRecord>, CoreError> {
1615            Ok(self.signals.lock().unwrap().get(instance_id).cloned())
1616        }
1617
1618        async fn acknowledge_signal(
1619            &self,
1620            instance_id: &str,
1621        ) -> std::result::Result<(), CoreError> {
1622            self.signals.lock().unwrap().remove(instance_id);
1623            Ok(())
1624        }
1625
1626        async fn insert_custom_signal(
1627            &self,
1628            _instance_id: &str,
1629            _checkpoint_id: &str,
1630            _payload: &[u8],
1631        ) -> std::result::Result<(), CoreError> {
1632            Ok(())
1633        }
1634
1635        async fn take_pending_custom_signal(
1636            &self,
1637            instance_id: &str,
1638            checkpoint_id: &str,
1639        ) -> std::result::Result<Option<CustomSignalRecord>, CoreError> {
1640            Ok(self
1641                .custom_signals
1642                .lock()
1643                .unwrap()
1644                .remove(&(instance_id.to_string(), checkpoint_id.to_string())))
1645        }
1646
1647        async fn save_retry_attempt(
1648            &self,
1649            _instance_id: &str,
1650            _checkpoint_id: &str,
1651            _attempt: i32,
1652            _error_message: Option<&str>,
1653        ) -> std::result::Result<(), CoreError> {
1654            Ok(())
1655        }
1656
1657        async fn list_instances(
1658            &self,
1659            _tenant_id: Option<&str>,
1660            _status: Option<&str>,
1661            _limit: i64,
1662            _offset: i64,
1663        ) -> std::result::Result<Vec<InstanceRecord>, CoreError> {
1664            Ok(Vec::new())
1665        }
1666
1667        async fn health_check_db(&self) -> std::result::Result<bool, CoreError> {
1668            Ok(true)
1669        }
1670
1671        async fn count_active_instances(&self) -> std::result::Result<i64, CoreError> {
1672            Ok(0)
1673        }
1674
1675        async fn set_instance_sleep(
1676            &self,
1677            instance_id: &str,
1678            sleep_until: DateTime<Utc>,
1679        ) -> std::result::Result<(), CoreError> {
1680            if let Some(inst) = self.instances.lock().unwrap().get_mut(instance_id) {
1681                inst.sleep_until = Some(sleep_until);
1682            }
1683            Ok(())
1684        }
1685
1686        async fn clear_instance_sleep(
1687            &self,
1688            _instance_id: &str,
1689        ) -> std::result::Result<(), CoreError> {
1690            Ok(())
1691        }
1692
1693        async fn get_sleeping_instances_due(
1694            &self,
1695            _limit: i64,
1696        ) -> std::result::Result<Vec<InstanceRecord>, CoreError> {
1697            Ok(Vec::new())
1698        }
1699
1700        async fn list_events(
1701            &self,
1702            _instance_id: &str,
1703            _filter: &ListEventsFilter,
1704            _limit: i64,
1705            _offset: i64,
1706        ) -> std::result::Result<Vec<EventRecord>, CoreError> {
1707            Ok(Vec::new())
1708        }
1709
1710        async fn count_events(
1711            &self,
1712            _instance_id: &str,
1713            _filter: &ListEventsFilter,
1714        ) -> std::result::Result<i64, CoreError> {
1715            Ok(0)
1716        }
1717
1718        async fn list_step_summaries(
1719            &self,
1720            _instance_id: &str,
1721            _filter: &ListStepSummariesFilter,
1722            _limit: i64,
1723            _offset: i64,
1724        ) -> std::result::Result<Vec<StepSummaryRecord>, CoreError> {
1725            Ok(Vec::new())
1726        }
1727
1728        async fn count_step_summaries(
1729            &self,
1730            _instance_id: &str,
1731            _filter: &ListStepSummariesFilter,
1732        ) -> std::result::Result<i64, CoreError> {
1733            Ok(0)
1734        }
1735    }
1736
1737    #[test]
1738    fn test_event_type_mapping() {
1739        assert_eq!(
1740            map_event_type(InstanceEventType::EventHeartbeat),
1741            "heartbeat"
1742        );
1743        assert_eq!(
1744            map_event_type(InstanceEventType::EventCompleted),
1745            "completed"
1746        );
1747        assert_eq!(map_event_type(InstanceEventType::EventFailed), "failed");
1748        assert_eq!(
1749            map_event_type(InstanceEventType::EventSuspended),
1750            "suspended"
1751        );
1752        assert_eq!(map_event_type(InstanceEventType::EventCustom), "custom");
1753    }
1754
1755    #[test]
1756    fn test_status_mapping_all_variants() {
1757        assert_eq!(map_status("pending"), InstanceStatus::StatusPending);
1758        assert_eq!(map_status("running"), InstanceStatus::StatusRunning);
1759        assert_eq!(map_status("suspended"), InstanceStatus::StatusSuspended);
1760        assert_eq!(map_status("completed"), InstanceStatus::StatusCompleted);
1761        assert_eq!(map_status("failed"), InstanceStatus::StatusFailed);
1762        assert_eq!(map_status("cancelled"), InstanceStatus::StatusCancelled);
1763        assert_eq!(map_status("invalid"), InstanceStatus::StatusUnknown);
1764        assert_eq!(map_status(""), InstanceStatus::StatusUnknown);
1765    }
1766
1767    #[test]
1768    fn test_signal_type_mapping() {
1769        assert_eq!(map_signal_type(SignalType::SignalCancel), "cancel");
1770        assert_eq!(map_signal_type(SignalType::SignalPause), "pause");
1771        assert_eq!(map_signal_type(SignalType::SignalResume), "resume");
1772    }
1773
1774    #[test]
1775    fn test_instance_handler_state_new() {
1776        let persistence = Arc::new(MockPersistence::new());
1777        let state = InstanceHandlerState::new(persistence);
1778        // Just verify it compiles and persistence is accessible
1779        let _ = &state.persistence;
1780    }
1781
1782    // ========================================================================
1783    // Register Instance Handler Tests
1784    // ========================================================================
1785
1786    #[tokio::test]
1787    async fn test_register_empty_instance_id() {
1788        let persistence = Arc::new(MockPersistence::new());
1789        let state = InstanceHandlerState::new(persistence);
1790
1791        let request = RegisterInstanceRequest {
1792            instance_id: "".to_string(),
1793            tenant_id: "tenant-1".to_string(),
1794            checkpoint_id: None,
1795        };
1796
1797        let result = handle_register_instance(&state, request).await.unwrap();
1798        assert!(!result.success);
1799        assert!(result.error.contains("instance_id is required"));
1800    }
1801
1802    #[tokio::test]
1803    async fn test_register_empty_tenant_id() {
1804        let persistence = Arc::new(MockPersistence::new());
1805        let state = InstanceHandlerState::new(persistence);
1806
1807        let request = RegisterInstanceRequest {
1808            instance_id: "inst-1".to_string(),
1809            tenant_id: "".to_string(),
1810            checkpoint_id: None,
1811        };
1812
1813        let result = handle_register_instance(&state, request).await.unwrap();
1814        assert!(!result.success);
1815        assert!(result.error.contains("tenant_id is required"));
1816    }
1817
1818    #[tokio::test]
1819    async fn test_register_self_registration() {
1820        let persistence = Arc::new(MockPersistence::new());
1821        let state = InstanceHandlerState::new(persistence);
1822
1823        let request = RegisterInstanceRequest {
1824            instance_id: "inst-new".to_string(),
1825            tenant_id: "tenant-1".to_string(),
1826            checkpoint_id: None,
1827        };
1828
1829        let result = handle_register_instance(&state, request).await.unwrap();
1830        assert!(result.success);
1831        assert!(result.error.is_empty());
1832    }
1833
1834    #[tokio::test]
1835    async fn test_register_existing_instance() {
1836        let persistence = Arc::new(
1837            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "pending")),
1838        );
1839        let state = InstanceHandlerState::new(persistence);
1840
1841        let request = RegisterInstanceRequest {
1842            instance_id: "inst-1".to_string(),
1843            tenant_id: "tenant-1".to_string(),
1844            checkpoint_id: None,
1845        };
1846
1847        let result = handle_register_instance(&state, request).await.unwrap();
1848        assert!(result.success);
1849    }
1850
1851    #[tokio::test]
1852    async fn test_register_with_valid_checkpoint() {
1853        let persistence = Arc::new(
1854            MockPersistence::new()
1855                .with_instance(make_instance("inst-1", "tenant-1", "pending"))
1856                .with_checkpoint(make_checkpoint("inst-1", "cp-1", b"state")),
1857        );
1858        let state = InstanceHandlerState::new(persistence);
1859
1860        let request = RegisterInstanceRequest {
1861            instance_id: "inst-1".to_string(),
1862            tenant_id: "tenant-1".to_string(),
1863            checkpoint_id: Some("cp-1".to_string()),
1864        };
1865
1866        let result = handle_register_instance(&state, request).await.unwrap();
1867        assert!(result.success);
1868    }
1869
1870    #[tokio::test]
1871    async fn test_register_with_invalid_checkpoint() {
1872        let persistence = Arc::new(
1873            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "pending")),
1874        );
1875        let state = InstanceHandlerState::new(persistence);
1876
1877        let request = RegisterInstanceRequest {
1878            instance_id: "inst-1".to_string(),
1879            tenant_id: "tenant-1".to_string(),
1880            checkpoint_id: Some("nonexistent".to_string()),
1881        };
1882
1883        let result = handle_register_instance(&state, request).await.unwrap();
1884        assert!(!result.success);
1885        assert!(result.error.contains("not found"));
1886    }
1887
1888    #[tokio::test]
1889    async fn test_register_creates_started_event() {
1890        let mock = MockPersistence::new();
1891        let persistence = Arc::new(mock);
1892        let state = InstanceHandlerState::new(persistence.clone());
1893
1894        let request = RegisterInstanceRequest {
1895            instance_id: "inst-1".to_string(),
1896            tenant_id: "tenant-1".to_string(),
1897            checkpoint_id: None,
1898        };
1899
1900        let result = handle_register_instance(&state, request).await.unwrap();
1901        assert!(result.success);
1902
1903        // Check that started event was created
1904        let events = persistence.get_events();
1905        assert!(!events.is_empty());
1906        assert_eq!(events[0].event_type, "started");
1907        assert_eq!(events[0].instance_id, "inst-1");
1908    }
1909
1910    // ========================================================================
1911    // Checkpoint Handler Tests
1912    // ========================================================================
1913
1914    #[tokio::test]
1915    async fn test_checkpoint_instance_not_found() {
1916        let persistence = Arc::new(MockPersistence::new());
1917        let state = InstanceHandlerState::new(persistence);
1918
1919        let request = CheckpointRequest {
1920            instance_id: "nonexistent".to_string(),
1921            checkpoint_id: "cp-1".to_string(),
1922            state: b"test state".to_vec(),
1923        };
1924
1925        let result = handle_checkpoint(&state, request).await;
1926        assert!(result.is_err());
1927    }
1928
1929    #[tokio::test]
1930    async fn test_checkpoint_instance_not_running() {
1931        let persistence = Arc::new(MockPersistence::new().with_instance(make_instance(
1932            "inst-1",
1933            "tenant-1",
1934            "completed",
1935        )));
1936        let state = InstanceHandlerState::new(persistence);
1937
1938        let request = CheckpointRequest {
1939            instance_id: "inst-1".to_string(),
1940            checkpoint_id: "cp-1".to_string(),
1941            state: b"test state".to_vec(),
1942        };
1943
1944        let result = handle_checkpoint(&state, request).await;
1945        assert!(result.is_err());
1946    }
1947
1948    #[tokio::test]
1949    async fn test_checkpoint_new_saves_state() {
1950        let persistence = Arc::new(
1951            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
1952        );
1953        let state = InstanceHandlerState::new(persistence);
1954
1955        let request = CheckpointRequest {
1956            instance_id: "inst-1".to_string(),
1957            checkpoint_id: "cp-1".to_string(),
1958            state: b"test state".to_vec(),
1959        };
1960
1961        let result = handle_checkpoint(&state, request).await.unwrap();
1962        assert!(!result.found); // New checkpoint, not found
1963    }
1964
1965    #[tokio::test]
1966    async fn test_checkpoint_existing_returns_state() {
1967        let persistence = Arc::new(
1968            MockPersistence::new()
1969                .with_instance(make_instance("inst-1", "tenant-1", "running"))
1970                .with_checkpoint(make_checkpoint("inst-1", "cp-1", b"existing state")),
1971        );
1972        let state = InstanceHandlerState::new(persistence);
1973
1974        let request = CheckpointRequest {
1975            instance_id: "inst-1".to_string(),
1976            checkpoint_id: "cp-1".to_string(),
1977            state: b"new state".to_vec(), // This should be ignored
1978        };
1979
1980        let result = handle_checkpoint(&state, request).await.unwrap();
1981        assert!(result.found);
1982        assert_eq!(result.state, b"existing state");
1983    }
1984
1985    #[tokio::test]
1986    async fn test_checkpoint_returns_pending_signal() {
1987        let persistence = Arc::new(
1988            MockPersistence::new()
1989                .with_instance(make_instance("inst-1", "tenant-1", "running"))
1990                .with_signal(make_signal("inst-1", "cancel")),
1991        );
1992        let state = InstanceHandlerState::new(persistence);
1993
1994        let request = CheckpointRequest {
1995            instance_id: "inst-1".to_string(),
1996            checkpoint_id: "cp-1".to_string(),
1997            state: b"test state".to_vec(),
1998        };
1999
2000        let result = handle_checkpoint(&state, request).await.unwrap();
2001        assert!(result.pending_signal.is_some());
2002        let signal = result.pending_signal.unwrap();
2003        assert_eq!(signal.signal_type, SignalType::SignalCancel as i32);
2004    }
2005
2006    #[tokio::test]
2007    async fn test_checkpoint_returns_custom_signal() {
2008        let custom_signal = CustomSignalRecord {
2009            instance_id: "inst-1".to_string(),
2010            checkpoint_id: "cp-1".to_string(),
2011            payload: Some(b"custom payload".to_vec()),
2012            created_at: Utc::now(),
2013        };
2014        let persistence = Arc::new(
2015            MockPersistence::new()
2016                .with_instance(make_instance("inst-1", "tenant-1", "running"))
2017                .with_custom_signal(custom_signal),
2018        );
2019        let state = InstanceHandlerState::new(persistence);
2020
2021        let request = CheckpointRequest {
2022            instance_id: "inst-1".to_string(),
2023            checkpoint_id: "cp-1".to_string(),
2024            state: b"test state".to_vec(),
2025        };
2026
2027        let result = handle_checkpoint(&state, request).await.unwrap();
2028        assert!(result.custom_signal.is_some());
2029        let cs = result.custom_signal.unwrap();
2030        assert_eq!(cs.checkpoint_id, "cp-1");
2031        assert_eq!(cs.payload, b"custom payload");
2032    }
2033
2034    // ========================================================================
2035    // Get Instance Status Handler Tests
2036    // ========================================================================
2037
2038    #[tokio::test]
2039    async fn test_get_status_not_found() {
2040        let persistence = Arc::new(MockPersistence::new());
2041        let state = InstanceHandlerState::new(persistence);
2042
2043        let request = GetInstanceStatusRequest {
2044            instance_id: "nonexistent".to_string(),
2045        };
2046
2047        let result = handle_get_instance_status(&state, request).await.unwrap();
2048        // Instance not found returns StatusUnknown
2049        assert_eq!(result.status, InstanceStatus::StatusUnknown as i32);
2050    }
2051
2052    #[tokio::test]
2053    async fn test_get_status_found() {
2054        let persistence = Arc::new(
2055            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2056        );
2057        let state = InstanceHandlerState::new(persistence);
2058
2059        let request = GetInstanceStatusRequest {
2060            instance_id: "inst-1".to_string(),
2061        };
2062
2063        let result = handle_get_instance_status(&state, request).await.unwrap();
2064        assert_eq!(result.status, InstanceStatus::StatusRunning as i32);
2065    }
2066
2067    // ========================================================================
2068    // Poll Signals Handler Tests
2069    // ========================================================================
2070
2071    #[tokio::test]
2072    async fn test_poll_signals_no_signal() {
2073        let persistence = Arc::new(
2074            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2075        );
2076        let state = InstanceHandlerState::new(persistence);
2077
2078        let request = PollSignalsRequest {
2079            instance_id: "inst-1".to_string(),
2080            checkpoint_id: None,
2081        };
2082
2083        let result = handle_poll_signals(&state, request).await.unwrap();
2084        assert!(result.signal.is_none());
2085    }
2086
2087    #[tokio::test]
2088    async fn test_poll_signals_with_pending_signal() {
2089        let persistence = Arc::new(
2090            MockPersistence::new()
2091                .with_instance(make_instance("inst-1", "tenant-1", "running"))
2092                .with_signal(make_signal("inst-1", "pause")),
2093        );
2094        let state = InstanceHandlerState::new(persistence);
2095
2096        let request = PollSignalsRequest {
2097            instance_id: "inst-1".to_string(),
2098            checkpoint_id: None,
2099        };
2100
2101        let result = handle_poll_signals(&state, request).await.unwrap();
2102        assert!(result.signal.is_some());
2103        let signal = result.signal.unwrap();
2104        assert_eq!(signal.signal_type, SignalType::SignalPause as i32);
2105    }
2106
2107    // ========================================================================
2108    // Signal Ack Handler Tests
2109    // ========================================================================
2110
2111    #[tokio::test]
2112    async fn test_signal_ack_success() {
2113        let persistence = Arc::new(
2114            MockPersistence::new()
2115                .with_instance(make_instance("inst-1", "tenant-1", "running"))
2116                .with_signal(make_signal("inst-1", "cancel")),
2117        );
2118        let state = InstanceHandlerState::new(persistence.clone());
2119
2120        let request = SignalAck {
2121            instance_id: "inst-1".to_string(),
2122            signal_type: SignalType::SignalCancel as i32,
2123            acknowledged: true,
2124        };
2125
2126        // handle_signal_ack returns Result<()>
2127        handle_signal_ack(&state, request).await.unwrap();
2128
2129        // Verify signal was acknowledged (removed from pending)
2130        assert!(
2131            persistence
2132                .get_pending_signal("inst-1")
2133                .await
2134                .unwrap()
2135                .is_none()
2136        );
2137    }
2138
2139    // ========================================================================
2140    // Instance Event Handler Tests
2141    // ========================================================================
2142
2143    #[tokio::test]
2144    async fn test_handle_event_heartbeat() {
2145        let persistence = Arc::new(
2146            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2147        );
2148        let state = InstanceHandlerState::new(persistence.clone());
2149
2150        let event = InstanceEvent {
2151            instance_id: "inst-1".to_string(),
2152            event_type: InstanceEventType::EventHeartbeat as i32,
2153            checkpoint_id: None,
2154            payload: Vec::new(),
2155            timestamp_ms: chrono::Utc::now().timestamp_millis(),
2156            subtype: None,
2157        };
2158
2159        let result = handle_instance_event(&state, event).await.unwrap();
2160        assert!(result.success);
2161
2162        // Verify event was inserted
2163        let events = persistence.get_events();
2164        assert!(!events.is_empty());
2165        assert_eq!(events[0].event_type, "heartbeat");
2166    }
2167
2168    #[tokio::test]
2169    async fn test_handle_event_completed() {
2170        let persistence = Arc::new(
2171            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2172        );
2173        let state = InstanceHandlerState::new(persistence.clone());
2174
2175        let event = InstanceEvent {
2176            instance_id: "inst-1".to_string(),
2177            event_type: InstanceEventType::EventCompleted as i32,
2178            checkpoint_id: None,
2179            payload: b"result".to_vec(),
2180            timestamp_ms: chrono::Utc::now().timestamp_millis(),
2181            subtype: None,
2182        };
2183
2184        let result = handle_instance_event(&state, event).await.unwrap();
2185        assert!(result.success);
2186
2187        // Verify instance was completed
2188        let inst = persistence.get_instance("inst-1").await.unwrap().unwrap();
2189        assert_eq!(inst.status, "completed");
2190    }
2191
2192    #[tokio::test]
2193    async fn test_handle_event_failed() {
2194        let persistence = Arc::new(
2195            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2196        );
2197        let state = InstanceHandlerState::new(persistence.clone());
2198
2199        let event = InstanceEvent {
2200            instance_id: "inst-1".to_string(),
2201            event_type: InstanceEventType::EventFailed as i32,
2202            checkpoint_id: None,
2203            payload: b"error message".to_vec(),
2204            timestamp_ms: chrono::Utc::now().timestamp_millis(),
2205            subtype: None,
2206        };
2207
2208        let result = handle_instance_event(&state, event).await.unwrap();
2209        assert!(result.success);
2210
2211        // Verify instance was failed
2212        let inst = persistence.get_instance("inst-1").await.unwrap().unwrap();
2213        assert_eq!(inst.status, "failed");
2214    }
2215
2216    #[tokio::test]
2217    async fn test_handle_event_suspended() {
2218        let persistence = Arc::new(
2219            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2220        );
2221        let state = InstanceHandlerState::new(persistence.clone());
2222
2223        let event = InstanceEvent {
2224            instance_id: "inst-1".to_string(),
2225            event_type: InstanceEventType::EventSuspended as i32,
2226            checkpoint_id: None,
2227            payload: Vec::new(),
2228            timestamp_ms: chrono::Utc::now().timestamp_millis(),
2229            subtype: None,
2230        };
2231
2232        let result = handle_instance_event(&state, event).await.unwrap();
2233        assert!(result.success);
2234
2235        // Verify instance was suspended
2236        let inst = persistence.get_instance("inst-1").await.unwrap().unwrap();
2237        assert_eq!(inst.status, "suspended");
2238    }
2239
2240    #[tokio::test]
2241    async fn test_handle_event_custom() {
2242        let persistence = Arc::new(
2243            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2244        );
2245        let state = InstanceHandlerState::new(persistence.clone());
2246
2247        let event = InstanceEvent {
2248            instance_id: "inst-1".to_string(),
2249            event_type: InstanceEventType::EventCustom as i32,
2250            checkpoint_id: None,
2251            payload: b"custom data".to_vec(),
2252            timestamp_ms: chrono::Utc::now().timestamp_millis(),
2253            subtype: Some("my_custom_type".to_string()),
2254        };
2255
2256        let result = handle_instance_event(&state, event).await.unwrap();
2257        assert!(result.success);
2258
2259        // Verify event was inserted with subtype
2260        let events = persistence.get_events();
2261        assert!(!events.is_empty());
2262        assert_eq!(events[0].event_type, "custom");
2263        assert_eq!(events[0].subtype.as_deref(), Some("my_custom_type"));
2264    }
2265
2266    #[tokio::test]
2267    async fn test_handle_event_suspended_with_sleep() {
2268        use base64::Engine;
2269
2270        let persistence = Arc::new(
2271            MockPersistence::new().with_instance(make_instance("inst-1", "tenant-1", "running")),
2272        );
2273        let state = InstanceHandlerState::new(persistence.clone());
2274
2275        // Create sleep payload like SDK does
2276        let wake_at = chrono::Utc::now() + chrono::Duration::hours(1);
2277        let checkpoint_state = b"test checkpoint state";
2278        let payload = serde_json::json!({
2279            "wake_at_ms": wake_at.timestamp_millis(),
2280            "state": base64::engine::general_purpose::STANDARD.encode(checkpoint_state),
2281        });
2282
2283        let event = InstanceEvent {
2284            instance_id: "inst-1".to_string(),
2285            event_type: InstanceEventType::EventSuspended as i32,
2286            checkpoint_id: Some("sleep-cp-1".to_string()),
2287            payload: payload.to_string().into_bytes(),
2288            timestamp_ms: chrono::Utc::now().timestamp_millis(),
2289            subtype: None,
2290        };
2291
2292        let result = handle_instance_event(&state, event).await.unwrap();
2293        assert!(result.success);
2294
2295        // Verify instance was suspended with sleep data
2296        let inst = persistence.get_instance("inst-1").await.unwrap().unwrap();
2297        assert_eq!(inst.status, "suspended");
2298        assert_eq!(inst.termination_reason.as_deref(), Some("sleeping"));
2299        assert_eq!(inst.checkpoint_id.as_deref(), Some("sleep-cp-1"));
2300        assert!(inst.sleep_until.is_some());
2301
2302        // Verify checkpoint was saved
2303        let cp = persistence
2304            .load_checkpoint("inst-1", "sleep-cp-1")
2305            .await
2306            .unwrap()
2307            .unwrap();
2308        assert_eq!(cp.state, checkpoint_state);
2309    }
2310
2311    #[test]
2312    fn test_parse_sleep_payload_valid() {
2313        use base64::Engine;
2314
2315        let state = b"test state";
2316        let wake_at_ms = 1750000000000i64; // Some future timestamp
2317        let payload = serde_json::json!({
2318            "wake_at_ms": wake_at_ms,
2319            "state": base64::engine::general_purpose::STANDARD.encode(state),
2320        });
2321
2322        let result = parse_sleep_payload(&payload.to_string().into_bytes());
2323        assert!(result.is_some());
2324
2325        let sleep_data = result.unwrap();
2326        assert_eq!(sleep_data.state, state);
2327        assert!(sleep_data.wake_at.is_some());
2328        assert_eq!(sleep_data.wake_at.unwrap().timestamp_millis(), wake_at_ms);
2329    }
2330
2331    #[test]
2332    fn test_parse_sleep_payload_invalid_json() {
2333        let result = parse_sleep_payload(b"not json");
2334        assert!(result.is_none());
2335    }
2336
2337    #[test]
2338    fn test_parse_sleep_payload_missing_fields() {
2339        let payload = serde_json::json!({
2340            "wake_at_ms": 1750000000000i64,
2341            // missing "state"
2342        });
2343        let result = parse_sleep_payload(&payload.to_string().into_bytes());
2344        assert!(result.is_none());
2345    }
2346}