Skip to main content

nexus_sdk/scheduler/
snapshot.rs

1//! Object backed Task and occurrence snapshots.
2
3use {
4    crate::{
5        scheduler::{FailurePolicy, OccurrenceRef},
6        sui,
7    },
8    serde::{Deserialize, Serialize},
9    std::time::Duration,
10};
11
12const DEFAULT_WATCH_TIMEOUT: Duration = Duration::from_secs(60 * 60);
13const DEFAULT_WATCH_POLL_INTERVAL: Duration = Duration::from_secs(1);
14
15/// Immutable controller of a Task.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(tag = "kind", rename_all = "snake_case")]
18pub enum TaskController {
19    /// A Sui address controls the Task.
20    Address {
21        /// Controller address.
22        address: sui::types::Address,
23    },
24    /// An Agent object controls the Task.
25    Agent {
26        /// Controller Agent identifier.
27        agent_id: sui::types::Address,
28    },
29}
30
31/// Durable lifecycle state of a Task.
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum TaskStatus {
35    /// The Task may advertise and dispatch eligible work.
36    Active,
37    /// Dispatch is paused while future work is retained.
38    Paused,
39    /// Future work was canceled while in flight work may still settle.
40    Canceled,
41    /// Current admission rejected the proposal and released its reserve.
42    Rejected {
43        /// On chain reason proving why admission could not honor the proposal.
44        reason: TaskRejectionReason,
45    },
46    /// Live resources were released while history was retained.
47    Finalized,
48}
49
50/// On chain reason that made a Task proposal permanently inadmissible.
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum TaskRejectionReason {
54    /// Current code does not implement the proposal's WorkAdmission contract.
55    UnsupportedWorkAdmission,
56    /// The referenced Agent skill no longer matches the proposal snapshot.
57    StaleSkillContract,
58    /// The proposal references a mutable DAG rather than a finalized contract.
59    MutableDAG,
60}
61
62/// Origin of an allocated occurrence.
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(tag = "kind", rename_all = "snake_case")]
65pub enum OccurrenceSource {
66    /// The controller scheduled the occurrence independently.
67    Standalone,
68    /// A recurrence allocated the occurrence.
69    Recurring {
70        /// Zero based recurrence iteration.
71        iteration: u64,
72    },
73}
74
75/// Reason an occurrence left its Schedule before dispatch.
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum WithdrawalReason {
79    /// A new recurrence replaced the recurrence candidate.
80    RecurrenceReplaced,
81    /// The controller cleared the recurrence.
82    RecurrenceCleared,
83    /// The controller canceled the Task.
84    TaskCanceled,
85    /// Current admission permanently rejected the enclosing Task.
86    TaskRejected,
87}
88
89/// Public lifecycle projection of one occurrence.
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(tag = "status", rename_all = "snake_case")]
92pub enum OccurrenceStatus {
93    /// Allocated but not currently offered for dispatch.
94    Pending,
95    /// Currently offered to the leader network.
96    Advertised,
97    /// Dispatched while runtime work remains observable as incomplete.
98    Executing,
99    /// Runtime work is complete but scheduler settlement is pending.
100    Finished,
101    /// Scheduler settlement completed.
102    Settled {
103        /// Whether runtime execution succeeded.
104        succeeded: bool,
105    },
106    /// The dispatch deadline elapsed before dispatch.
107    Missed {
108        /// Timestamp at which the scheduler recorded expiration.
109        missed_at_ms: u64,
110    },
111    /// Removed from the Schedule before dispatch.
112    Withdrawn {
113        /// Reason the occurrence left its Schedule.
114        reason: WithdrawalReason,
115    },
116}
117
118impl OccurrenceStatus {
119    /// Returns whether scheduler lifecycle processing is complete.
120    pub const fn is_terminal(self) -> bool {
121        matches!(
122            self,
123            Self::Settled { .. } | Self::Missed { .. } | Self::Withdrawn { .. }
124        )
125    }
126}
127
128/// Owned reference that makes one scheduler Task discoverable.
129#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
130pub struct TaskPointer {
131    task_pointer_id: sui::types::Address,
132    task_id: sui::types::Address,
133}
134
135impl TaskPointer {
136    pub(crate) const fn new(
137        task_pointer_id: sui::types::Address,
138        task_id: sui::types::Address,
139    ) -> Self {
140        Self {
141            task_pointer_id,
142            task_id,
143        }
144    }
145
146    /// Returns the [`TaskPointer`] object identifier.
147    pub const fn task_pointer_id(&self) -> sui::types::Address {
148        self.task_pointer_id
149    }
150
151    /// Returns the referenced Task identifier.
152    pub const fn task_id(&self) -> sui::types::Address {
153        self.task_id
154    }
155}
156
157/// One page of owned [`TaskPointer`] objects.
158#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
159pub struct TaskPointerPage {
160    task_pointers: Vec<TaskPointer>,
161    next_cursor: Option<Vec<u8>>,
162}
163
164impl TaskPointerPage {
165    pub(crate) fn new(task_pointers: Vec<TaskPointer>, next_cursor: Option<Vec<u8>>) -> Self {
166        Self {
167            task_pointers,
168            next_cursor,
169        }
170    }
171
172    /// Returns the [`TaskPointer`] values in RPC order.
173    pub fn task_pointers(&self) -> &[TaskPointer] {
174        &self.task_pointers
175    }
176
177    /// Returns the opaque cursor for the next page.
178    pub fn next_cursor(&self) -> Option<&[u8]> {
179        self.next_cursor.as_deref()
180    }
181
182    /// Separates the [`TaskPointer`] values from the opaque next page cursor.
183    pub fn into_parts(self) -> (Vec<TaskPointer>, Option<Vec<u8>>) {
184        (self.task_pointers, self.next_cursor)
185    }
186}
187
188/// Observable workflow state related to one dispatched occurrence.
189#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
190pub struct ExecutionSnapshot {
191    execution_id: sui::types::Address,
192    created_at_ms: Option<u64>,
193    active_walks: Option<u64>,
194    pending_abort_walks: Option<u64>,
195    pending_settlement_walks: Option<u64>,
196    successful_walks: Option<u64>,
197    failed_walks: Option<u64>,
198    aborted_walks: Option<u64>,
199}
200
201pub(crate) struct ExecutionObservation {
202    pub created_at_ms: u64,
203    pub active_walks: u64,
204    pub pending_abort_walks: u64,
205    pub pending_settlement_walks: u64,
206    pub successful_walks: u64,
207    pub failed_walks: u64,
208    pub aborted_walks: u64,
209}
210
211impl ExecutionSnapshot {
212    pub(crate) const fn unavailable(execution_id: sui::types::Address) -> Self {
213        Self {
214            execution_id,
215            created_at_ms: None,
216            active_walks: None,
217            pending_abort_walks: None,
218            pending_settlement_walks: None,
219            successful_walks: None,
220            failed_walks: None,
221            aborted_walks: None,
222        }
223    }
224
225    pub(crate) const fn observed(
226        execution_id: sui::types::Address,
227        observation: ExecutionObservation,
228    ) -> Self {
229        Self {
230            execution_id,
231            created_at_ms: Some(observation.created_at_ms),
232            active_walks: Some(observation.active_walks),
233            pending_abort_walks: Some(observation.pending_abort_walks),
234            pending_settlement_walks: Some(observation.pending_settlement_walks),
235            successful_walks: Some(observation.successful_walks),
236            failed_walks: Some(observation.failed_walks),
237            aborted_walks: Some(observation.aborted_walks),
238        }
239    }
240
241    /// Returns the deterministic workflow execution identifier.
242    pub const fn execution_id(&self) -> sui::types::Address {
243        self.execution_id
244    }
245
246    /// Returns the runtime creation timestamp when the object is available.
247    pub const fn created_at_ms(&self) -> Option<u64> {
248        self.created_at_ms
249    }
250
251    /// Returns the active walk count when the object is available.
252    pub const fn active_walks(&self) -> Option<u64> {
253        self.active_walks
254    }
255
256    /// Returns the pending abort walk count when the object is available.
257    pub const fn pending_abort_walks(&self) -> Option<u64> {
258        self.pending_abort_walks
259    }
260
261    /// Returns the pending settlement walk count when the object is available.
262    pub const fn pending_settlement_walks(&self) -> Option<u64> {
263        self.pending_settlement_walks
264    }
265
266    /// Returns the successful walk count when the object is available.
267    pub const fn successful_walks(&self) -> Option<u64> {
268        self.successful_walks
269    }
270
271    /// Returns the failed walk count when the object is available.
272    pub const fn failed_walks(&self) -> Option<u64> {
273        self.failed_walks
274    }
275
276    /// Returns the aborted walk count when the object is available.
277    pub const fn aborted_walks(&self) -> Option<u64> {
278        self.aborted_walks
279    }
280}
281
282/// Current object backed view of one occurrence.
283#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
284pub struct OccurrenceSnapshot {
285    pub(crate) reference: OccurrenceRef,
286    pub(crate) source: OccurrenceSource,
287    pub(crate) requested_start_time_ms: u64,
288    pub(crate) effective_start_time_ms: Option<u64>,
289    pub(crate) deadline_ms: Option<u64>,
290    pub(crate) priority_fee_percentage: u64,
291    pub(crate) dispatched_at_ms: Option<u64>,
292    pub(crate) settled_at_ms: Option<u64>,
293    pub(crate) status: OccurrenceStatus,
294    pub(crate) execution: Option<ExecutionSnapshot>,
295    pub(crate) observed_task_version: sui::types::Version,
296}
297
298impl OccurrenceSnapshot {
299    /// Returns the stable occurrence identity.
300    pub const fn reference(&self) -> OccurrenceRef {
301        self.reference
302    }
303
304    /// Returns how the occurrence was allocated.
305    pub const fn source(&self) -> OccurrenceSource {
306        self.source
307    }
308
309    /// Returns the requested absolute start timestamp.
310    pub const fn requested_start_time_ms(&self) -> u64 {
311        self.requested_start_time_ms
312    }
313
314    /// Returns the latest effective start advertised to leaders.
315    pub const fn effective_start_time_ms(&self) -> Option<u64> {
316        self.effective_start_time_ms
317    }
318
319    /// Returns the optional absolute dispatch deadline.
320    pub const fn deadline_ms(&self) -> Option<u64> {
321        self.deadline_ms
322    }
323
324    /// Returns the dispatch priority fee percentage.
325    pub const fn priority_fee_percentage(&self) -> u64 {
326        self.priority_fee_percentage
327    }
328
329    /// Returns the dispatch timestamp when dispatch occurred.
330    pub const fn dispatched_at_ms(&self) -> Option<u64> {
331        self.dispatched_at_ms
332    }
333
334    /// Returns the settlement timestamp when settlement occurred.
335    pub const fn settled_at_ms(&self) -> Option<u64> {
336        self.settled_at_ms
337    }
338
339    /// Returns the projected public lifecycle state.
340    pub const fn status(&self) -> OccurrenceStatus {
341        self.status
342    }
343
344    /// Returns observable runtime data after dispatch.
345    pub const fn execution(&self) -> Option<&ExecutionSnapshot> {
346        self.execution.as_ref()
347    }
348
349    /// Returns the Task version used for this snapshot.
350    pub const fn observed_task_version(&self) -> sui::types::Version {
351        self.observed_task_version
352    }
353}
354
355/// Current object backed view of one Task.
356#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
357pub struct TaskSnapshot {
358    pub(crate) task_id: sui::types::Address,
359    pub(crate) controller: TaskController,
360    pub(crate) status: TaskStatus,
361    pub(crate) failure_policy: FailurePolicy,
362    pub(crate) advertised: Option<OccurrenceRef>,
363    pub(crate) allocated_occurrences: u64,
364    pub(crate) pending_occurrences: u64,
365    pub(crate) dispatched_occurrences: u64,
366    pub(crate) in_flight_occurrences: u64,
367    pub(crate) observed_version: sui::types::Version,
368}
369
370impl TaskSnapshot {
371    /// Returns the Task identifier.
372    pub const fn task_id(&self) -> sui::types::Address {
373        self.task_id
374    }
375
376    /// Returns the immutable Task controller.
377    pub const fn controller(&self) -> TaskController {
378        self.controller
379    }
380
381    /// Returns the durable Task lifecycle state.
382    pub const fn status(&self) -> TaskStatus {
383        self.status
384    }
385
386    /// Returns the configured failure policy.
387    pub const fn failure_policy(&self) -> FailurePolicy {
388        self.failure_policy
389    }
390
391    /// Returns the occurrence currently offered to leaders.
392    pub const fn advertised(&self) -> Option<OccurrenceRef> {
393        self.advertised
394    }
395
396    /// Returns the number of occurrence identities allocated by the Task.
397    pub const fn allocated_occurrences(&self) -> u64 {
398        self.allocated_occurrences
399    }
400
401    /// Returns the number of pending occurrences.
402    pub const fn pending_occurrences(&self) -> u64 {
403        self.pending_occurrences
404    }
405
406    /// Returns the number of occurrences dispatched by the Task.
407    pub const fn dispatched_occurrences(&self) -> u64 {
408        self.dispatched_occurrences
409    }
410
411    /// Returns the number of dispatched occurrences awaiting settlement.
412    pub const fn in_flight_occurrences(&self) -> u64 {
413        self.in_flight_occurrences
414    }
415
416    /// Returns the object version used for this snapshot.
417    pub const fn observed_version(&self) -> sui::types::Version {
418        self.observed_version
419    }
420}
421
422/// One page of durable occurrence records.
423#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
424pub struct OccurrencePage {
425    occurrences: Vec<OccurrenceSnapshot>,
426    next_cursor: Option<Vec<u8>>,
427}
428
429impl OccurrencePage {
430    /// Creates one occurrence page with an unchanged RPC cursor.
431    pub(crate) fn new(occurrences: Vec<OccurrenceSnapshot>, next_cursor: Option<Vec<u8>>) -> Self {
432        Self {
433            occurrences,
434            next_cursor,
435        }
436    }
437
438    /// Returns the occurrence snapshots in RPC order.
439    pub fn occurrences(&self) -> &[OccurrenceSnapshot] {
440        &self.occurrences
441    }
442
443    /// Returns the opaque cursor for the next page.
444    pub fn next_cursor(&self) -> Option<&[u8]> {
445        self.next_cursor.as_deref()
446    }
447
448    /// Separates the occurrence snapshots from the opaque next page cursor.
449    pub fn into_parts(self) -> (Vec<OccurrenceSnapshot>, Option<Vec<u8>>) {
450        (self.occurrences, self.next_cursor)
451    }
452}
453
454/// Polling policy for watching an occurrence.
455#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456pub struct WatchOptions {
457    timeout: Duration,
458    poll_interval: Duration,
459}
460
461impl WatchOptions {
462    /// Creates a polling policy.
463    pub const fn new(timeout: Duration, poll_interval: Duration) -> Self {
464        Self {
465            timeout,
466            poll_interval,
467        }
468    }
469
470    /// Returns the maximum observation duration.
471    pub const fn timeout(self) -> Duration {
472        self.timeout
473    }
474
475    /// Returns the delay between object reads.
476    pub const fn poll_interval(self) -> Duration {
477        self.poll_interval
478    }
479}
480
481impl Default for WatchOptions {
482    fn default() -> Self {
483        Self::new(DEFAULT_WATCH_TIMEOUT, DEFAULT_WATCH_POLL_INTERVAL)
484    }
485}
486
487/// Current payment accounting for one occurrence execution.
488#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
489pub struct OccurrenceCost {
490    pub(crate) payment_id: sui::types::Address,
491    pub(crate) max_budget_mist: u64,
492    pub(crate) locked_budget_mist: u64,
493    pub(crate) consumed_mist: u64,
494    pub(crate) outstanding_locks: u64,
495    #[serde(default)]
496    pub(crate) outstanding_invocation_ids: Vec<sui::types::Address>,
497    pub(crate) accomplished: bool,
498    pub(crate) refunded: bool,
499}
500
501impl OccurrenceCost {
502    /// Returns the execution payment object identifier.
503    pub const fn payment_id(&self) -> sui::types::Address {
504        self.payment_id
505    }
506
507    /// Returns the maximum execution budget.
508    pub const fn max_budget_mist(&self) -> u64 {
509        self.max_budget_mist
510    }
511
512    /// Returns the budget currently locked by tool calls.
513    pub const fn locked_budget_mist(&self) -> u64 {
514        self.locked_budget_mist
515    }
516
517    /// Returns the amount already consumed.
518    pub const fn consumed_mist(&self) -> u64 {
519        self.consumed_mist
520    }
521
522    /// Returns the number of unresolved payment locks.
523    pub const fn outstanding_locks(&self) -> u64 {
524        self.outstanding_locks
525    }
526
527    /// Returns the exact invocations that still hold payment locks.
528    pub fn outstanding_invocation_ids(&self) -> &[sui::types::Address] {
529        &self.outstanding_invocation_ids
530    }
531
532    /// Returns whether execution accounting is complete.
533    pub const fn accomplished(&self) -> bool {
534        self.accomplished
535    }
536
537    /// Returns whether remaining funds were refunded.
538    pub const fn refunded(&self) -> bool {
539        self.refunded
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546
547    fn address(value: &'static str) -> sui::types::Address {
548        sui::types::Address::from_static(value)
549    }
550
551    #[test]
552    fn occurrence_terminality_matches_scheduler_completion() {
553        for status in [
554            OccurrenceStatus::Pending,
555            OccurrenceStatus::Advertised,
556            OccurrenceStatus::Executing,
557            OccurrenceStatus::Finished,
558        ] {
559            assert!(!status.is_terminal());
560        }
561        for status in [
562            OccurrenceStatus::Settled { succeeded: true },
563            OccurrenceStatus::Missed { missed_at_ms: 10 },
564            OccurrenceStatus::Withdrawn {
565                reason: WithdrawalReason::TaskCanceled,
566            },
567        ] {
568            assert!(status.is_terminal());
569        }
570    }
571
572    #[test]
573    fn execution_snapshots_distinguish_missing_and_observed_runtime_state() {
574        let execution_id = address("0x10");
575        let unavailable = ExecutionSnapshot::unavailable(execution_id);
576        assert_eq!(unavailable.execution_id(), execution_id);
577        assert_eq!(unavailable.created_at_ms(), None);
578        assert_eq!(unavailable.active_walks(), None);
579        assert_eq!(unavailable.pending_abort_walks(), None);
580        assert_eq!(unavailable.pending_settlement_walks(), None);
581        assert_eq!(unavailable.successful_walks(), None);
582        assert_eq!(unavailable.failed_walks(), None);
583        assert_eq!(unavailable.aborted_walks(), None);
584
585        let observed = ExecutionSnapshot::observed(
586            execution_id,
587            ExecutionObservation {
588                created_at_ms: 1,
589                active_walks: 2,
590                pending_abort_walks: 3,
591                pending_settlement_walks: 4,
592                successful_walks: 5,
593                failed_walks: 6,
594                aborted_walks: 7,
595            },
596        );
597        assert_eq!(observed.execution_id(), execution_id);
598        assert_eq!(observed.created_at_ms(), Some(1));
599        assert_eq!(observed.active_walks(), Some(2));
600        assert_eq!(observed.pending_abort_walks(), Some(3));
601        assert_eq!(observed.pending_settlement_walks(), Some(4));
602        assert_eq!(observed.successful_walks(), Some(5));
603        assert_eq!(observed.failed_walks(), Some(6));
604        assert_eq!(observed.aborted_walks(), Some(7));
605    }
606
607    #[test]
608    fn occurrence_and_task_snapshots_expose_one_coherent_object_view() {
609        let task_id = address("0x11");
610        let execution_id = address("0x12");
611        let reference = OccurrenceRef::new(task_id, 8);
612        let execution = ExecutionSnapshot::observed(
613            execution_id,
614            ExecutionObservation {
615                created_at_ms: 10,
616                active_walks: 0,
617                pending_abort_walks: 0,
618                pending_settlement_walks: 0,
619                successful_walks: 1,
620                failed_walks: 0,
621                aborted_walks: 0,
622            },
623        );
624        let occurrence = OccurrenceSnapshot {
625            reference,
626            source: OccurrenceSource::Recurring { iteration: 2 },
627            requested_start_time_ms: 20,
628            effective_start_time_ms: Some(21),
629            deadline_ms: Some(30),
630            priority_fee_percentage: 40,
631            dispatched_at_ms: Some(22),
632            settled_at_ms: Some(29),
633            status: OccurrenceStatus::Settled { succeeded: true },
634            execution: Some(execution),
635            observed_task_version: 7,
636        };
637
638        assert_eq!(occurrence.reference(), reference);
639        assert_eq!(
640            occurrence.source(),
641            OccurrenceSource::Recurring { iteration: 2 }
642        );
643        assert_eq!(occurrence.requested_start_time_ms(), 20);
644        assert_eq!(occurrence.effective_start_time_ms(), Some(21));
645        assert_eq!(occurrence.deadline_ms(), Some(30));
646        assert_eq!(occurrence.priority_fee_percentage(), 40);
647        assert_eq!(occurrence.dispatched_at_ms(), Some(22));
648        assert_eq!(occurrence.settled_at_ms(), Some(29));
649        assert_eq!(
650            occurrence.status(),
651            OccurrenceStatus::Settled { succeeded: true }
652        );
653        assert_eq!(
654            occurrence.execution().map(ExecutionSnapshot::execution_id),
655            Some(execution_id)
656        );
657        assert_eq!(occurrence.observed_task_version(), 7);
658
659        let task = TaskSnapshot {
660            task_id,
661            controller: TaskController::Address {
662                address: address("0x13"),
663            },
664            status: TaskStatus::Paused,
665            failure_policy: FailurePolicy::Pause,
666            advertised: Some(reference),
667            allocated_occurrences: 9,
668            pending_occurrences: 3,
669            dispatched_occurrences: 6,
670            in_flight_occurrences: 2,
671            observed_version: 8,
672        };
673        assert_eq!(task.task_id(), task_id);
674        assert_eq!(
675            task.controller(),
676            TaskController::Address {
677                address: address("0x13")
678            }
679        );
680        assert_eq!(task.status(), TaskStatus::Paused);
681        assert_eq!(task.failure_policy(), FailurePolicy::Pause);
682        assert_eq!(task.advertised(), Some(reference));
683        assert_eq!(task.allocated_occurrences(), 9);
684        assert_eq!(task.pending_occurrences(), 3);
685        assert_eq!(task.dispatched_occurrences(), 6);
686        assert_eq!(task.in_flight_occurrences(), 2);
687        assert_eq!(task.observed_version(), 8);
688    }
689
690    #[test]
691    fn occurrence_pages_preserve_records_and_opaque_cursors() {
692        let page = OccurrencePage::new(Vec::new(), Some(vec![1, 2, 3]));
693        assert!(page.occurrences().is_empty());
694        assert_eq!(page.next_cursor(), Some([1, 2, 3].as_slice()));
695
696        let (occurrences, cursor) = page.into_parts();
697        assert!(occurrences.is_empty());
698        assert_eq!(cursor, Some(vec![1, 2, 3]));
699    }
700
701    #[test]
702    fn task_pointer_pages_preserve_identity_and_opaque_cursors() {
703        let pointer = TaskPointer::new(address("0x20"), address("0x21"));
704        assert_eq!(pointer.task_pointer_id(), address("0x20"));
705        assert_eq!(pointer.task_id(), address("0x21"));
706
707        let page = TaskPointerPage::new(vec![pointer], Some(vec![1, 2, 3]));
708        assert_eq!(page.task_pointers(), &[pointer]);
709        assert_eq!(page.next_cursor(), Some([1, 2, 3].as_slice()));
710
711        let (pointers, cursor) = page.into_parts();
712        assert_eq!(pointers, vec![pointer]);
713        assert_eq!(cursor, Some(vec![1, 2, 3]));
714    }
715
716    #[test]
717    fn watch_options_and_costs_expose_their_complete_policy() {
718        let options = WatchOptions::new(Duration::from_secs(4), Duration::from_millis(250));
719        assert_eq!(options.timeout(), Duration::from_secs(4));
720        assert_eq!(options.poll_interval(), Duration::from_millis(250));
721
722        let defaults = WatchOptions::default();
723        assert_eq!(defaults.timeout(), DEFAULT_WATCH_TIMEOUT);
724        assert_eq!(defaults.poll_interval(), DEFAULT_WATCH_POLL_INTERVAL);
725
726        let cost = OccurrenceCost {
727            payment_id: address("0x14"),
728            max_budget_mist: 100,
729            locked_budget_mist: 30,
730            consumed_mist: 40,
731            outstanding_locks: 2,
732            outstanding_invocation_ids: vec![address("0x15"), address("0x16")],
733            accomplished: true,
734            refunded: false,
735        };
736        assert_eq!(cost.payment_id(), address("0x14"));
737        assert_eq!(cost.max_budget_mist(), 100);
738        assert_eq!(cost.locked_budget_mist(), 30);
739        assert_eq!(cost.consumed_mist(), 40);
740        assert_eq!(cost.outstanding_locks(), 2);
741        assert_eq!(
742            cost.outstanding_invocation_ids(),
743            [address("0x15"), address("0x16")]
744        );
745        let mut json = serde_json::to_value(&cost).unwrap();
746        assert_eq!(
747            serde_json::from_value::<OccurrenceCost>(json.clone()).unwrap(),
748            cost
749        );
750        json.as_object_mut()
751            .unwrap()
752            .remove("outstanding_invocation_ids");
753        assert!(serde_json::from_value::<OccurrenceCost>(json)
754            .unwrap()
755            .outstanding_invocation_ids()
756            .is_empty());
757        assert!(cost.accomplished());
758        assert!(!cost.refunded());
759    }
760}