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 pub rubric_path: Option<String>,
405 #[serde(default)]
407 pub is_promotion_gate: bool,
408 #[serde(default)]
410 pub agent_phase: u8,
411 #[serde(with = "surreal_datetime")]
413 pub created_at: DateTime<Utc>,
414 #[serde(default, with = "surreal_datetime_opt")]
416 pub completed_at: Option<DateTime<Utc>>,
417}
418
419impl RunRecord {
420 #[allow(clippy::too_many_arguments)]
422 pub fn new(
423 run_id: String,
424 spec_digest: String,
425 git_sha: Option<String>,
426 agent_name: String,
427 tags: serde_json::Value,
428 rubric_path: Option<String>,
429 is_promotion_gate: bool,
430 agent_phase: u8,
431 ) -> Self {
432 RunRecord {
433 id: None,
434 run_id,
435 spec_digest,
436 git_sha,
437 agent_name,
438 tags,
439 status: "RUNNING".to_string(),
440 total_events: 0,
441 final_state_digest: None,
442 duration_ms: 0,
443 success: false,
444 rubric_path,
445 is_promotion_gate,
446 agent_phase,
447 created_at: Utc::now(),
448 completed_at: None,
449 }
450 }
451
452 pub fn complete(
454 mut self,
455 total_events: u64,
456 final_state_digest: Option<String>,
457 duration_ms: u64,
458 ) -> Self {
459 self.status = "COMPLETED".to_string();
460 self.total_events = total_events;
461 self.final_state_digest = final_state_digest;
462 self.duration_ms = duration_ms;
463 self.success = true;
464 self.completed_at = Some(Utc::now());
465 self
466 }
467
468 pub fn fail(mut self, total_events: u64, duration_ms: u64) -> Self {
470 self.status = "FAILED".to_string();
471 self.total_events = total_events;
472 self.duration_ms = duration_ms;
473 self.success = false;
474 self.completed_at = Some(Utc::now());
475 self
476 }
477
478 pub fn cancel(mut self, total_events: u64, duration_ms: u64) -> Self {
480 self.status = "CANCELLED".to_string();
481 self.total_events = total_events;
482 self.duration_ms = duration_ms;
483 self.success = false;
484 self.completed_at = Some(Utc::now());
485 self
486 }
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct RunEventRecord {
492 pub id: Option<surrealdb::sql::Thing>,
494 pub run_id: String,
496 pub seq: u64,
498 pub kind: String,
500 pub payload: serde_json::Value,
502 #[serde(with = "surreal_datetime")]
504 pub timestamp: DateTime<Utc>,
505}
506
507impl RunEventRecord {
508 pub fn new(run_id: String, seq: u64, kind: String, payload: serde_json::Value) -> Self {
510 RunEventRecord {
511 id: None,
512 run_id,
513 seq,
514 kind,
515 payload,
516 timestamp: Utc::now(),
517 }
518 }
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct ReleaseRecordSchema {
524 pub id: Option<surrealdb::sql::Thing>,
526 pub name: String,
528 pub spec_digest: String,
530 pub version_label: Option<String>,
532 pub promoted_by: String,
534 pub notes: Option<String>,
536 #[serde(with = "surreal_datetime")]
538 pub created_at: DateTime<Utc>,
539}
540
541impl ReleaseRecordSchema {
542 pub fn new(
544 name: String,
545 spec_digest: String,
546 version_label: Option<String>,
547 promoted_by: String,
548 notes: Option<String>,
549 ) -> Self {
550 ReleaseRecordSchema {
551 id: None,
552 name,
553 spec_digest,
554 version_label,
555 promoted_by,
556 notes,
557 created_at: Utc::now(),
558 }
559 }
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct DecisionRecord {
569 pub id: Option<surrealdb::sql::Thing>,
571 pub decision_id: String,
573 pub commit_id: String,
575 pub task: String,
577 pub action: String,
579 pub rationale: String,
581 pub alternatives: Vec<String>,
583 pub confidence: f32,
585 pub outcome: Option<String>, #[serde(with = "surreal_datetime")]
589 pub timestamp: DateTime<Utc>,
590 #[serde(default, with = "surreal_datetime_opt")]
592 pub outcome_at: Option<DateTime<Utc>>,
593}
594
595impl DecisionRecord {
596 pub fn new(
598 decision_id: String,
599 commit_id: String,
600 task: String,
601 action: String,
602 rationale: String,
603 confidence: f32,
604 ) -> Self {
605 DecisionRecord {
606 id: None,
607 decision_id,
608 commit_id,
609 task,
610 action,
611 rationale,
612 alternatives: Vec::new(),
613 confidence,
614 outcome: None,
615 timestamp: Utc::now(),
616 outcome_at: None,
617 }
618 }
619
620 pub fn with_alternatives(mut self, alternatives: Vec<String>) -> Self {
622 self.alternatives = alternatives;
623 self
624 }
625
626 pub fn with_outcome(mut self, outcome: String) -> Self {
628 self.outcome = Some(outcome);
629 self.outcome_at = Some(Utc::now());
630 self
631 }
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
636#[serde(rename_all = "snake_case")]
637pub enum ProvenanceSourceType {
638 RunTrace,
640 StateSnapshot,
642 UserAnnotation,
644 MemoryDerivation,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct MemoryProvenanceRecord {
651 pub id: Option<surrealdb::sql::Thing>,
653 pub memory_id: String,
655 pub source_type: String,
657 pub source_data: serde_json::Value,
659 pub derived_from: Option<String>,
661 #[serde(with = "surreal_datetime")]
663 pub created_at: DateTime<Utc>,
664 #[serde(default, with = "surreal_datetime_opt")]
666 pub invalidated_at: Option<DateTime<Utc>>,
667}
668
669impl MemoryProvenanceRecord {
670 pub fn from_run_trace(memory_id: String, run_id: String, event_idx: usize) -> Self {
672 MemoryProvenanceRecord {
673 id: None,
674 memory_id,
675 source_type: ProvenanceSourceType::RunTrace.to_string(),
676 source_data: serde_json::json!({ "run_id": run_id, "event_idx": event_idx }),
677 derived_from: None,
678 created_at: Utc::now(),
679 invalidated_at: None,
680 }
681 }
682
683 pub fn from_snapshot(memory_id: String, commit_id: String) -> Self {
685 MemoryProvenanceRecord {
686 id: None,
687 memory_id,
688 source_type: ProvenanceSourceType::StateSnapshot.to_string(),
689 source_data: serde_json::json!({ "commit_id": commit_id }),
690 derived_from: None,
691 created_at: Utc::now(),
692 invalidated_at: None,
693 }
694 }
695
696 pub fn from_user_annotation(memory_id: String, user_id: String) -> Self {
698 MemoryProvenanceRecord {
699 id: None,
700 memory_id,
701 source_type: ProvenanceSourceType::UserAnnotation.to_string(),
702 source_data: serde_json::json!({ "user_id": user_id }),
703 derived_from: None,
704 created_at: Utc::now(),
705 invalidated_at: None,
706 }
707 }
708
709 pub fn from_derivation(memory_id: String, parent_id: String, derivation: String) -> Self {
711 MemoryProvenanceRecord {
712 id: None,
713 memory_id,
714 source_type: ProvenanceSourceType::MemoryDerivation.to_string(),
715 source_data: serde_json::json!({ "derivation": derivation }),
716 derived_from: Some(parent_id),
717 created_at: Utc::now(),
718 invalidated_at: None,
719 }
720 }
721
722 pub fn invalidate(mut self) -> Self {
724 self.invalidated_at = Some(Utc::now());
725 self
726 }
727}
728
729impl core::fmt::Display for ProvenanceSourceType {
730 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
731 match self {
732 ProvenanceSourceType::RunTrace => write!(f, "run_trace"),
733 ProvenanceSourceType::StateSnapshot => write!(f, "state_snapshot"),
734 ProvenanceSourceType::UserAnnotation => write!(f, "user_annotation"),
735 ProvenanceSourceType::MemoryDerivation => write!(f, "memory_derivation"),
736 }
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn test_commit_id_from_state() {
746 let state = b"test state data";
747 let commit_id = CommitId::from_state(state);
748
749 assert!(!commit_id.hash.is_empty());
750 assert_eq!(commit_id.hash.len(), 64); assert!(commit_id.logic_hash.is_none());
752 assert!(commit_id.env_hash.is_none());
753 }
754
755 #[test]
756 fn test_commit_id_deterministic() {
757 let state = b"same state";
758 let id1 = CommitId::from_state(state);
759 let id2 = CommitId::from_state(state);
760
761 assert_eq!(id1.hash, id2.hash);
762 }
763
764 #[test]
765 fn test_commit_id_different_states() {
766 let id1 = CommitId::from_state(b"state 1");
767 let id2 = CommitId::from_state(b"state 2");
768
769 assert_ne!(id1.hash, id2.hash);
770 }
771
772 #[test]
773 fn test_commit_id_short() {
774 let commit_id = CommitId::from_state(b"test");
775 assert_eq!(commit_id.short().len(), 8);
776 }
777
778 #[test]
779 fn test_composite_commit_id() {
780 let commit_id = CommitId::new(Some("logic-hash"), "state-hash", Some("env-hash"));
781
782 assert!(!commit_id.hash.is_empty());
783 assert_eq!(commit_id.logic_hash, Some("logic-hash".to_string()));
784 assert_eq!(commit_id.env_hash, Some("env-hash".to_string()));
785 }
786
787 #[test]
788 fn test_commit_id_collision_prevention() {
789 let id1 = CommitId::new(Some("ab"), "cd", None);
791 let id2 = CommitId::new(Some("a"), "bcd", None);
792 assert_ne!(id1.hash, id2.hash);
793
794 let id3 = CommitId::new(None, "state", None);
798 let id4 = CommitId::new(Some("none"), "state", None);
799 assert_ne!(id3.hash, id4.hash);
800 }
801
802 #[test]
803 fn test_snapshot_record_size() {
804 let state = serde_json::json!({"key": "value", "nested": {"a": 1}});
805 let snapshot = SnapshotRecord::new("commit-123", state);
806
807 assert!(snapshot.size_bytes > 0);
808 }
809
810 #[test]
811 fn test_run_record_new() {
812 let run = RunRecord::new(
813 "run-123".to_string(),
814 "spec-digest-abc".to_string(),
815 Some("abc123".to_string()),
816 "test-agent".to_string(),
817 serde_json::json!({"env": "test"}),
818 None,
819 false,
820 1,
821 );
822
823 assert_eq!(run.run_id, "run-123");
824 assert_eq!(run.status, "RUNNING");
825 assert_eq!(run.total_events, 0);
826 assert!(!run.success);
827 }
828
829 #[test]
830 fn test_run_record_complete() {
831 let run = RunRecord::new(
832 "run-123".to_string(),
833 "spec-digest-abc".to_string(),
834 Some("abc123".to_string()),
835 "test-agent".to_string(),
836 serde_json::json!({}),
837 None,
838 false,
839 1,
840 )
841 .complete(5, Some("state-digest-xyz".to_string()), 1000);
842
843 assert_eq!(run.status, "COMPLETED");
844 assert_eq!(run.total_events, 5);
845 assert!(run.success);
846 assert!(run.completed_at.is_some());
847 }
848
849 #[test]
850 fn test_run_record_fail() {
851 let run = RunRecord::new(
852 "run-123".to_string(),
853 "spec-digest-abc".to_string(),
854 None,
855 "test-agent".to_string(),
856 serde_json::json!({}),
857 None,
858 false,
859 1,
860 )
861 .fail(2, 500);
862
863 assert_eq!(run.status, "FAILED");
864 assert_eq!(run.total_events, 2);
865 assert!(!run.success);
866 assert!(run.completed_at.is_some());
867 }
868
869 #[test]
870 fn test_run_event_record() {
871 let event = RunEventRecord::new(
872 "run-123".to_string(),
873 1,
874 "graph_started".to_string(),
875 serde_json::json!({"graph_id": "g1"}),
876 );
877
878 assert_eq!(event.run_id, "run-123");
879 assert_eq!(event.seq, 1);
880 assert_eq!(event.kind, "graph_started");
881 }
882
883 #[test]
884 fn test_release_record() {
885 let release = ReleaseRecordSchema::new(
886 "my-agent".to_string(),
887 "spec-digest-abc".to_string(),
888 Some("v1.0.0".to_string()),
889 "alice".to_string(),
890 Some("Initial release".to_string()),
891 );
892
893 assert_eq!(release.name, "my-agent");
894 assert_eq!(release.version_label, Some("v1.0.0".to_string()));
895 }
896
897 #[test]
898 fn test_decision_record_new() {
899 let decision = DecisionRecord::new(
900 "dec-123".to_string(),
901 "commit-abc".to_string(),
902 "task-optimize".to_string(),
903 "use_parallel".to_string(),
904 "Improves throughput".to_string(),
905 0.85,
906 );
907
908 assert_eq!(decision.decision_id, "dec-123");
909 assert_eq!(decision.commit_id, "commit-abc");
910 assert_eq!(decision.task, "task-optimize");
911 assert_eq!(decision.action, "use_parallel");
912 assert_eq!(decision.confidence, 0.85);
913 assert!(decision.outcome.is_none());
914 }
915
916 #[test]
917 fn test_decision_record_with_alternatives() {
918 let decision = DecisionRecord::new(
919 "dec-456".to_string(),
920 "commit-def".to_string(),
921 "task-retry".to_string(),
922 "exponential_backoff".to_string(),
923 "Reduces thundering herd".to_string(),
924 0.75,
925 )
926 .with_alternatives(vec!["linear_backoff".to_string(), "no_retry".to_string()]);
927
928 assert_eq!(decision.alternatives.len(), 2);
929 assert!(decision
930 .alternatives
931 .contains(&"linear_backoff".to_string()));
932 }
933
934 #[test]
935 fn test_decision_record_with_outcome() {
936 let outcome_json = serde_json::json!({
937 "status": "success",
938 "benefit": 1.23,
939 "duration_ms": 5000
940 });
941 let decision = DecisionRecord::new(
942 "dec-789".to_string(),
943 "commit-ghi".to_string(),
944 "task-cache".to_string(),
945 "redis_cache".to_string(),
946 "Faster lookups".to_string(),
947 0.9,
948 )
949 .with_outcome(outcome_json.to_string());
950
951 assert!(decision.outcome.is_some());
952 assert!(decision.outcome_at.is_some());
953 }
954
955 #[test]
956 fn test_memory_provenance_from_run_trace() {
957 let prov = MemoryProvenanceRecord::from_run_trace(
958 "mem-123".to_string(),
959 "run-456".to_string(),
960 42,
961 );
962
963 assert_eq!(prov.memory_id, "mem-123");
964 assert_eq!(prov.source_type, ProvenanceSourceType::RunTrace.to_string());
965 assert!(prov.derived_from.is_none());
966 assert!(prov.invalidated_at.is_none());
967
968 let source_data: serde_json::Value = prov.source_data;
969 assert_eq!(source_data["run_id"], "run-456");
970 assert_eq!(source_data["event_idx"], 42);
971 }
972
973 #[test]
974 fn test_memory_provenance_from_snapshot() {
975 let prov =
976 MemoryProvenanceRecord::from_snapshot("mem-789".to_string(), "commit-abc".to_string());
977
978 assert_eq!(prov.memory_id, "mem-789");
979 assert_eq!(
980 prov.source_type,
981 ProvenanceSourceType::StateSnapshot.to_string()
982 );
983 assert_eq!(prov.source_data["commit_id"], "commit-abc");
984 }
985
986 #[test]
987 fn test_memory_provenance_from_derivation() {
988 let prov = MemoryProvenanceRecord::from_derivation(
989 "mem-new".to_string(),
990 "mem-parent".to_string(),
991 "summarize".to_string(),
992 );
993
994 assert_eq!(prov.memory_id, "mem-new");
995 assert_eq!(prov.derived_from, Some("mem-parent".to_string()));
996 assert_eq!(
997 prov.source_type,
998 ProvenanceSourceType::MemoryDerivation.to_string()
999 );
1000 assert_eq!(prov.source_data["derivation"], "summarize");
1001 }
1002
1003 #[test]
1004 fn test_memory_provenance_invalidation() {
1005 let prov = MemoryProvenanceRecord::from_user_annotation(
1006 "mem-123".to_string(),
1007 "user-456".to_string(),
1008 );
1009
1010 assert!(prov.invalidated_at.is_none());
1011
1012 let invalidated = prov.invalidate();
1013 assert!(invalidated.invalidated_at.is_some());
1014 }
1015
1016 #[test]
1017 fn test_provenance_source_type_display() {
1018 assert_eq!(ProvenanceSourceType::RunTrace.to_string(), "run_trace");
1019 assert_eq!(
1020 ProvenanceSourceType::StateSnapshot.to_string(),
1021 "state_snapshot"
1022 );
1023 assert_eq!(
1024 ProvenanceSourceType::UserAnnotation.to_string(),
1025 "user_annotation"
1026 );
1027 assert_eq!(
1028 ProvenanceSourceType::MemoryDerivation.to_string(),
1029 "memory_derivation"
1030 );
1031 }
1032}