1use chrono::{DateTime, Utc};
10
11mod surreal_datetime {
13 use chrono::{DateTime, Utc};
14 use serde::{self, Deserialize, Deserializer, Serializer};
15 use surrealdb::sql::Datetime as SurrealDatetime;
16
17 pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
18 where
19 S: Serializer,
20 {
21 let sd = SurrealDatetime::from(*date);
22 serde::Serialize::serialize(&sd, serializer)
23 }
24
25 pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
26 where
27 D: Deserializer<'de>,
28 {
29 let sd = SurrealDatetime::deserialize(deserializer)?;
30 Ok(DateTime::from(sd))
31 }
32}
33
34mod surreal_datetime_opt {
36 use chrono::{DateTime, Utc};
37 use serde::{self, Deserialize, Deserializer, Serializer};
38 use surrealdb::sql::Datetime as SurrealDatetime;
39
40 pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
41 where
42 S: Serializer,
43 {
44 match date {
45 Some(d) => {
46 let sd = SurrealDatetime::from(*d);
47 serde::Serialize::serialize(&Some(sd), serializer)
48 }
49 None => serde::Serialize::serialize(&None::<SurrealDatetime>, serializer),
50 }
51 }
52
53 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
54 where
55 D: Deserializer<'de>,
56 {
57 let sd = Option::<SurrealDatetime>::deserialize(deserializer)?;
58 Ok(sd.map(DateTime::from))
59 }
60}
61use serde::{Deserialize, Serialize};
62use sha2::{Digest, Sha256};
63use uuid::Uuid;
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
72pub struct CommitId {
73 pub hash: String,
75 pub logic_hash: Option<String>,
77 pub state_hash: String,
79 pub env_hash: Option<String>,
81}
82
83impl CommitId {
84 pub fn from_state(state: &[u8]) -> Self {
86 let mut hasher = Sha256::new();
87 hasher.update(state);
88 let state_hash = hex::encode(hasher.finalize());
89
90 Self::new(None, &state_hash, None)
92 }
93
94 pub fn new(logic_hash: Option<&str>, state_hash: &str, env_hash: Option<&str>) -> Self {
96 let mut hasher = Sha256::new();
97
98 hasher.update(b"L");
101 if let Some(lh) = logic_hash {
102 hasher.update(b"S");
103 hasher.update(lh.as_bytes());
104 } else {
105 hasher.update(b"N");
106 }
107 hasher.update(b"\0");
108
109 hasher.update(b"S:");
111 hasher.update(state_hash.as_bytes());
112 hasher.update(b"\0");
113
114 hasher.update(b"E");
116 if let Some(eh) = env_hash {
117 hasher.update(b"S");
118 hasher.update(eh.as_bytes());
119 } else {
120 hasher.update(b"N");
121 }
122
123 let composite = hex::encode(hasher.finalize());
124
125 CommitId {
126 hash: composite,
127 logic_hash: logic_hash.map(String::from),
128 state_hash: state_hash.to_string(),
129 env_hash: env_hash.map(String::from),
130 }
131 }
132
133 pub fn short(&self) -> String {
135 self.hash.chars().take(8).collect()
136 }
137}
138
139impl std::fmt::Display for CommitId {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 write!(f, "{}", self.hash)
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct CommitRecord {
148 pub id: Option<surrealdb::sql::Thing>,
150 pub commit_id: CommitId,
152 pub parent_ids: Vec<String>,
154 pub message: String,
156 pub author: String,
158 #[serde(with = "surreal_datetime")]
160 pub created_at: DateTime<Utc>,
161 pub branch: Option<String>,
163}
164
165impl CommitRecord {
166 pub fn new(commit_id: CommitId, parent_ids: Vec<String>, message: &str, author: &str) -> Self {
168 CommitRecord {
169 id: None,
170 commit_id,
171 parent_ids,
172 message: message.to_string(),
173 author: author.to_string(),
174 created_at: Utc::now(),
175 branch: None,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct SnapshotRecord {
183 pub id: Option<surrealdb::sql::Thing>,
185 pub commit_id: String,
187 pub state: serde_json::Value,
189 pub size_bytes: u64,
191 #[serde(with = "surreal_datetime")]
193 pub created_at: DateTime<Utc>,
194}
195
196impl SnapshotRecord {
197 pub fn new(commit_id: &str, state: serde_json::Value) -> Self {
199 let size = serde_json::to_string(&state)
200 .map(|s| s.len() as u64)
201 .unwrap_or(0);
202
203 SnapshotRecord {
204 id: None,
205 commit_id: commit_id.to_string(),
206 state,
207 size_bytes: size,
208 created_at: Utc::now(),
209 }
210 }
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct BranchRecord {
216 pub id: Option<surrealdb::sql::Thing>,
218 pub name: String,
220 pub head_commit_id: String,
222 pub is_default: bool,
224 #[serde(with = "surreal_datetime")]
226 pub created_at: DateTime<Utc>,
227 #[serde(with = "surreal_datetime")]
229 pub updated_at: DateTime<Utc>,
230}
231
232impl BranchRecord {
233 pub fn new(name: &str, head_commit_id: &str, is_default: bool) -> Self {
235 let now = Utc::now();
236 BranchRecord {
237 id: None,
238 name: name.to_string(),
239 head_commit_id: head_commit_id.to_string(),
240 is_default,
241 created_at: now,
242 updated_at: now,
243 }
244 }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct AgentRecord {
250 pub id: Option<surrealdb::sql::Thing>,
252 pub agent_id: Uuid,
254 pub name: String,
256 pub agent_type: String,
258 pub config: serde_json::Value,
260 #[serde(with = "surreal_datetime")]
262 pub created_at: DateTime<Utc>,
263}
264
265impl AgentRecord {
266 pub fn new(name: &str, agent_type: &str, config: serde_json::Value) -> Self {
268 AgentRecord {
269 id: None,
270 agent_id: Uuid::new_v4(),
271 name: name.to_string(),
272 agent_type: agent_type.to_string(),
273 config,
274 created_at: Utc::now(),
275 }
276 }
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct MemoryRecord {
282 pub id: Option<surrealdb::sql::Thing>,
284 pub commit_id: String,
286 pub key: String,
288 pub content: String,
290 pub embedding: Option<Vec<f32>>,
292 pub metadata: serde_json::Value,
294 #[serde(with = "surreal_datetime")]
296 pub created_at: DateTime<Utc>,
297}
298
299impl MemoryRecord {
300 pub fn new(commit_id: &str, key: &str, content: &str) -> Self {
302 MemoryRecord {
303 id: None,
304 commit_id: commit_id.to_string(),
305 key: key.to_string(),
306 content: content.to_string(),
307 embedding: None,
308 metadata: serde_json::json!({}),
309 created_at: Utc::now(),
310 }
311 }
312
313 pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
315 self.embedding = Some(embedding);
316 self
317 }
318
319 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
321 self.metadata = metadata;
322 self
323 }
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct GraphEdge {
329 pub child_id: String,
331 pub parent_id: String,
333 pub edge_type: EdgeType,
335 #[serde(with = "surreal_datetime")]
337 pub created_at: DateTime<Utc>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
342#[serde(rename_all = "lowercase")]
343pub enum EdgeType {
344 Normal,
346 Merge,
348 Fork,
350}
351
352impl GraphEdge {
353 pub fn new(child_id: &str, parent_id: &str) -> Self {
355 GraphEdge {
356 child_id: child_id.to_string(),
357 parent_id: parent_id.to_string(),
358 edge_type: EdgeType::Normal,
359 created_at: Utc::now(),
360 }
361 }
362
363 pub fn merge(child_id: &str, parent_id: &str) -> Self {
365 GraphEdge {
366 child_id: child_id.to_string(),
367 parent_id: parent_id.to_string(),
368 edge_type: EdgeType::Merge,
369 created_at: Utc::now(),
370 }
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct RunRecord {
381 pub id: Option<surrealdb::sql::Thing>,
383 pub run_id: String,
385 pub spec_digest: String,
387 pub git_sha: Option<String>,
389 pub agent_name: String,
391 pub tags: serde_json::Value,
393 pub status: String,
395 pub total_events: u64,
397 pub final_state_digest: Option<String>,
399 pub duration_ms: u64,
401 pub success: bool,
403 #[serde(with = "surreal_datetime")]
405 pub created_at: DateTime<Utc>,
406 #[serde(default, with = "surreal_datetime_opt")]
408 pub completed_at: Option<DateTime<Utc>>,
409}
410
411impl RunRecord {
412 pub fn new(
414 run_id: String,
415 spec_digest: String,
416 git_sha: Option<String>,
417 agent_name: String,
418 tags: serde_json::Value,
419 ) -> Self {
420 RunRecord {
421 id: None,
422 run_id,
423 spec_digest,
424 git_sha,
425 agent_name,
426 tags,
427 status: "RUNNING".to_string(),
428 total_events: 0,
429 final_state_digest: None,
430 duration_ms: 0,
431 success: false,
432 created_at: Utc::now(),
433 completed_at: None,
434 }
435 }
436
437 pub fn complete(
439 mut self,
440 total_events: u64,
441 final_state_digest: Option<String>,
442 duration_ms: u64,
443 ) -> Self {
444 self.status = "COMPLETED".to_string();
445 self.total_events = total_events;
446 self.final_state_digest = final_state_digest;
447 self.duration_ms = duration_ms;
448 self.success = true;
449 self.completed_at = Some(Utc::now());
450 self
451 }
452
453 pub fn fail(mut self, total_events: u64, duration_ms: u64) -> Self {
455 self.status = "FAILED".to_string();
456 self.total_events = total_events;
457 self.duration_ms = duration_ms;
458 self.success = false;
459 self.completed_at = Some(Utc::now());
460 self
461 }
462
463 pub fn cancel(mut self, total_events: u64, duration_ms: u64) -> Self {
465 self.status = "CANCELLED".to_string();
466 self.total_events = total_events;
467 self.duration_ms = duration_ms;
468 self.success = false;
469 self.completed_at = Some(Utc::now());
470 self
471 }
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct RunEventRecord {
477 pub id: Option<surrealdb::sql::Thing>,
479 pub run_id: String,
481 pub seq: u64,
483 pub kind: String,
485 pub payload: serde_json::Value,
487 #[serde(with = "surreal_datetime")]
489 pub timestamp: DateTime<Utc>,
490}
491
492impl RunEventRecord {
493 pub fn new(run_id: String, seq: u64, kind: String, payload: serde_json::Value) -> Self {
495 RunEventRecord {
496 id: None,
497 run_id,
498 seq,
499 kind,
500 payload,
501 timestamp: Utc::now(),
502 }
503 }
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct ReleaseRecordSchema {
509 pub id: Option<surrealdb::sql::Thing>,
511 pub name: String,
513 pub spec_digest: String,
515 pub version_label: Option<String>,
517 pub promoted_by: String,
519 pub notes: Option<String>,
521 #[serde(with = "surreal_datetime")]
523 pub created_at: DateTime<Utc>,
524}
525
526impl ReleaseRecordSchema {
527 pub fn new(
529 name: String,
530 spec_digest: String,
531 version_label: Option<String>,
532 promoted_by: String,
533 notes: Option<String>,
534 ) -> Self {
535 ReleaseRecordSchema {
536 id: None,
537 name,
538 spec_digest,
539 version_label,
540 promoted_by,
541 notes,
542 created_at: Utc::now(),
543 }
544 }
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct DecisionRecord {
554 pub id: Option<surrealdb::sql::Thing>,
556 pub decision_id: String,
558 pub commit_id: String,
560 pub task: String,
562 pub action: String,
564 pub rationale: String,
566 pub alternatives: serde_json::Value,
568 pub confidence: f32,
570 pub outcome: Option<String>, #[serde(with = "surreal_datetime")]
574 pub timestamp: DateTime<Utc>,
575 #[serde(default, with = "surreal_datetime_opt")]
577 pub outcome_at: Option<DateTime<Utc>>,
578}
579
580impl DecisionRecord {
581 pub fn new(
583 decision_id: String,
584 commit_id: String,
585 task: String,
586 action: String,
587 rationale: String,
588 confidence: f32,
589 ) -> Self {
590 DecisionRecord {
591 id: None,
592 decision_id,
593 commit_id,
594 task,
595 action,
596 rationale,
597 alternatives: serde_json::json!([]),
598 confidence,
599 outcome: None,
600 timestamp: Utc::now(),
601 outcome_at: None,
602 }
603 }
604
605 pub fn with_alternatives(mut self, alternatives: Vec<String>) -> Self {
607 self.alternatives = serde_json::json!(alternatives);
608 self
609 }
610
611 pub fn with_outcome(mut self, outcome: String) -> Self {
613 self.outcome = Some(outcome);
614 self.outcome_at = Some(Utc::now());
615 self
616 }
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
621#[serde(rename_all = "snake_case")]
622pub enum ProvenanceSourceType {
623 RunTrace,
625 StateSnapshot,
627 UserAnnotation,
629 MemoryDerivation,
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct MemoryProvenanceRecord {
636 pub id: Option<surrealdb::sql::Thing>,
638 pub memory_id: String,
640 pub source_type: String,
642 pub source_data: serde_json::Value,
644 pub derived_from: Option<String>,
646 #[serde(with = "surreal_datetime")]
648 pub created_at: DateTime<Utc>,
649 #[serde(default, with = "surreal_datetime_opt")]
651 pub invalidated_at: Option<DateTime<Utc>>,
652}
653
654impl MemoryProvenanceRecord {
655 pub fn from_run_trace(memory_id: String, run_id: String, event_idx: usize) -> Self {
657 MemoryProvenanceRecord {
658 id: None,
659 memory_id,
660 source_type: ProvenanceSourceType::RunTrace.to_string(),
661 source_data: serde_json::json!({ "run_id": run_id, "event_idx": event_idx }),
662 derived_from: None,
663 created_at: Utc::now(),
664 invalidated_at: None,
665 }
666 }
667
668 pub fn from_snapshot(memory_id: String, commit_id: String) -> Self {
670 MemoryProvenanceRecord {
671 id: None,
672 memory_id,
673 source_type: ProvenanceSourceType::StateSnapshot.to_string(),
674 source_data: serde_json::json!({ "commit_id": commit_id }),
675 derived_from: None,
676 created_at: Utc::now(),
677 invalidated_at: None,
678 }
679 }
680
681 pub fn from_user_annotation(memory_id: String, user_id: String) -> Self {
683 MemoryProvenanceRecord {
684 id: None,
685 memory_id,
686 source_type: ProvenanceSourceType::UserAnnotation.to_string(),
687 source_data: serde_json::json!({ "user_id": user_id }),
688 derived_from: None,
689 created_at: Utc::now(),
690 invalidated_at: None,
691 }
692 }
693
694 pub fn from_derivation(memory_id: String, parent_id: String, derivation: String) -> Self {
696 MemoryProvenanceRecord {
697 id: None,
698 memory_id,
699 source_type: ProvenanceSourceType::MemoryDerivation.to_string(),
700 source_data: serde_json::json!({ "derivation": derivation }),
701 derived_from: Some(parent_id),
702 created_at: Utc::now(),
703 invalidated_at: None,
704 }
705 }
706
707 pub fn invalidate(mut self) -> Self {
709 self.invalidated_at = Some(Utc::now());
710 self
711 }
712}
713
714impl core::fmt::Display for ProvenanceSourceType {
715 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
716 match self {
717 ProvenanceSourceType::RunTrace => write!(f, "run_trace"),
718 ProvenanceSourceType::StateSnapshot => write!(f, "state_snapshot"),
719 ProvenanceSourceType::UserAnnotation => write!(f, "user_annotation"),
720 ProvenanceSourceType::MemoryDerivation => write!(f, "memory_derivation"),
721 }
722 }
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728
729 #[test]
730 fn test_commit_id_from_state() {
731 let state = b"test state data";
732 let commit_id = CommitId::from_state(state);
733
734 assert!(!commit_id.hash.is_empty());
735 assert_eq!(commit_id.hash.len(), 64); assert!(commit_id.logic_hash.is_none());
737 assert!(commit_id.env_hash.is_none());
738 }
739
740 #[test]
741 fn test_commit_id_deterministic() {
742 let state = b"same state";
743 let id1 = CommitId::from_state(state);
744 let id2 = CommitId::from_state(state);
745
746 assert_eq!(id1.hash, id2.hash);
747 }
748
749 #[test]
750 fn test_commit_id_different_states() {
751 let id1 = CommitId::from_state(b"state 1");
752 let id2 = CommitId::from_state(b"state 2");
753
754 assert_ne!(id1.hash, id2.hash);
755 }
756
757 #[test]
758 fn test_commit_id_short() {
759 let commit_id = CommitId::from_state(b"test");
760 assert_eq!(commit_id.short().len(), 8);
761 }
762
763 #[test]
764 fn test_composite_commit_id() {
765 let commit_id = CommitId::new(Some("logic-hash"), "state-hash", Some("env-hash"));
766
767 assert!(!commit_id.hash.is_empty());
768 assert_eq!(commit_id.logic_hash, Some("logic-hash".to_string()));
769 assert_eq!(commit_id.env_hash, Some("env-hash".to_string()));
770 }
771
772 #[test]
773 fn test_commit_id_collision_prevention() {
774 let id1 = CommitId::new(Some("ab"), "cd", None);
776 let id2 = CommitId::new(Some("a"), "bcd", None);
777 assert_ne!(id1.hash, id2.hash);
778
779 let id3 = CommitId::new(None, "state", None);
783 let id4 = CommitId::new(Some("none"), "state", None);
784 assert_ne!(id3.hash, id4.hash);
785 }
786
787 #[test]
788 fn test_snapshot_record_size() {
789 let state = serde_json::json!({"key": "value", "nested": {"a": 1}});
790 let snapshot = SnapshotRecord::new("commit-123", state);
791
792 assert!(snapshot.size_bytes > 0);
793 }
794
795 #[test]
796 fn test_run_record_new() {
797 let run = RunRecord::new(
798 "run-123".to_string(),
799 "spec-digest-abc".to_string(),
800 Some("abc123".to_string()),
801 "test-agent".to_string(),
802 serde_json::json!({"env": "test"}),
803 );
804
805 assert_eq!(run.run_id, "run-123");
806 assert_eq!(run.status, "RUNNING");
807 assert_eq!(run.total_events, 0);
808 assert!(!run.success);
809 }
810
811 #[test]
812 fn test_run_record_complete() {
813 let run = RunRecord::new(
814 "run-123".to_string(),
815 "spec-digest-abc".to_string(),
816 Some("abc123".to_string()),
817 "test-agent".to_string(),
818 serde_json::json!({}),
819 )
820 .complete(5, Some("state-digest-xyz".to_string()), 1000);
821
822 assert_eq!(run.status, "COMPLETED");
823 assert_eq!(run.total_events, 5);
824 assert!(run.success);
825 assert!(run.completed_at.is_some());
826 }
827
828 #[test]
829 fn test_run_record_fail() {
830 let run = RunRecord::new(
831 "run-123".to_string(),
832 "spec-digest-abc".to_string(),
833 None,
834 "test-agent".to_string(),
835 serde_json::json!({}),
836 )
837 .fail(2, 500);
838
839 assert_eq!(run.status, "FAILED");
840 assert_eq!(run.total_events, 2);
841 assert!(!run.success);
842 assert!(run.completed_at.is_some());
843 }
844
845 #[test]
846 fn test_run_event_record() {
847 let event = RunEventRecord::new(
848 "run-123".to_string(),
849 1,
850 "graph_started".to_string(),
851 serde_json::json!({"graph_id": "g1"}),
852 );
853
854 assert_eq!(event.run_id, "run-123");
855 assert_eq!(event.seq, 1);
856 assert_eq!(event.kind, "graph_started");
857 }
858
859 #[test]
860 fn test_release_record() {
861 let release = ReleaseRecordSchema::new(
862 "my-agent".to_string(),
863 "spec-digest-abc".to_string(),
864 Some("v1.0.0".to_string()),
865 "alice".to_string(),
866 Some("Initial release".to_string()),
867 );
868
869 assert_eq!(release.name, "my-agent");
870 assert_eq!(release.version_label, Some("v1.0.0".to_string()));
871 }
872
873 #[test]
874 fn test_decision_record_new() {
875 let decision = DecisionRecord::new(
876 "dec-123".to_string(),
877 "commit-abc".to_string(),
878 "task-optimize".to_string(),
879 "use_parallel".to_string(),
880 "Improves throughput".to_string(),
881 0.85,
882 );
883
884 assert_eq!(decision.decision_id, "dec-123");
885 assert_eq!(decision.commit_id, "commit-abc");
886 assert_eq!(decision.task, "task-optimize");
887 assert_eq!(decision.action, "use_parallel");
888 assert_eq!(decision.confidence, 0.85);
889 assert!(decision.outcome.is_none());
890 }
891
892 #[test]
893 fn test_decision_record_with_alternatives() {
894 let decision = DecisionRecord::new(
895 "dec-456".to_string(),
896 "commit-def".to_string(),
897 "task-retry".to_string(),
898 "exponential_backoff".to_string(),
899 "Reduces thundering herd".to_string(),
900 0.75,
901 )
902 .with_alternatives(vec!["linear_backoff".to_string(), "no_retry".to_string()]);
903
904 let alts: Vec<String> = serde_json::from_value(decision.alternatives).unwrap();
905 assert_eq!(alts.len(), 2);
906 assert!(alts.contains(&"linear_backoff".to_string()));
907 }
908
909 #[test]
910 fn test_decision_record_with_outcome() {
911 let outcome_json = serde_json::json!({
912 "status": "success",
913 "benefit": 1.23,
914 "duration_ms": 5000
915 });
916 let decision = DecisionRecord::new(
917 "dec-789".to_string(),
918 "commit-ghi".to_string(),
919 "task-cache".to_string(),
920 "redis_cache".to_string(),
921 "Faster lookups".to_string(),
922 0.9,
923 )
924 .with_outcome(outcome_json.to_string());
925
926 assert!(decision.outcome.is_some());
927 assert!(decision.outcome_at.is_some());
928 }
929
930 #[test]
931 fn test_memory_provenance_from_run_trace() {
932 let prov = MemoryProvenanceRecord::from_run_trace(
933 "mem-123".to_string(),
934 "run-456".to_string(),
935 42,
936 );
937
938 assert_eq!(prov.memory_id, "mem-123");
939 assert_eq!(prov.source_type, ProvenanceSourceType::RunTrace.to_string());
940 assert!(prov.derived_from.is_none());
941 assert!(prov.invalidated_at.is_none());
942
943 let source_data: serde_json::Value = prov.source_data;
944 assert_eq!(source_data["run_id"], "run-456");
945 assert_eq!(source_data["event_idx"], 42);
946 }
947
948 #[test]
949 fn test_memory_provenance_from_snapshot() {
950 let prov =
951 MemoryProvenanceRecord::from_snapshot("mem-789".to_string(), "commit-abc".to_string());
952
953 assert_eq!(prov.memory_id, "mem-789");
954 assert_eq!(
955 prov.source_type,
956 ProvenanceSourceType::StateSnapshot.to_string()
957 );
958 assert_eq!(prov.source_data["commit_id"], "commit-abc");
959 }
960
961 #[test]
962 fn test_memory_provenance_from_derivation() {
963 let prov = MemoryProvenanceRecord::from_derivation(
964 "mem-new".to_string(),
965 "mem-parent".to_string(),
966 "summarize".to_string(),
967 );
968
969 assert_eq!(prov.memory_id, "mem-new");
970 assert_eq!(prov.derived_from, Some("mem-parent".to_string()));
971 assert_eq!(
972 prov.source_type,
973 ProvenanceSourceType::MemoryDerivation.to_string()
974 );
975 assert_eq!(prov.source_data["derivation"], "summarize");
976 }
977
978 #[test]
979 fn test_memory_provenance_invalidation() {
980 let prov = MemoryProvenanceRecord::from_user_annotation(
981 "mem-123".to_string(),
982 "user-456".to_string(),
983 );
984
985 assert!(prov.invalidated_at.is_none());
986
987 let invalidated = prov.invalidate();
988 assert!(invalidated.invalidated_at.is_some());
989 }
990
991 #[test]
992 fn test_provenance_source_type_display() {
993 assert_eq!(ProvenanceSourceType::RunTrace.to_string(), "run_trace");
994 assert_eq!(
995 ProvenanceSourceType::StateSnapshot.to_string(),
996 "state_snapshot"
997 );
998 assert_eq!(
999 ProvenanceSourceType::UserAnnotation.to_string(),
1000 "user_annotation"
1001 );
1002 assert_eq!(
1003 ProvenanceSourceType::MemoryDerivation.to_string(),
1004 "memory_derivation"
1005 );
1006 }
1007}