1use std::collections::HashMap;
22
23use temporalio_client::tonic::Request;
24use temporalio_common::protos::temporal::api::{
25 common::v1::{Payload as ProtoPayload, Payloads, WorkflowExecution},
26 enums::v1::HistoryEventFilterType,
27 failure::v1::Failure,
28 history::v1::{HistoryEvent, history_event::Attributes},
29 update::v1::outcome::Value as OutcomeValue,
30 workflowservice::v1::GetWorkflowExecutionHistoryRequest,
31};
32use tmprl_core::history::{Category, GroupRef, NormalizedEvent, Outcome, Role};
33use tmprl_core::payload::Payload;
34
35use super::OpError;
36use crate::Conn;
37
38#[derive(Debug, Clone, Default)]
40pub struct HistoryPage {
41 pub events: Vec<NormalizedEvent>,
42 pub next_page_token: Vec<u8>,
44}
45
46impl HistoryPage {
47 pub fn has_more(&self) -> bool {
48 !self.next_page_token.is_empty()
49 }
50}
51
52impl Conn {
53 pub async fn follow_history(
77 &self,
78 namespace: &str,
79 workflow_id: &str,
80 run_id: &str,
81 next_page_token: Vec<u8>,
82 ) -> Result<HistoryPage, OpError> {
83 self.history_request(namespace, workflow_id, run_id, 100, next_page_token, true)
84 .await
85 }
86
87 pub async fn get_history(
94 &self,
95 namespace: &str,
96 workflow_id: &str,
97 run_id: &str,
98 page_size: i32,
99 next_page_token: Vec<u8>,
100 ) -> Result<HistoryPage, OpError> {
101 self.history_request(
102 namespace,
103 workflow_id,
104 run_id,
105 page_size,
106 next_page_token,
107 false,
108 )
109 .await
110 }
111
112 async fn history_request(
113 &self,
114 namespace: &str,
115 workflow_id: &str,
116 run_id: &str,
117 page_size: i32,
118 next_page_token: Vec<u8>,
119 wait_new_event: bool,
120 ) -> Result<HistoryPage, OpError> {
121 let resp = self
122 .wf()
123 .get_workflow_execution_history(Request::new(GetWorkflowExecutionHistoryRequest {
124 namespace: namespace.to_string(),
125 execution: Some(WorkflowExecution {
126 workflow_id: workflow_id.to_string(),
127 run_id: run_id.to_string(),
128 }),
129 maximum_page_size: page_size,
130 next_page_token,
131 wait_new_event,
132 history_event_filter_type: HistoryEventFilterType::AllEvent as i32,
133 skip_archival: false,
134 }))
135 .await
136 .map_err(|s| OpError::rpc("GetWorkflowExecutionHistory", s))?
137 .into_inner();
138
139 Ok(HistoryPage {
140 events: resp
141 .history
142 .map(|h| h.events)
143 .unwrap_or_default()
144 .into_iter()
145 .map(normalize)
146 .collect(),
147 next_page_token: resp.next_page_token,
148 })
149 }
150}
151
152struct Mapped {
155 category: Category,
156 group: GroupRef,
157 role: Role,
158 outcome: Outcome,
159 subject: String,
160 attempt: Option<i32>,
161 failure: Option<String>,
162 fields: Vec<(&'static str, String)>,
163 payloads: Vec<(String, Payload)>,
164}
165
166fn at(category: Category, group: GroupRef, role: Role) -> Mapped {
168 Mapped {
169 category,
170 group,
171 role,
172 outcome: Outcome::Pending,
173 subject: String::new(),
174 attempt: None,
175 failure: None,
176 fields: Vec::new(),
177 payloads: Vec::new(),
178 }
179}
180
181impl Mapped {
182 fn subject(mut self, s: impl Into<String>) -> Self {
183 self.subject = s.into();
184 self
185 }
186 fn ends(mut self, outcome: Outcome) -> Self {
187 self.outcome = outcome;
188 self
189 }
190 fn failed(mut self, f: Option<Failure>) -> Self {
191 self.failure = f.map(|f| f.message);
192 self
193 }
194 fn attempt(mut self, n: i32) -> Self {
195 self.attempt = Some(n);
196 self
197 }
198 fn field(mut self, k: &'static str, v: impl Into<String>) -> Self {
199 let v = v.into();
200 if !v.is_empty() {
201 self.fields.push((k, v));
202 }
203 self
204 }
205
206 fn args(mut self, label: &str, p: Option<Payloads>) -> Self {
209 let Some(list) = p else { return self };
210 let n = list.payloads.len();
211 for (i, raw) in list.payloads.into_iter().enumerate() {
212 let name = if n == 1 {
213 label.to_string()
214 } else {
215 format!("{label}[{i}]")
216 };
217 self.payloads.push((name, convert(raw)));
218 }
219 self
220 }
221
222 fn keyed_args(mut self, map: HashMap<String, Payloads>) -> Self {
227 let mut keys: Vec<String> = map.keys().cloned().collect();
228 keys.sort();
229 for k in keys {
230 let list = map[&k].clone();
231 self = self.args(&k, Some(list));
232 }
233 self
234 }
235
236 fn arg(mut self, label: &str, p: Option<ProtoPayload>) -> Self {
238 if let Some(raw) = p {
239 self.payloads.push((label.to_string(), convert(raw)));
240 }
241 self
242 }
243}
244
245fn convert(p: ProtoPayload) -> Payload {
248 let meta = |k: &str| {
249 p.metadata
250 .get(k)
251 .and_then(|v| std::str::from_utf8(v).ok())
252 .map(str::to_string)
253 };
254 Payload {
255 encoding: meta("encoding").unwrap_or_default(),
256 type_hint: meta("type"),
257 data: p.data,
258 }
259}
260
261pub fn normalize(e: HistoryEvent) -> NormalizedEvent {
263 let id = e.event_id;
264 let time = e
265 .event_time
266 .map(|t| t.seconds * 1000 + i64::from(t.nanos) / 1_000_000);
267 let name = event_name(e.event_type().as_str_name());
269
270 let m = match e.attributes {
271 None => at(Category::Workflow, GroupRef::Workflow, Role::Continues),
274
275 Some(Attributes::WorkflowExecutionStartedEventAttributes(a)) => {
276 at(Category::Workflow, GroupRef::Workflow, Role::Opens)
277 .subject(a.workflow_type.map(|t| t.name).unwrap_or_default())
278 .field(
279 "taskQueue",
280 a.task_queue.map(|q| q.name).unwrap_or_default(),
281 )
282 .field("attempt", a.attempt.to_string())
283 .field("firstRunId", a.first_execution_run_id)
284 .args("input", a.input)
285 .args("lastCompletionResult", a.last_completion_result)
286 }
287 Some(Attributes::WorkflowExecutionCompletedEventAttributes(a)) => {
288 at(Category::Workflow, GroupRef::Workflow, Role::Closes)
289 .ends(Outcome::Completed)
290 .args("result", a.result)
291 }
292 Some(Attributes::WorkflowExecutionFailedEventAttributes(a)) => {
293 at(Category::Workflow, GroupRef::Workflow, Role::Closes)
294 .ends(Outcome::Failed)
295 .failed(a.failure)
296 }
297 Some(Attributes::WorkflowExecutionTimedOutEventAttributes(_)) => {
298 at(Category::Workflow, GroupRef::Workflow, Role::Closes).ends(Outcome::TimedOut)
299 }
300 Some(Attributes::WorkflowExecutionCanceledEventAttributes(a)) => {
301 at(Category::Workflow, GroupRef::Workflow, Role::Closes)
302 .ends(Outcome::Canceled)
303 .args("details", a.details)
304 }
305 Some(Attributes::WorkflowExecutionTerminatedEventAttributes(a)) => {
306 at(Category::Workflow, GroupRef::Workflow, Role::Closes)
307 .ends(Outcome::Terminated)
308 .field("reason", a.reason)
309 .field("identity", a.identity)
310 .args("details", a.details)
311 }
312 Some(Attributes::WorkflowExecutionContinuedAsNewEventAttributes(a)) => {
313 at(Category::Workflow, GroupRef::Workflow, Role::Closes)
314 .ends(Outcome::ContinuedAsNew)
315 .field("newRunId", a.new_execution_run_id)
316 .args("input", a.input)
317 .args("lastCompletionResult", a.last_completion_result)
318 }
319 Some(Attributes::WorkflowExecutionCancelRequestedEventAttributes(a)) => {
320 at(Category::Workflow, GroupRef::Workflow, Role::Continues)
321 .field("cause", a.cause)
322 .field("identity", a.identity)
323 }
324 Some(Attributes::WorkflowExecutionSignaledEventAttributes(a)) => {
325 at(Category::Workflow, GroupRef::Workflow, Role::Continues)
326 .subject(a.signal_name)
327 .field("identity", a.identity)
328 .args("input", a.input)
329 }
330 Some(Attributes::WorkflowExecutionPausedEventAttributes(_)) => {
331 at(Category::Workflow, GroupRef::Workflow, Role::Continues)
332 }
333 Some(Attributes::WorkflowExecutionUnpausedEventAttributes(_)) => {
334 at(Category::Workflow, GroupRef::Workflow, Role::Continues)
335 }
336 Some(Attributes::WorkflowExecutionOptionsUpdatedEventAttributes(_)) => {
337 at(Category::Workflow, GroupRef::Workflow, Role::Continues)
338 }
339 Some(Attributes::WorkflowPropertiesModifiedEventAttributes(_)) => {
340 at(Category::Workflow, GroupRef::Workflow, Role::Continues)
341 }
342 Some(Attributes::WorkflowPropertiesModifiedExternallyEventAttributes(_)) => {
343 at(Category::Workflow, GroupRef::Workflow, Role::Continues)
344 }
345 Some(Attributes::WorkflowExecutionTimeSkippingTransitionedEventAttributes(_)) => {
346 at(Category::Workflow, GroupRef::Workflow, Role::Continues)
347 }
348
349 Some(Attributes::WorkflowTaskScheduledEventAttributes(a)) => {
350 at(Category::WorkflowTask, GroupRef::Opened(id), Role::Opens)
351 .attempt(a.attempt)
352 .field(
353 "taskQueue",
354 a.task_queue.map(|q| q.name).unwrap_or_default(),
355 )
356 }
357 Some(Attributes::WorkflowTaskStartedEventAttributes(a)) => at(
358 Category::WorkflowTask,
359 GroupRef::Opened(a.scheduled_event_id),
360 Role::Continues,
361 )
362 .field("identity", a.identity),
363 Some(Attributes::WorkflowTaskCompletedEventAttributes(a)) => at(
364 Category::WorkflowTask,
365 GroupRef::Opened(a.scheduled_event_id),
366 Role::Closes,
367 )
368 .ends(Outcome::Completed),
369 Some(Attributes::WorkflowTaskTimedOutEventAttributes(a)) => at(
370 Category::WorkflowTask,
371 GroupRef::Opened(a.scheduled_event_id),
372 Role::Closes,
373 )
374 .ends(Outcome::TimedOut),
375 Some(Attributes::WorkflowTaskFailedEventAttributes(a)) => at(
376 Category::WorkflowTask,
377 GroupRef::Opened(a.scheduled_event_id),
378 Role::Closes,
379 )
380 .ends(Outcome::Failed)
381 .failed(a.failure),
382
383 Some(Attributes::ActivityTaskScheduledEventAttributes(a)) => {
384 at(Category::Activity, GroupRef::Opened(id), Role::Opens)
385 .subject(a.activity_type.map(|t| t.name).unwrap_or_default())
386 .field("activityId", a.activity_id)
387 .field(
388 "taskQueue",
389 a.task_queue.map(|q| q.name).unwrap_or_default(),
390 )
391 .args("input", a.input)
392 }
393 Some(Attributes::ActivityTaskStartedEventAttributes(a)) => at(
394 Category::Activity,
395 GroupRef::Opened(a.scheduled_event_id),
396 Role::Continues,
397 )
398 .attempt(a.attempt)
401 .failed(a.last_failure)
402 .field("identity", a.identity),
403 Some(Attributes::ActivityTaskCompletedEventAttributes(a)) => at(
404 Category::Activity,
405 GroupRef::Opened(a.scheduled_event_id),
406 Role::Closes,
407 )
408 .ends(Outcome::Completed)
409 .args("result", a.result),
410 Some(Attributes::ActivityTaskFailedEventAttributes(a)) => at(
411 Category::Activity,
412 GroupRef::Opened(a.scheduled_event_id),
413 Role::Closes,
414 )
415 .ends(Outcome::Failed)
416 .failed(a.failure),
417 Some(Attributes::ActivityTaskTimedOutEventAttributes(a)) => at(
418 Category::Activity,
419 GroupRef::Opened(a.scheduled_event_id),
420 Role::Closes,
421 )
422 .ends(Outcome::TimedOut)
423 .failed(a.failure),
424 Some(Attributes::ActivityTaskCanceledEventAttributes(a)) => at(
425 Category::Activity,
426 GroupRef::Opened(a.scheduled_event_id),
427 Role::Closes,
428 )
429 .ends(Outcome::Canceled)
430 .args("details", a.details),
431 Some(Attributes::ActivityTaskCancelRequestedEventAttributes(a)) => at(
432 Category::Activity,
433 GroupRef::Opened(a.scheduled_event_id),
434 Role::Continues,
435 ),
436 Some(Attributes::ActivityPropertiesModifiedExternallyEventAttributes(a)) => at(
437 Category::Activity,
438 GroupRef::Opened(a.scheduled_event_id),
439 Role::Continues,
440 ),
441
442 Some(Attributes::TimerStartedEventAttributes(a)) => {
443 at(Category::Timer, GroupRef::Opened(id), Role::Opens)
444 .subject(a.timer_id)
445 .field(
446 "startToFireTimeout",
447 a.start_to_fire_timeout
448 .map(|d| format!("{}s", d.seconds))
449 .unwrap_or_default(),
450 )
451 }
452 Some(Attributes::TimerFiredEventAttributes(a)) => at(
453 Category::Timer,
454 GroupRef::Opened(a.started_event_id),
455 Role::Closes,
456 )
457 .ends(Outcome::Completed),
458 Some(Attributes::TimerCanceledEventAttributes(a)) => at(
459 Category::Timer,
460 GroupRef::Opened(a.started_event_id),
461 Role::Closes,
462 )
463 .ends(Outcome::Canceled),
464
465 Some(Attributes::StartChildWorkflowExecutionInitiatedEventAttributes(a)) => {
466 at(Category::ChildWorkflow, GroupRef::Opened(id), Role::Opens)
467 .subject(a.workflow_type.map(|t| t.name).unwrap_or_default())
468 .field("workflowId", a.workflow_id)
469 .field("namespace", a.namespace)
470 .args("input", a.input)
471 }
472 Some(Attributes::StartChildWorkflowExecutionFailedEventAttributes(a)) => at(
473 Category::ChildWorkflow,
474 GroupRef::Opened(a.initiated_event_id),
475 Role::Closes,
476 )
477 .ends(Outcome::Failed)
478 .field("workflowId", a.workflow_id),
479 Some(Attributes::ChildWorkflowExecutionStartedEventAttributes(a)) => at(
480 Category::ChildWorkflow,
481 GroupRef::Opened(a.initiated_event_id),
482 Role::Continues,
483 )
484 .field(
485 "runId",
486 a.workflow_execution.map(|w| w.run_id).unwrap_or_default(),
487 ),
488 Some(Attributes::ChildWorkflowExecutionCompletedEventAttributes(a)) => at(
489 Category::ChildWorkflow,
490 GroupRef::Opened(a.initiated_event_id),
491 Role::Closes,
492 )
493 .ends(Outcome::Completed)
494 .args("result", a.result),
495 Some(Attributes::ChildWorkflowExecutionFailedEventAttributes(a)) => at(
496 Category::ChildWorkflow,
497 GroupRef::Opened(a.initiated_event_id),
498 Role::Closes,
499 )
500 .ends(Outcome::Failed)
501 .failed(a.failure),
502 Some(Attributes::ChildWorkflowExecutionCanceledEventAttributes(a)) => at(
503 Category::ChildWorkflow,
504 GroupRef::Opened(a.initiated_event_id),
505 Role::Closes,
506 )
507 .ends(Outcome::Canceled)
508 .args("details", a.details),
509 Some(Attributes::ChildWorkflowExecutionTimedOutEventAttributes(a)) => at(
510 Category::ChildWorkflow,
511 GroupRef::Opened(a.initiated_event_id),
512 Role::Closes,
513 )
514 .ends(Outcome::TimedOut),
515 Some(Attributes::ChildWorkflowExecutionTerminatedEventAttributes(a)) => at(
516 Category::ChildWorkflow,
517 GroupRef::Opened(a.initiated_event_id),
518 Role::Closes,
519 )
520 .ends(Outcome::Terminated),
521
522 Some(Attributes::SignalExternalWorkflowExecutionInitiatedEventAttributes(a)) => at(
523 Category::ExternalWorkflow,
524 GroupRef::Opened(id),
525 Role::Opens,
526 )
527 .subject(a.signal_name)
528 .field(
529 "workflowId",
530 a.workflow_execution
531 .map(|w| w.workflow_id)
532 .unwrap_or_default(),
533 )
534 .field("namespace", a.namespace)
535 .args("input", a.input),
536 Some(Attributes::SignalExternalWorkflowExecutionFailedEventAttributes(a)) => at(
537 Category::ExternalWorkflow,
538 GroupRef::Opened(a.initiated_event_id),
539 Role::Closes,
540 )
541 .ends(Outcome::Failed)
542 .field("cause", a.cause.to_string()),
543 Some(Attributes::ExternalWorkflowExecutionSignaledEventAttributes(a)) => at(
544 Category::ExternalWorkflow,
545 GroupRef::Opened(a.initiated_event_id),
546 Role::Closes,
547 )
548 .ends(Outcome::Completed),
549 Some(Attributes::RequestCancelExternalWorkflowExecutionInitiatedEventAttributes(a)) => at(
550 Category::ExternalWorkflow,
551 GroupRef::Opened(id),
552 Role::Opens,
553 )
554 .subject("cancel")
555 .field(
556 "workflowId",
557 a.workflow_execution
558 .map(|w| w.workflow_id)
559 .unwrap_or_default(),
560 ),
561 Some(Attributes::RequestCancelExternalWorkflowExecutionFailedEventAttributes(a)) => at(
562 Category::ExternalWorkflow,
563 GroupRef::Opened(a.initiated_event_id),
564 Role::Closes,
565 )
566 .ends(Outcome::Failed)
567 .field("cause", a.cause.to_string()),
568 Some(Attributes::ExternalWorkflowExecutionCancelRequestedEventAttributes(a)) => at(
569 Category::ExternalWorkflow,
570 GroupRef::Opened(a.initiated_event_id),
571 Role::Closes,
572 )
573 .ends(Outcome::Completed),
574
575 Some(Attributes::WorkflowExecutionUpdateAdmittedEventAttributes(_)) => {
576 at(Category::Update, GroupRef::Opened(id), Role::Opens)
577 }
578 Some(Attributes::WorkflowExecutionUpdateAcceptedEventAttributes(a)) => {
579 let input = a
581 .accepted_request
582 .and_then(|r| r.input)
583 .and_then(|i| i.args);
584 at(Category::Update, GroupRef::Opened(id), Role::Opens)
585 .subject(a.protocol_instance_id)
586 .args("input", input)
587 }
588 Some(Attributes::WorkflowExecutionUpdateCompletedEventAttributes(a)) => {
589 let m = at(
592 Category::Update,
593 GroupRef::Opened(a.accepted_event_id),
594 Role::Closes,
595 );
596 match a.outcome.and_then(|o| o.value) {
597 Some(OutcomeValue::Success(p)) => {
598 m.ends(Outcome::Completed).args("result", Some(p))
599 }
600 Some(OutcomeValue::Failure(f)) => m.ends(Outcome::Failed).failed(Some(f)),
601 None => m.ends(Outcome::Completed),
602 }
603 }
604 Some(Attributes::WorkflowExecutionUpdateRejectedEventAttributes(a)) => {
605 at(Category::Update, GroupRef::Opened(id), Role::Opens)
607 .subject(a.protocol_instance_id)
608 .ends(Outcome::Rejected)
609 .failed(a.failure)
610 }
611
612 Some(Attributes::NexusOperationScheduledEventAttributes(a)) => {
613 at(Category::Nexus, GroupRef::Opened(id), Role::Opens)
614 .subject(format!("{}/{}", a.service, a.operation))
615 .field("endpoint", a.endpoint)
616 .arg("input", a.input)
617 }
618 Some(Attributes::NexusOperationStartedEventAttributes(a)) => at(
619 Category::Nexus,
620 GroupRef::Opened(a.scheduled_event_id),
621 Role::Continues,
622 ),
623 Some(Attributes::NexusOperationCompletedEventAttributes(a)) => at(
624 Category::Nexus,
625 GroupRef::Opened(a.scheduled_event_id),
626 Role::Closes,
627 )
628 .ends(Outcome::Completed)
629 .arg("result", a.result),
630 Some(Attributes::NexusOperationFailedEventAttributes(a)) => at(
631 Category::Nexus,
632 GroupRef::Opened(a.scheduled_event_id),
633 Role::Closes,
634 )
635 .ends(Outcome::Failed)
636 .failed(a.failure),
637 Some(Attributes::NexusOperationCanceledEventAttributes(a)) => at(
638 Category::Nexus,
639 GroupRef::Opened(a.scheduled_event_id),
640 Role::Closes,
641 )
642 .ends(Outcome::Canceled),
643 Some(Attributes::NexusOperationTimedOutEventAttributes(a)) => at(
644 Category::Nexus,
645 GroupRef::Opened(a.scheduled_event_id),
646 Role::Closes,
647 )
648 .ends(Outcome::TimedOut),
649 Some(Attributes::NexusOperationCancelRequestedEventAttributes(a)) => at(
650 Category::Nexus,
651 GroupRef::Opened(a.scheduled_event_id),
652 Role::Continues,
653 ),
654 Some(Attributes::NexusOperationCancelRequestCompletedEventAttributes(a)) => at(
655 Category::Nexus,
656 GroupRef::Opened(a.scheduled_event_id),
657 Role::Continues,
658 ),
659 Some(Attributes::NexusOperationCancelRequestFailedEventAttributes(a)) => at(
660 Category::Nexus,
661 GroupRef::Opened(a.scheduled_event_id),
662 Role::Continues,
663 )
664 .failed(a.failure),
665
666 Some(Attributes::MarkerRecordedEventAttributes(a)) => {
667 at(Category::Marker, GroupRef::Opened(id), Role::Opens)
668 .subject(a.marker_name)
669 .failed(a.failure)
670 .keyed_args(a.details)
671 }
672 Some(Attributes::UpsertWorkflowSearchAttributesEventAttributes(_)) => at(
673 Category::SearchAttributes,
674 GroupRef::Opened(id),
675 Role::Opens,
676 ),
677 };
678
679 NormalizedEvent {
680 id,
681 time,
682 name,
683 category: m.category,
684 group: m.group,
685 role: m.role,
686 outcome: m.outcome,
687 subject: m.subject,
688 attempt: m.attempt,
689 failure: m.failure,
690 fields: m.fields,
691 payloads: m.payloads,
692 }
693}
694
695fn event_name(proto: &'static str) -> &'static str {
701 proto.strip_prefix("EVENT_TYPE_").unwrap_or(proto)
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use temporalio_common::protos::temporal::api::{
712 common::v1::ActivityType,
713 enums::v1::EventType,
714 history::v1::{
715 ActivityTaskFailedEventAttributes, ActivityTaskScheduledEventAttributes,
716 ActivityTaskStartedEventAttributes, MarkerRecordedEventAttributes,
717 TimerFiredEventAttributes, TimerStartedEventAttributes,
718 WorkflowExecutionCompletedEventAttributes, WorkflowExecutionSignaledEventAttributes,
719 WorkflowExecutionStartedEventAttributes,
720 WorkflowExecutionUpdateCompletedEventAttributes,
721 },
722 update::v1::Outcome as UpdateOutcome,
723 };
724
725 fn event(id: i64, ty: EventType, attrs: Attributes) -> HistoryEvent {
726 HistoryEvent {
727 event_id: id,
728 event_time: Some(prost_wkt_types::Timestamp {
729 seconds: id,
730 nanos: 0,
731 }),
732 event_type: ty as i32,
733 attributes: Some(attrs),
734 ..Default::default()
735 }
736 }
737
738 #[test]
739 fn an_activity_scheduled_event_opens_a_group_at_its_own_id() {
740 let n = normalize(event(
741 5,
742 EventType::ActivityTaskScheduled,
743 Attributes::ActivityTaskScheduledEventAttributes(
744 ActivityTaskScheduledEventAttributes {
745 activity_id: "charge".into(),
746 activity_type: Some(ActivityType {
747 name: "ChargeCard".into(),
748 }),
749 workflow_task_completed_event_id: 4,
752 ..Default::default()
753 },
754 ),
755 ));
756
757 assert_eq!(n.id, 5);
758 assert_eq!(n.group, GroupRef::Opened(5), "not Opened(4)");
759 assert_eq!(n.role, Role::Opens);
760 assert_eq!(n.category, Category::Activity);
761 assert_eq!(n.subject, "ChargeCard");
762 assert_eq!(n.name, "ACTIVITY_TASK_SCHEDULED");
763 assert_eq!(n.time, Some(5_000));
764 assert!(
765 n.fields
766 .iter()
767 .any(|(k, v)| *k == "activityId" && v == "charge")
768 );
769 }
770
771 #[test]
772 fn an_activity_started_event_joins_its_scheduled_group_and_carries_the_attempt() {
773 let n = normalize(event(
774 6,
775 EventType::ActivityTaskStarted,
776 Attributes::ActivityTaskStartedEventAttributes(ActivityTaskStartedEventAttributes {
777 scheduled_event_id: 5,
778 attempt: 3,
779 last_failure: Some(Failure {
780 message: "card declined".into(),
781 ..Default::default()
782 }),
783 ..Default::default()
784 }),
785 ));
786
787 assert_eq!(n.group, GroupRef::Opened(5));
788 assert_eq!(n.role, Role::Continues);
789 assert_eq!(
790 n.attempt,
791 Some(3),
792 "retries do not re-schedule; the count is here"
793 );
794 assert_eq!(n.failure.as_deref(), Some("card declined"));
795 }
796
797 #[test]
798 fn a_failed_activity_closes_its_group_with_the_failure_message() {
799 let n = normalize(event(
800 7,
801 EventType::ActivityTaskFailed,
802 Attributes::ActivityTaskFailedEventAttributes(ActivityTaskFailedEventAttributes {
803 scheduled_event_id: 5,
804 started_event_id: 6,
805 failure: Some(Failure {
806 message: "out of retries".into(),
807 ..Default::default()
808 }),
809 ..Default::default()
810 }),
811 ));
812
813 assert_eq!(n.group, GroupRef::Opened(5));
814 assert_eq!(n.role, Role::Closes);
815 assert_eq!(n.outcome, Outcome::Failed);
816 assert!(n.outcome.is_failure());
817 assert_eq!(n.failure.as_deref(), Some("out of retries"));
818 }
819
820 #[test]
821 fn a_timer_is_grouped_by_its_started_event_not_a_scheduled_one() {
822 let started = normalize(event(
825 10,
826 EventType::TimerStarted,
827 Attributes::TimerStartedEventAttributes(TimerStartedEventAttributes {
828 timer_id: "sleep-1".into(),
829 workflow_task_completed_event_id: 9,
830 ..Default::default()
831 }),
832 ));
833 let fired = normalize(event(
834 11,
835 EventType::TimerFired,
836 Attributes::TimerFiredEventAttributes(TimerFiredEventAttributes {
837 timer_id: "sleep-1".into(),
838 started_event_id: 10,
839 }),
840 ));
841
842 assert_eq!(started.group, GroupRef::Opened(10));
843 assert_eq!(started.subject, "sleep-1");
844 assert_eq!(fired.group, started.group, "the fired timer must join it");
845 assert_eq!(fired.outcome, Outcome::Completed);
846 }
847
848 #[test]
849 fn the_workflow_start_event_belongs_to_the_workflow_group() {
850 let n = normalize(event(
851 1,
852 EventType::WorkflowExecutionStarted,
853 Attributes::WorkflowExecutionStartedEventAttributes(
854 WorkflowExecutionStartedEventAttributes {
855 workflow_type: Some(
856 temporalio_common::protos::temporal::api::common::v1::WorkflowType {
857 name: "OrderWorkflow".into(),
858 },
859 ),
860 attempt: 1,
861 ..Default::default()
862 },
863 ),
864 ));
865
866 assert_eq!(n.group, GroupRef::Workflow);
867 assert_eq!(n.role, Role::Opens);
868 assert_eq!(n.subject, "OrderWorkflow");
869 }
870
871 #[test]
872 fn an_event_with_no_attributes_is_kept_rather_than_dropped() {
873 let mut e = HistoryEvent {
874 event_id: 99,
875 ..Default::default()
876 };
877 e.attributes = None;
878 let n = normalize(e);
879 assert_eq!(n.id, 99);
880 assert_eq!(n.group, GroupRef::Workflow);
881 }
882
883 fn json_payload(body: &str) -> ProtoPayload {
884 ProtoPayload {
885 metadata: [("encoding".to_string(), b"json/plain".to_vec())]
886 .into_iter()
887 .collect(),
888 data: body.as_bytes().to_vec(),
889 external_payloads: Vec::new(),
890 }
891 }
892
893 #[test]
894 fn an_activity_carries_its_input_payload() {
895 let n = normalize(event(
896 5,
897 EventType::ActivityTaskScheduled,
898 Attributes::ActivityTaskScheduledEventAttributes(
899 ActivityTaskScheduledEventAttributes {
900 activity_type: Some(ActivityType {
901 name: "ChargeCard".into(),
902 }),
903 input: Some(Payloads {
904 payloads: vec![json_payload("100")],
905 }),
906 ..Default::default()
907 },
908 ),
909 ));
910
911 assert_eq!(n.payloads.len(), 1);
912 let (label, p) = &n.payloads[0];
913 assert_eq!(label, "input", "a lone argument is not indexed");
914 assert_eq!(p.encoding, "json/plain");
915 assert_eq!(
916 p.render(),
917 tmprl_core::payload::Rendered::Text("100".into())
918 );
919 }
920
921 #[test]
922 fn several_arguments_are_indexed() {
923 let n = normalize(event(
926 5,
927 EventType::ActivityTaskScheduled,
928 Attributes::ActivityTaskScheduledEventAttributes(
929 ActivityTaskScheduledEventAttributes {
930 input: Some(Payloads {
931 payloads: vec![json_payload("1"), json_payload("\"two\"")],
932 }),
933 ..Default::default()
934 },
935 ),
936 ));
937
938 let labels: Vec<&str> = n.payloads.iter().map(|(l, _)| l.as_str()).collect();
939 assert_eq!(labels, ["input[0]", "input[1]"]);
940 }
941
942 #[test]
943 fn an_event_with_no_payloads_carries_none() {
944 let n = normalize(event(
945 6,
946 EventType::ActivityTaskStarted,
947 Attributes::ActivityTaskStartedEventAttributes(ActivityTaskStartedEventAttributes {
948 scheduled_event_id: 5,
949 ..Default::default()
950 }),
951 ));
952 assert!(n.payloads.is_empty());
953 }
954
955 #[test]
956 fn payload_metadata_is_decoded_from_bytes() {
957 let mut raw = json_payload("{}");
960 raw.metadata.insert("type".to_string(), b"Keyword".to_vec());
961 let p = convert(raw);
962 assert_eq!(p.encoding, "json/plain");
963 assert_eq!(p.type_hint.as_deref(), Some("Keyword"));
964 }
965
966 #[test]
967 fn a_workflow_carries_its_own_input() {
968 let n = normalize(event(
971 1,
972 EventType::WorkflowExecutionStarted,
973 Attributes::WorkflowExecutionStartedEventAttributes(
974 WorkflowExecutionStartedEventAttributes {
975 input: Some(Payloads {
976 payloads: vec![json_payload(r#"{"orderId":7}"#)],
977 }),
978 ..Default::default()
979 },
980 ),
981 ));
982
983 let (label, p) = &n.payloads[0];
984 assert_eq!(label, "input");
985 assert_eq!(
986 p.render(),
987 tmprl_core::payload::Rendered::Text("{\n \"orderId\": 7\n}".into())
988 );
989 }
990
991 #[test]
992 fn a_workflow_carries_its_own_result() {
993 let n = normalize(event(
994 11,
995 EventType::WorkflowExecutionCompleted,
996 Attributes::WorkflowExecutionCompletedEventAttributes(
997 WorkflowExecutionCompletedEventAttributes {
998 result: Some(Payloads {
999 payloads: vec![json_payload(r#""ok""#)],
1000 }),
1001 ..Default::default()
1002 },
1003 ),
1004 ));
1005
1006 assert_eq!(n.outcome, Outcome::Completed);
1007 let labels: Vec<&str> = n.payloads.iter().map(|(l, _)| l.as_str()).collect();
1008 assert_eq!(labels, ["result"]);
1009 }
1010
1011 #[test]
1012 fn a_signal_carries_its_argument() {
1013 let n = normalize(event(
1014 8,
1015 EventType::WorkflowExecutionSignaled,
1016 Attributes::WorkflowExecutionSignaledEventAttributes(
1017 WorkflowExecutionSignaledEventAttributes {
1018 signal_name: "approve".into(),
1019 input: Some(Payloads {
1020 payloads: vec![json_payload("true")],
1021 }),
1022 ..Default::default()
1023 },
1024 ),
1025 ));
1026
1027 assert_eq!(n.subject, "approve");
1028 assert_eq!(n.payloads.len(), 1);
1029 }
1030
1031 #[test]
1032 fn a_marker_details_map_is_ordered_by_key() {
1033 let details = HashMap::from([
1036 (
1037 "side-effect-id".to_string(),
1038 Payloads {
1039 payloads: vec![json_payload("1")],
1040 },
1041 ),
1042 (
1043 "data".to_string(),
1044 Payloads {
1045 payloads: vec![json_payload("2")],
1046 },
1047 ),
1048 ]);
1049 let n = normalize(event(
1050 9,
1051 EventType::MarkerRecorded,
1052 Attributes::MarkerRecordedEventAttributes(MarkerRecordedEventAttributes {
1053 marker_name: "SideEffect".into(),
1054 details,
1055 ..Default::default()
1056 }),
1057 ));
1058
1059 let labels: Vec<&str> = n.payloads.iter().map(|(l, _)| l.as_str()).collect();
1060 assert_eq!(labels, ["data", "side-effect-id"]);
1061 }
1062
1063 #[test]
1064 fn an_update_that_the_workflow_rejected_is_not_reported_as_completed() {
1065 let n = normalize(event(
1068 12,
1069 EventType::WorkflowExecutionUpdateCompleted,
1070 Attributes::WorkflowExecutionUpdateCompletedEventAttributes(
1071 WorkflowExecutionUpdateCompletedEventAttributes {
1072 outcome: Some(UpdateOutcome {
1073 value: Some(OutcomeValue::Failure(Failure {
1074 message: "not allowed in this state".into(),
1075 ..Default::default()
1076 })),
1077 }),
1078 ..Default::default()
1079 },
1080 ),
1081 ));
1082
1083 assert_eq!(n.outcome, Outcome::Failed);
1084 assert_eq!(n.failure.as_deref(), Some("not allowed in this state"));
1085 assert!(n.payloads.is_empty());
1086 }
1087
1088 #[test]
1089 fn an_update_that_succeeded_carries_its_result() {
1090 let n = normalize(event(
1091 12,
1092 EventType::WorkflowExecutionUpdateCompleted,
1093 Attributes::WorkflowExecutionUpdateCompletedEventAttributes(
1094 WorkflowExecutionUpdateCompletedEventAttributes {
1095 outcome: Some(UpdateOutcome {
1096 value: Some(OutcomeValue::Success(Payloads {
1097 payloads: vec![json_payload("42")],
1098 })),
1099 }),
1100 ..Default::default()
1101 },
1102 ),
1103 ));
1104
1105 assert_eq!(n.outcome, Outcome::Completed);
1106 let labels: Vec<&str> = n.payloads.iter().map(|(l, _)| l.as_str()).collect();
1107 assert_eq!(labels, ["result"]);
1108 }
1109
1110 #[test]
1111 fn a_page_knows_whether_more_exist() {
1112 assert!(!HistoryPage::default().has_more());
1113 assert!(
1114 HistoryPage {
1115 next_page_token: vec![1],
1116 ..Default::default()
1117 }
1118 .has_more()
1119 );
1120 }
1121}