1use crate::entities::{Artifact, Note, Provenance, Scope, Trajectory, TrajectoryOutcome};
24use crate::event::{Event, EventKind};
25use crate::*;
26
27pub trait EventSourced: Sized {
29 fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self>;
34
35 fn apply_event(&mut self, event: &Event<serde_json::Value>);
37
38 fn relevant_event_kinds() -> &'static [EventKind];
40}
41
42fn sorted_relevant_events<'a>(
44 events: &'a [Event<serde_json::Value>],
45 relevant: &[EventKind],
46) -> Vec<&'a Event<serde_json::Value>> {
47 let mut filtered: Vec<&Event<serde_json::Value>> = events
48 .iter()
49 .filter(|e| relevant.contains(&e.header.event_kind))
50 .collect();
51 filtered.sort_by_key(|e| e.header.timestamp);
52 filtered
53}
54
55fn extract_id<T: identity::EntityIdType>(payload: &serde_json::Value, field: &str) -> Option<T> {
61 payload
62 .get(field)
63 .and_then(|v| serde_json::from_value(v.clone()).ok())
64}
65
66fn extract_string(payload: &serde_json::Value, field: &str) -> Option<String> {
68 payload
69 .get(field)
70 .and_then(|v| v.as_str())
71 .map(|s| s.to_string())
72}
73
74fn extract_optional_string(payload: &serde_json::Value, field: &str) -> Option<String> {
76 payload.get(field).and_then(|v| {
77 if v.is_null() {
78 None
79 } else {
80 v.as_str().map(|s| s.to_string())
81 }
82 })
83}
84
85fn extract_i32(payload: &serde_json::Value, field: &str, default: i32) -> i32 {
87 payload
88 .get(field)
89 .and_then(|v| v.as_i64())
90 .map(|v| v as i32)
91 .unwrap_or(default)
92}
93
94fn extract_optional_i32(payload: &serde_json::Value, field: &str) -> Option<i32> {
96 payload
97 .get(field)
98 .and_then(|v| v.as_i64())
99 .map(|v| v as i32)
100}
101
102fn timestamp_to_datetime(ts: i64) -> Timestamp {
104 chrono::DateTime::from_timestamp_micros(ts).unwrap_or_else(chrono::Utc::now)
105}
106
107impl EventSourced for Trajectory {
112 fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self> {
113 let relevant = sorted_relevant_events(events, Self::relevant_event_kinds());
114
115 let create_event = relevant
117 .iter()
118 .find(|e| e.header.event_kind == EventKind::TRAJECTORY_CREATED)?;
119
120 let payload = &create_event.payload;
121 let trajectory_id = extract_id::<TrajectoryId>(payload, "trajectory_id")?;
122 let name = extract_string(payload, "name").unwrap_or_default();
123 let created_at = timestamp_to_datetime(create_event.header.timestamp);
124
125 let mut trajectory = Trajectory {
126 trajectory_id,
127 name,
128 description: extract_optional_string(payload, "description"),
129 status: TrajectoryStatus::Active,
130 parent_trajectory_id: extract_id(payload, "parent_trajectory_id"),
131 root_trajectory_id: extract_id(payload, "root_trajectory_id"),
132 agent_id: extract_id(payload, "agent_id"),
133 created_at,
134 updated_at: created_at,
135 completed_at: None,
136 outcome: None,
137 metadata: payload.get("metadata").cloned(),
138 };
139
140 for event in &relevant {
142 if event.header.event_kind != EventKind::TRAJECTORY_CREATED {
143 trajectory.apply_event(event);
144 }
145 }
146
147 Some(trajectory)
149 }
150
151 fn apply_event(&mut self, event: &Event<serde_json::Value>) {
152 let payload = &event.payload;
153 let ts = timestamp_to_datetime(event.header.timestamp);
154
155 match event.header.event_kind {
156 EventKind::TRAJECTORY_UPDATED => {
157 if let Some(name) = extract_string(payload, "name") {
158 self.name = name;
159 }
160 if let Some(desc) = payload.get("description") {
161 self.description = if desc.is_null() {
162 None
163 } else {
164 desc.as_str().map(|s| s.to_string())
165 };
166 }
167 if let Some(status) = payload
168 .get("status")
169 .and_then(|v| serde_json::from_value(v.clone()).ok())
170 {
171 self.status = status;
172 }
173 if let Some(meta) = payload.get("metadata") {
174 self.metadata = Some(meta.clone());
175 }
176 self.updated_at = ts;
177 }
178 EventKind::TRAJECTORY_COMPLETED => {
179 self.status = TrajectoryStatus::Completed;
180 self.completed_at = Some(ts);
181 self.updated_at = ts;
182
183 if let Some(outcome_val) = payload.get("outcome") {
185 if let Ok(outcome) =
186 serde_json::from_value::<TrajectoryOutcome>(outcome_val.clone())
187 {
188 self.outcome = Some(outcome);
189 }
190 }
191 }
192 EventKind::TRAJECTORY_FAILED => {
193 self.status = TrajectoryStatus::Failed;
194 self.completed_at = Some(ts);
195 self.updated_at = ts;
196
197 if let Some(error) = extract_string(payload, "error") {
199 self.outcome = Some(TrajectoryOutcome {
200 status: OutcomeStatus::Failure,
201 summary: error,
202 produced_artifacts: vec![],
203 produced_notes: vec![],
204 error: extract_optional_string(payload, "error"),
205 });
206 }
207 }
208 EventKind::TRAJECTORY_SUSPENDED => {
209 self.status = TrajectoryStatus::Suspended;
210 self.updated_at = ts;
211 }
212 EventKind::TRAJECTORY_RESUMED => {
213 self.status = TrajectoryStatus::Active;
214 self.updated_at = ts;
215 }
216 EventKind::TRAJECTORY_DELETED => {
217 self.completed_at = Some(ts);
220 self.updated_at = ts;
221 }
222 _ => {} }
224 }
225
226 fn relevant_event_kinds() -> &'static [EventKind] {
227 &[
228 EventKind::TRAJECTORY_CREATED,
229 EventKind::TRAJECTORY_UPDATED,
230 EventKind::TRAJECTORY_COMPLETED,
231 EventKind::TRAJECTORY_FAILED,
232 EventKind::TRAJECTORY_SUSPENDED,
233 EventKind::TRAJECTORY_RESUMED,
234 EventKind::TRAJECTORY_DELETED,
235 ]
236 }
237}
238
239impl EventSourced for Scope {
244 fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self> {
245 let relevant = sorted_relevant_events(events, Self::relevant_event_kinds());
246
247 let create_event = relevant
248 .iter()
249 .find(|e| e.header.event_kind == EventKind::SCOPE_CREATED)?;
250
251 let payload = &create_event.payload;
252 let scope_id = extract_id::<ScopeId>(payload, "scope_id")?;
253 let trajectory_id = extract_id::<TrajectoryId>(payload, "trajectory_id")?;
254 let created_at = timestamp_to_datetime(create_event.header.timestamp);
255
256 let mut scope = Scope {
257 scope_id,
258 trajectory_id,
259 parent_scope_id: extract_id(payload, "parent_scope_id"),
260 name: extract_string(payload, "name").unwrap_or_default(),
261 purpose: extract_optional_string(payload, "purpose"),
262 is_active: true,
263 created_at,
264 closed_at: None,
265 checkpoint: None,
266 token_budget: extract_i32(payload, "token_budget", 8000),
267 tokens_used: extract_i32(payload, "tokens_used", 0),
268 metadata: payload.get("metadata").cloned(),
269 };
270
271 for event in &relevant {
272 if event.header.event_kind != EventKind::SCOPE_CREATED {
273 scope.apply_event(event);
274 }
275 }
276
277 Some(scope)
278 }
279
280 fn apply_event(&mut self, event: &Event<serde_json::Value>) {
281 let payload = &event.payload;
282 let ts = timestamp_to_datetime(event.header.timestamp);
283
284 match event.header.event_kind {
285 EventKind::SCOPE_UPDATED => {
286 if let Some(name) = extract_string(payload, "name") {
287 self.name = name;
288 }
289 if let Some(purpose) = payload.get("purpose") {
290 self.purpose = if purpose.is_null() {
291 None
292 } else {
293 purpose.as_str().map(|s| s.to_string())
294 };
295 }
296 if let Some(budget) = extract_optional_i32(payload, "token_budget") {
297 self.token_budget = budget;
298 }
299 if let Some(used) = extract_optional_i32(payload, "tokens_used") {
300 self.tokens_used = used;
301 }
302 if let Some(meta) = payload.get("metadata") {
303 self.metadata = Some(meta.clone());
304 }
305 }
306 EventKind::SCOPE_CLOSED => {
307 self.is_active = false;
308 self.closed_at = Some(ts);
309 }
310 EventKind::SCOPE_CHECKPOINTED => {
311 if let Some(cp_val) = payload.get("checkpoint") {
313 if let Ok(cp) = serde_json::from_value(cp_val.clone()) {
314 self.checkpoint = Some(cp);
315 }
316 }
317 }
318 _ => {} }
320 }
321
322 fn relevant_event_kinds() -> &'static [EventKind] {
323 &[
324 EventKind::SCOPE_CREATED,
325 EventKind::SCOPE_UPDATED,
326 EventKind::SCOPE_CLOSED,
327 EventKind::SCOPE_CHECKPOINTED,
328 ]
329 }
330}
331
332impl EventSourced for Artifact {
337 fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self> {
338 let relevant = sorted_relevant_events(events, Self::relevant_event_kinds());
339
340 let create_event = relevant
341 .iter()
342 .find(|e| e.header.event_kind == EventKind::ARTIFACT_CREATED)?;
343
344 let payload = &create_event.payload;
345 let artifact_id = extract_id::<ArtifactId>(payload, "artifact_id")?;
346 let trajectory_id = extract_id::<TrajectoryId>(payload, "trajectory_id")?;
347 let scope_id = extract_id::<ScopeId>(payload, "scope_id")?;
348 let created_at = timestamp_to_datetime(create_event.header.timestamp);
349
350 let artifact_type = payload
351 .get("artifact_type")
352 .and_then(|v| serde_json::from_value(v.clone()).ok())
353 .unwrap_or(ArtifactType::Custom);
354
355 let content_hash: ContentHash = payload
356 .get("content_hash")
357 .and_then(|v| serde_json::from_value(v.clone()).ok())
358 .unwrap_or_default();
359
360 let provenance = payload
361 .get("provenance")
362 .and_then(|v| serde_json::from_value(v.clone()).ok())
363 .unwrap_or(Provenance {
364 source_turn: 0,
365 extraction_method: ExtractionMethod::Unknown,
366 confidence: None,
367 });
368
369 let ttl = payload
370 .get("ttl")
371 .and_then(|v| serde_json::from_value(v.clone()).ok())
372 .unwrap_or(TTL::Persistent);
373
374 let mut artifact = Artifact {
375 artifact_id,
376 trajectory_id,
377 scope_id,
378 artifact_type,
379 name: extract_string(payload, "name").unwrap_or_default(),
380 content: extract_string(payload, "content").unwrap_or_default(),
381 content_hash,
382 embedding: None,
383 provenance,
384 ttl,
385 created_at,
386 updated_at: created_at,
387 superseded_by: None,
388 metadata: payload.get("metadata").cloned(),
389 };
390
391 let mut deleted = false;
393
394 for event in &relevant {
395 if event.header.event_kind == EventKind::ARTIFACT_CREATED {
396 continue;
397 }
398 if event.header.event_kind == EventKind::ARTIFACT_DELETED {
399 deleted = true;
400 }
401 artifact.apply_event(event);
402 }
403
404 if deleted {
405 None
406 } else {
407 Some(artifact)
408 }
409 }
410
411 fn apply_event(&mut self, event: &Event<serde_json::Value>) {
412 let payload = &event.payload;
413 let ts = timestamp_to_datetime(event.header.timestamp);
414
415 match event.header.event_kind {
416 EventKind::ARTIFACT_UPDATED => {
417 if let Some(name) = extract_string(payload, "name") {
418 self.name = name;
419 }
420 if let Some(content) = extract_string(payload, "content") {
421 self.content = content;
422 }
423 if let Some(hash) = payload
424 .get("content_hash")
425 .and_then(|v| serde_json::from_value(v.clone()).ok())
426 {
427 self.content_hash = hash;
428 }
429 if let Some(meta) = payload.get("metadata") {
430 self.metadata = Some(meta.clone());
431 }
432 self.updated_at = ts;
433 }
434 EventKind::ARTIFACT_SUPERSEDED => {
435 self.superseded_by = extract_id(payload, "superseded_by");
436 self.updated_at = ts;
437 }
438 EventKind::ARTIFACT_DELETED => {
439 self.updated_at = ts;
441 }
442 _ => {} }
444 }
445
446 fn relevant_event_kinds() -> &'static [EventKind] {
447 &[
448 EventKind::ARTIFACT_CREATED,
449 EventKind::ARTIFACT_UPDATED,
450 EventKind::ARTIFACT_SUPERSEDED,
451 EventKind::ARTIFACT_DELETED,
452 ]
453 }
454}
455
456impl EventSourced for Note {
461 fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self> {
462 let relevant = sorted_relevant_events(events, Self::relevant_event_kinds());
463
464 let create_event = relevant
465 .iter()
466 .find(|e| e.header.event_kind == EventKind::NOTE_CREATED)?;
467
468 let payload = &create_event.payload;
469 let note_id = extract_id::<NoteId>(payload, "note_id")?;
470 let created_at = timestamp_to_datetime(create_event.header.timestamp);
471
472 let note_type = payload
473 .get("note_type")
474 .and_then(|v| serde_json::from_value(v.clone()).ok())
475 .unwrap_or(NoteType::Insight);
476
477 let content_hash: ContentHash = payload
478 .get("content_hash")
479 .and_then(|v| serde_json::from_value(v.clone()).ok())
480 .unwrap_or_default();
481
482 let ttl = payload
483 .get("ttl")
484 .and_then(|v| serde_json::from_value(v.clone()).ok())
485 .unwrap_or(TTL::Persistent);
486
487 let abstraction_level = payload
488 .get("abstraction_level")
489 .and_then(|v| serde_json::from_value(v.clone()).ok())
490 .unwrap_or(AbstractionLevel::Raw);
491
492 let source_trajectory_ids: Vec<TrajectoryId> = payload
493 .get("source_trajectory_ids")
494 .and_then(|v| serde_json::from_value(v.clone()).ok())
495 .unwrap_or_default();
496
497 let source_artifact_ids: Vec<ArtifactId> = payload
498 .get("source_artifact_ids")
499 .and_then(|v| serde_json::from_value(v.clone()).ok())
500 .unwrap_or_default();
501
502 let source_note_ids: Vec<NoteId> = payload
503 .get("source_note_ids")
504 .and_then(|v| serde_json::from_value(v.clone()).ok())
505 .unwrap_or_default();
506
507 let mut note = Note {
508 note_id,
509 note_type,
510 title: extract_string(payload, "title").unwrap_or_default(),
511 content: extract_string(payload, "content").unwrap_or_default(),
512 content_hash,
513 embedding: None,
514 source_trajectory_ids,
515 source_artifact_ids,
516 ttl,
517 created_at,
518 updated_at: created_at,
519 accessed_at: created_at,
520 access_count: 0,
521 superseded_by: None,
522 metadata: payload.get("metadata").cloned(),
523 abstraction_level,
524 source_note_ids,
525 };
526
527 let mut deleted = false;
528
529 for event in &relevant {
530 if event.header.event_kind == EventKind::NOTE_CREATED {
531 continue;
532 }
533 if event.header.event_kind == EventKind::NOTE_DELETED {
534 deleted = true;
535 }
536 note.apply_event(event);
537 }
538
539 if deleted {
540 None
541 } else {
542 Some(note)
543 }
544 }
545
546 fn apply_event(&mut self, event: &Event<serde_json::Value>) {
547 let payload = &event.payload;
548 let ts = timestamp_to_datetime(event.header.timestamp);
549
550 match event.header.event_kind {
551 EventKind::NOTE_UPDATED => {
552 if let Some(title) = extract_string(payload, "title") {
553 self.title = title;
554 }
555 if let Some(content) = extract_string(payload, "content") {
556 self.content = content;
557 }
558 if let Some(hash) = payload
559 .get("content_hash")
560 .and_then(|v| serde_json::from_value(v.clone()).ok())
561 {
562 self.content_hash = hash;
563 }
564 if let Some(meta) = payload.get("metadata") {
565 self.metadata = Some(meta.clone());
566 }
567 self.updated_at = ts;
568 }
569 EventKind::NOTE_SUPERSEDED => {
570 self.superseded_by = extract_id(payload, "superseded_by");
571 self.updated_at = ts;
572 }
573 EventKind::NOTE_ACCESSED => {
574 self.accessed_at = ts;
575 self.access_count += 1;
576 }
577 EventKind::NOTE_DELETED => {
578 self.updated_at = ts;
579 }
580 _ => {} }
582 }
583
584 fn relevant_event_kinds() -> &'static [EventKind] {
585 &[
586 EventKind::NOTE_CREATED,
587 EventKind::NOTE_UPDATED,
588 EventKind::NOTE_SUPERSEDED,
589 EventKind::NOTE_ACCESSED,
590 EventKind::NOTE_DELETED,
591 ]
592 }
593}
594
595#[cfg(test)]
600mod tests {
601 use super::*;
602 use crate::event::{DagPosition, EventFlags, EventHeader};
603 use crate::EventId;
604 use serde_json::json;
605
606 fn make_event(
608 event_kind: EventKind,
609 timestamp_micros: i64,
610 payload: serde_json::Value,
611 ) -> Event<serde_json::Value> {
612 Event::new(
613 EventHeader::new(
614 EventId::now_v7(),
615 EventId::now_v7(),
616 timestamp_micros,
617 DagPosition::root(),
618 0,
619 event_kind,
620 EventFlags::empty(),
621 None,
622 ),
623 payload,
624 )
625 }
626
627 fn base_ts() -> i64 {
629 1_735_689_600_000_000
630 }
631
632 #[test]
637 fn test_trajectory_from_events_empty() {
638 let events: Vec<Event<serde_json::Value>> = vec![];
639 assert!(Trajectory::from_events(&events).is_none());
640 }
641
642 #[test]
643 fn test_trajectory_from_events_missing_creation() {
644 let events = vec![make_event(
645 EventKind::TRAJECTORY_UPDATED,
646 base_ts(),
647 json!({"trajectory_id": TrajectoryId::now_v7(), "name": "updated"}),
648 )];
649 assert!(Trajectory::from_events(&events).is_none());
650 }
651
652 #[test]
653 fn test_trajectory_from_creation_event() {
654 let tid = TrajectoryId::now_v7();
655 let events = vec![make_event(
656 EventKind::TRAJECTORY_CREATED,
657 base_ts(),
658 json!({
659 "trajectory_id": tid,
660 "name": "Test Trajectory",
661 "description": "A test",
662 }),
663 )];
664
665 let trajectory = Trajectory::from_events(&events).unwrap();
666 assert_eq!(trajectory.trajectory_id, tid);
667 assert_eq!(trajectory.name, "Test Trajectory");
668 assert_eq!(trajectory.description.as_deref(), Some("A test"));
669 assert_eq!(trajectory.status, TrajectoryStatus::Active);
670 assert!(trajectory.completed_at.is_none());
671 }
672
673 #[test]
674 fn test_trajectory_updated() {
675 let tid = TrajectoryId::now_v7();
676 let events = vec![
677 make_event(
678 EventKind::TRAJECTORY_CREATED,
679 base_ts(),
680 json!({
681 "trajectory_id": tid,
682 "name": "Original",
683 }),
684 ),
685 make_event(
686 EventKind::TRAJECTORY_UPDATED,
687 base_ts() + 1_000_000,
688 json!({
689 "name": "Renamed",
690 "description": "Now has a description",
691 }),
692 ),
693 ];
694
695 let trajectory = Trajectory::from_events(&events).unwrap();
696 assert_eq!(trajectory.name, "Renamed");
697 assert_eq!(
698 trajectory.description.as_deref(),
699 Some("Now has a description")
700 );
701 }
702
703 #[test]
704 fn test_trajectory_completed() {
705 let tid = TrajectoryId::now_v7();
706 let events = vec![
707 make_event(
708 EventKind::TRAJECTORY_CREATED,
709 base_ts(),
710 json!({ "trajectory_id": tid, "name": "Task" }),
711 ),
712 make_event(
713 EventKind::TRAJECTORY_COMPLETED,
714 base_ts() + 2_000_000,
715 json!({
716 "outcome": {
717 "status": "success",
718 "summary": "Done",
719 "produced_artifacts": [],
720 "produced_notes": [],
721 "error": null,
722 }
723 }),
724 ),
725 ];
726
727 let trajectory = Trajectory::from_events(&events).unwrap();
728 assert_eq!(trajectory.status, TrajectoryStatus::Completed);
729 assert!(trajectory.completed_at.is_some());
730 assert!(trajectory.outcome.is_some());
731 assert_eq!(trajectory.outcome.unwrap().summary, "Done");
732 }
733
734 #[test]
735 fn test_trajectory_failed() {
736 let tid = TrajectoryId::now_v7();
737 let events = vec![
738 make_event(
739 EventKind::TRAJECTORY_CREATED,
740 base_ts(),
741 json!({ "trajectory_id": tid, "name": "Failing Task" }),
742 ),
743 make_event(
744 EventKind::TRAJECTORY_FAILED,
745 base_ts() + 1_000_000,
746 json!({ "error": "Something went wrong" }),
747 ),
748 ];
749
750 let trajectory = Trajectory::from_events(&events).unwrap();
751 assert_eq!(trajectory.status, TrajectoryStatus::Failed);
752 assert!(trajectory.completed_at.is_some());
753 assert!(trajectory.outcome.is_some());
754 }
755
756 #[test]
757 fn test_trajectory_out_of_order_events() {
758 let tid = TrajectoryId::now_v7();
759 let events = vec![
761 make_event(
762 EventKind::TRAJECTORY_COMPLETED,
763 base_ts() + 2_000_000,
764 json!({}),
765 ),
766 make_event(
767 EventKind::TRAJECTORY_CREATED,
768 base_ts(),
769 json!({ "trajectory_id": tid, "name": "Out of order" }),
770 ),
771 make_event(
772 EventKind::TRAJECTORY_UPDATED,
773 base_ts() + 1_000_000,
774 json!({ "name": "Updated name" }),
775 ),
776 ];
777
778 let trajectory = Trajectory::from_events(&events).unwrap();
779 assert_eq!(trajectory.status, TrajectoryStatus::Completed);
781 assert_eq!(trajectory.name, "Updated name");
782 }
783
784 #[test]
785 fn test_trajectory_ignores_irrelevant_events() {
786 let tid = TrajectoryId::now_v7();
787 let events = vec![
788 make_event(
789 EventKind::TRAJECTORY_CREATED,
790 base_ts(),
791 json!({ "trajectory_id": tid, "name": "Test" }),
792 ),
793 make_event(
794 EventKind::SCOPE_CREATED,
795 base_ts() + 500_000,
796 json!({ "scope_id": ScopeId::now_v7() }),
797 ),
798 make_event(
799 EventKind::SYSTEM_HEARTBEAT,
800 base_ts() + 1_000_000,
801 json!({}),
802 ),
803 ];
804
805 let trajectory = Trajectory::from_events(&events).unwrap();
806 assert_eq!(trajectory.name, "Test");
807 assert_eq!(trajectory.status, TrajectoryStatus::Active);
808 }
809
810 #[test]
811 fn test_trajectory_suspend_resume() {
812 let tid = TrajectoryId::now_v7();
813 let events = vec![
814 make_event(
815 EventKind::TRAJECTORY_CREATED,
816 base_ts(),
817 json!({ "trajectory_id": tid, "name": "Suspendable" }),
818 ),
819 make_event(
820 EventKind::TRAJECTORY_SUSPENDED,
821 base_ts() + 1_000_000,
822 json!({}),
823 ),
824 make_event(
825 EventKind::TRAJECTORY_RESUMED,
826 base_ts() + 2_000_000,
827 json!({}),
828 ),
829 ];
830
831 let trajectory = Trajectory::from_events(&events).unwrap();
832 assert_eq!(trajectory.status, TrajectoryStatus::Active);
833 }
834
835 #[test]
840 fn test_scope_from_events_empty() {
841 let events: Vec<Event<serde_json::Value>> = vec![];
842 assert!(Scope::from_events(&events).is_none());
843 }
844
845 #[test]
846 fn test_scope_from_creation_event() {
847 let sid = ScopeId::now_v7();
848 let tid = TrajectoryId::now_v7();
849 let events = vec![make_event(
850 EventKind::SCOPE_CREATED,
851 base_ts(),
852 json!({
853 "scope_id": sid,
854 "trajectory_id": tid,
855 "name": "Main Scope",
856 "purpose": "Primary workspace",
857 "token_budget": 4096,
858 }),
859 )];
860
861 let scope = Scope::from_events(&events).unwrap();
862 assert_eq!(scope.scope_id, sid);
863 assert_eq!(scope.trajectory_id, tid);
864 assert_eq!(scope.name, "Main Scope");
865 assert_eq!(scope.purpose.as_deref(), Some("Primary workspace"));
866 assert!(scope.is_active);
867 assert!(scope.closed_at.is_none());
868 assert_eq!(scope.token_budget, 4096);
869 }
870
871 #[test]
872 fn test_scope_updated_then_closed() {
873 let sid = ScopeId::now_v7();
874 let tid = TrajectoryId::now_v7();
875 let events = vec![
876 make_event(
877 EventKind::SCOPE_CREATED,
878 base_ts(),
879 json!({
880 "scope_id": sid,
881 "trajectory_id": tid,
882 "name": "Scope",
883 "token_budget": 4096,
884 }),
885 ),
886 make_event(
887 EventKind::SCOPE_UPDATED,
888 base_ts() + 1_000_000,
889 json!({
890 "name": "Updated Scope",
891 "tokens_used": 1024,
892 }),
893 ),
894 make_event(EventKind::SCOPE_CLOSED, base_ts() + 2_000_000, json!({})),
895 ];
896
897 let scope = Scope::from_events(&events).unwrap();
898 assert_eq!(scope.name, "Updated Scope");
899 assert_eq!(scope.tokens_used, 1024);
900 assert!(!scope.is_active);
901 assert!(scope.closed_at.is_some());
902 }
903
904 #[test]
905 fn test_scope_out_of_order() {
906 let sid = ScopeId::now_v7();
907 let tid = TrajectoryId::now_v7();
908 let events = vec![
909 make_event(EventKind::SCOPE_CLOSED, base_ts() + 2_000_000, json!({})),
910 make_event(
911 EventKind::SCOPE_CREATED,
912 base_ts(),
913 json!({
914 "scope_id": sid,
915 "trajectory_id": tid,
916 "name": "Scope",
917 }),
918 ),
919 ];
920
921 let scope = Scope::from_events(&events).unwrap();
922 assert!(!scope.is_active);
923 assert!(scope.closed_at.is_some());
924 }
925
926 #[test]
931 fn test_artifact_from_events_empty() {
932 let events: Vec<Event<serde_json::Value>> = vec![];
933 assert!(Artifact::from_events(&events).is_none());
934 }
935
936 #[test]
937 fn test_artifact_from_creation_event() {
938 let aid = ArtifactId::now_v7();
939 let tid = TrajectoryId::now_v7();
940 let sid = ScopeId::now_v7();
941 let events = vec![make_event(
942 EventKind::ARTIFACT_CREATED,
943 base_ts(),
944 json!({
945 "artifact_id": aid,
946 "trajectory_id": tid,
947 "scope_id": sid,
948 "artifact_type": "fact",
949 "name": "Test Artifact",
950 "content": "Some content",
951 "ttl": "persistent",
952 }),
953 )];
954
955 let artifact = Artifact::from_events(&events).unwrap();
956 assert_eq!(artifact.artifact_id, aid);
957 assert_eq!(artifact.trajectory_id, tid);
958 assert_eq!(artifact.scope_id, sid);
959 assert_eq!(artifact.name, "Test Artifact");
960 assert_eq!(artifact.content, "Some content");
961 assert_eq!(artifact.artifact_type, ArtifactType::Fact);
962 }
963
964 #[test]
965 fn test_artifact_updated() {
966 let aid = ArtifactId::now_v7();
967 let tid = TrajectoryId::now_v7();
968 let sid = ScopeId::now_v7();
969 let events = vec![
970 make_event(
971 EventKind::ARTIFACT_CREATED,
972 base_ts(),
973 json!({
974 "artifact_id": aid,
975 "trajectory_id": tid,
976 "scope_id": sid,
977 "name": "Original",
978 "content": "Original content",
979 }),
980 ),
981 make_event(
982 EventKind::ARTIFACT_UPDATED,
983 base_ts() + 1_000_000,
984 json!({
985 "name": "Updated",
986 "content": "New content",
987 }),
988 ),
989 ];
990
991 let artifact = Artifact::from_events(&events).unwrap();
992 assert_eq!(artifact.name, "Updated");
993 assert_eq!(artifact.content, "New content");
994 }
995
996 #[test]
997 fn test_artifact_deleted_returns_none() {
998 let aid = ArtifactId::now_v7();
999 let tid = TrajectoryId::now_v7();
1000 let sid = ScopeId::now_v7();
1001 let events = vec![
1002 make_event(
1003 EventKind::ARTIFACT_CREATED,
1004 base_ts(),
1005 json!({
1006 "artifact_id": aid,
1007 "trajectory_id": tid,
1008 "scope_id": sid,
1009 "name": "Doomed",
1010 "content": "Will be deleted",
1011 }),
1012 ),
1013 make_event(
1014 EventKind::ARTIFACT_DELETED,
1015 base_ts() + 1_000_000,
1016 json!({}),
1017 ),
1018 ];
1019
1020 assert!(Artifact::from_events(&events).is_none());
1021 }
1022
1023 #[test]
1024 fn test_artifact_superseded() {
1025 let aid = ArtifactId::now_v7();
1026 let new_aid = ArtifactId::now_v7();
1027 let tid = TrajectoryId::now_v7();
1028 let sid = ScopeId::now_v7();
1029 let events = vec![
1030 make_event(
1031 EventKind::ARTIFACT_CREATED,
1032 base_ts(),
1033 json!({
1034 "artifact_id": aid,
1035 "trajectory_id": tid,
1036 "scope_id": sid,
1037 "name": "V1",
1038 "content": "Version 1",
1039 }),
1040 ),
1041 make_event(
1042 EventKind::ARTIFACT_SUPERSEDED,
1043 base_ts() + 1_000_000,
1044 json!({ "superseded_by": new_aid }),
1045 ),
1046 ];
1047
1048 let artifact = Artifact::from_events(&events).unwrap();
1049 assert_eq!(artifact.superseded_by, Some(new_aid));
1050 }
1051
1052 #[test]
1057 fn test_note_from_events_empty() {
1058 let events: Vec<Event<serde_json::Value>> = vec![];
1059 assert!(Note::from_events(&events).is_none());
1060 }
1061
1062 #[test]
1063 fn test_note_from_creation_event() {
1064 let nid = NoteId::now_v7();
1065 let events = vec![make_event(
1066 EventKind::NOTE_CREATED,
1067 base_ts(),
1068 json!({
1069 "note_id": nid,
1070 "note_type": "insight",
1071 "title": "Test Note",
1072 "content": "Some insight",
1073 "abstraction_level": "raw",
1074 }),
1075 )];
1076
1077 let note = Note::from_events(&events).unwrap();
1078 assert_eq!(note.note_id, nid);
1079 assert_eq!(note.title, "Test Note");
1080 assert_eq!(note.content, "Some insight");
1081 assert_eq!(note.note_type, NoteType::Insight);
1082 assert_eq!(note.abstraction_level, AbstractionLevel::Raw);
1083 assert_eq!(note.access_count, 0);
1084 }
1085
1086 #[test]
1087 fn test_note_updated() {
1088 let nid = NoteId::now_v7();
1089 let events = vec![
1090 make_event(
1091 EventKind::NOTE_CREATED,
1092 base_ts(),
1093 json!({
1094 "note_id": nid,
1095 "title": "Original",
1096 "content": "Original content",
1097 }),
1098 ),
1099 make_event(
1100 EventKind::NOTE_UPDATED,
1101 base_ts() + 1_000_000,
1102 json!({
1103 "title": "Updated Title",
1104 "content": "New content",
1105 }),
1106 ),
1107 ];
1108
1109 let note = Note::from_events(&events).unwrap();
1110 assert_eq!(note.title, "Updated Title");
1111 assert_eq!(note.content, "New content");
1112 }
1113
1114 #[test]
1115 fn test_note_accessed_increments_count() {
1116 let nid = NoteId::now_v7();
1117 let events = vec![
1118 make_event(
1119 EventKind::NOTE_CREATED,
1120 base_ts(),
1121 json!({
1122 "note_id": nid,
1123 "title": "Popular Note",
1124 "content": "Accessed often",
1125 }),
1126 ),
1127 make_event(EventKind::NOTE_ACCESSED, base_ts() + 1_000_000, json!({})),
1128 make_event(EventKind::NOTE_ACCESSED, base_ts() + 2_000_000, json!({})),
1129 make_event(EventKind::NOTE_ACCESSED, base_ts() + 3_000_000, json!({})),
1130 ];
1131
1132 let note = Note::from_events(&events).unwrap();
1133 assert_eq!(note.access_count, 3);
1134 }
1135
1136 #[test]
1137 fn test_note_deleted_returns_none() {
1138 let nid = NoteId::now_v7();
1139 let events = vec![
1140 make_event(
1141 EventKind::NOTE_CREATED,
1142 base_ts(),
1143 json!({
1144 "note_id": nid,
1145 "title": "Doomed Note",
1146 "content": "Will be deleted",
1147 }),
1148 ),
1149 make_event(EventKind::NOTE_DELETED, base_ts() + 1_000_000, json!({})),
1150 ];
1151
1152 assert!(Note::from_events(&events).is_none());
1153 }
1154
1155 #[test]
1156 fn test_note_superseded() {
1157 let nid = NoteId::now_v7();
1158 let new_nid = NoteId::now_v7();
1159 let events = vec![
1160 make_event(
1161 EventKind::NOTE_CREATED,
1162 base_ts(),
1163 json!({
1164 "note_id": nid,
1165 "title": "V1",
1166 "content": "Version 1",
1167 }),
1168 ),
1169 make_event(
1170 EventKind::NOTE_SUPERSEDED,
1171 base_ts() + 1_000_000,
1172 json!({ "superseded_by": new_nid }),
1173 ),
1174 ];
1175
1176 let note = Note::from_events(&events).unwrap();
1177 assert_eq!(note.superseded_by, Some(new_nid));
1178 }
1179
1180 #[test]
1181 fn test_note_out_of_order_events() {
1182 let nid = NoteId::now_v7();
1183 let events = vec![
1184 make_event(EventKind::NOTE_ACCESSED, base_ts() + 2_000_000, json!({})),
1186 make_event(
1187 EventKind::NOTE_UPDATED,
1188 base_ts() + 1_000_000,
1189 json!({ "title": "Updated" }),
1190 ),
1191 make_event(
1192 EventKind::NOTE_CREATED,
1193 base_ts(),
1194 json!({
1195 "note_id": nid,
1196 "title": "Original",
1197 "content": "Content",
1198 }),
1199 ),
1200 ];
1201
1202 let note = Note::from_events(&events).unwrap();
1203 assert_eq!(note.title, "Updated");
1205 assert_eq!(note.access_count, 1);
1206 }
1207
1208 #[test]
1213 fn test_trajectory_relevant_event_kinds() {
1214 let kinds = Trajectory::relevant_event_kinds();
1215 assert!(kinds.contains(&EventKind::TRAJECTORY_CREATED));
1216 assert!(kinds.contains(&EventKind::TRAJECTORY_UPDATED));
1217 assert!(kinds.contains(&EventKind::TRAJECTORY_COMPLETED));
1218 assert!(kinds.contains(&EventKind::TRAJECTORY_FAILED));
1219 assert!(kinds.contains(&EventKind::TRAJECTORY_DELETED));
1220 assert!(!kinds.contains(&EventKind::SCOPE_CREATED));
1221 }
1222
1223 #[test]
1224 fn test_scope_relevant_event_kinds() {
1225 let kinds = Scope::relevant_event_kinds();
1226 assert!(kinds.contains(&EventKind::SCOPE_CREATED));
1227 assert!(kinds.contains(&EventKind::SCOPE_UPDATED));
1228 assert!(kinds.contains(&EventKind::SCOPE_CLOSED));
1229 assert!(kinds.contains(&EventKind::SCOPE_CHECKPOINTED));
1230 assert!(!kinds.contains(&EventKind::TRAJECTORY_CREATED));
1231 }
1232
1233 #[test]
1234 fn test_artifact_relevant_event_kinds() {
1235 let kinds = Artifact::relevant_event_kinds();
1236 assert!(kinds.contains(&EventKind::ARTIFACT_CREATED));
1237 assert!(kinds.contains(&EventKind::ARTIFACT_UPDATED));
1238 assert!(kinds.contains(&EventKind::ARTIFACT_SUPERSEDED));
1239 assert!(kinds.contains(&EventKind::ARTIFACT_DELETED));
1240 assert!(!kinds.contains(&EventKind::NOTE_CREATED));
1241 }
1242
1243 #[test]
1244 fn test_note_relevant_event_kinds() {
1245 let kinds = Note::relevant_event_kinds();
1246 assert!(kinds.contains(&EventKind::NOTE_CREATED));
1247 assert!(kinds.contains(&EventKind::NOTE_UPDATED));
1248 assert!(kinds.contains(&EventKind::NOTE_SUPERSEDED));
1249 assert!(kinds.contains(&EventKind::NOTE_ACCESSED));
1250 assert!(kinds.contains(&EventKind::NOTE_DELETED));
1251 assert!(!kinds.contains(&EventKind::ARTIFACT_CREATED));
1252 }
1253}