1use std::{
7 collections::HashMap,
8 fmt::{self, Display},
9};
10
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15use super::integrity::{IntegrityHash, compute_integrity_hash};
16use crate::errors::GitError;
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23#[serde(rename_all = "snake_case")]
24pub enum Visibility {
25 Private,
26 Public,
27}
28
29#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum ObjectType {
50 Commit = 1,
51 Tree,
52 Blob,
53 Tag,
54 OffsetZstdelta, OffsetDelta,
56 HashDelta,
57 ContextSnapshot,
58 Decision,
59 Evidence,
60 PatchSet,
61 Plan,
62 Provenance,
63 Run,
64 Task,
65 ToolInvocation,
66}
67
68const COMMIT_OBJECT_TYPE: &[u8] = b"commit";
69const TREE_OBJECT_TYPE: &[u8] = b"tree";
70const BLOB_OBJECT_TYPE: &[u8] = b"blob";
71const TAG_OBJECT_TYPE: &[u8] = b"tag";
72const CONTEXT_SNAPSHOT_OBJECT_TYPE: &[u8] = b"snapshot";
73const DECISION_OBJECT_TYPE: &[u8] = b"decision";
74const EVIDENCE_OBJECT_TYPE: &[u8] = b"evidence";
75const PATCH_SET_OBJECT_TYPE: &[u8] = b"patchset";
76const PLAN_OBJECT_TYPE: &[u8] = b"plan";
77const PROVENANCE_OBJECT_TYPE: &[u8] = b"provenance";
78const RUN_OBJECT_TYPE: &[u8] = b"run";
79const TASK_OBJECT_TYPE: &[u8] = b"task";
80const TOOL_INVOCATION_OBJECT_TYPE: &[u8] = b"invocation";
81
82impl Display for ObjectType {
84 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
85 match self {
86 ObjectType::Blob => write!(f, "blob"),
87 ObjectType::Tree => write!(f, "tree"),
88 ObjectType::Commit => write!(f, "commit"),
89 ObjectType::Tag => write!(f, "tag"),
90 ObjectType::OffsetZstdelta => write!(f, "OffsetZstdelta"),
91 ObjectType::OffsetDelta => write!(f, "OffsetDelta"),
92 ObjectType::HashDelta => write!(f, "HashDelta"),
93 ObjectType::ContextSnapshot => write!(f, "snapshot"),
94 ObjectType::Decision => write!(f, "decision"),
95 ObjectType::Evidence => write!(f, "evidence"),
96 ObjectType::PatchSet => write!(f, "patchset"),
97 ObjectType::Plan => write!(f, "plan"),
98 ObjectType::Provenance => write!(f, "provenance"),
99 ObjectType::Run => write!(f, "run"),
100 ObjectType::Task => write!(f, "task"),
101 ObjectType::ToolInvocation => write!(f, "invocation"),
102 }
103 }
104}
105
106impl ObjectType {
108 pub fn to_pack_type_u8(&self) -> Result<u8, GitError> {
114 match self {
115 ObjectType::Commit => Ok(1),
116 ObjectType::Tree => Ok(2),
117 ObjectType::Blob => Ok(3),
118 ObjectType::Tag => Ok(4),
119 ObjectType::OffsetZstdelta => Ok(5),
120 ObjectType::OffsetDelta => Ok(6),
121 ObjectType::HashDelta => Ok(7),
122 _ => Err(GitError::PackEncodeError(format!(
123 "object type `{}` cannot be encoded in pack header type bits",
124 self
125 ))),
126 }
127 }
128
129 pub fn from_pack_type_u8(number: u8) -> Result<ObjectType, GitError> {
131 match number {
132 1 => Ok(ObjectType::Commit),
133 2 => Ok(ObjectType::Tree),
134 3 => Ok(ObjectType::Blob),
135 4 => Ok(ObjectType::Tag),
136 5 => Ok(ObjectType::OffsetZstdelta),
137 6 => Ok(ObjectType::OffsetDelta),
138 7 => Ok(ObjectType::HashDelta),
139 _ => Err(GitError::InvalidObjectType(format!(
140 "Invalid pack object type number: {number}"
141 ))),
142 }
143 }
144
145 pub fn to_bytes(&self) -> &[u8] {
146 match self {
147 ObjectType::Commit => COMMIT_OBJECT_TYPE,
148 ObjectType::Tree => TREE_OBJECT_TYPE,
149 ObjectType::Blob => BLOB_OBJECT_TYPE,
150 ObjectType::Tag => TAG_OBJECT_TYPE,
151 ObjectType::ContextSnapshot => CONTEXT_SNAPSHOT_OBJECT_TYPE,
152 ObjectType::Decision => DECISION_OBJECT_TYPE,
153 ObjectType::Evidence => EVIDENCE_OBJECT_TYPE,
154 ObjectType::PatchSet => PATCH_SET_OBJECT_TYPE,
155 ObjectType::Plan => PLAN_OBJECT_TYPE,
156 ObjectType::Provenance => PROVENANCE_OBJECT_TYPE,
157 ObjectType::Run => RUN_OBJECT_TYPE,
158 ObjectType::Task => TASK_OBJECT_TYPE,
159 ObjectType::ToolInvocation => TOOL_INVOCATION_OBJECT_TYPE,
160 _ => panic!("can put compute the delta hash value"),
161 }
162 }
163
164 pub fn from_string(s: &str) -> Result<ObjectType, GitError> {
166 match s {
167 "blob" => Ok(ObjectType::Blob),
168 "tree" => Ok(ObjectType::Tree),
169 "commit" => Ok(ObjectType::Commit),
170 "tag" => Ok(ObjectType::Tag),
171 "snapshot" => Ok(ObjectType::ContextSnapshot),
172 "decision" => Ok(ObjectType::Decision),
173 "evidence" => Ok(ObjectType::Evidence),
174 "patchset" => Ok(ObjectType::PatchSet),
175 "plan" => Ok(ObjectType::Plan),
176 "provenance" => Ok(ObjectType::Provenance),
177 "run" => Ok(ObjectType::Run),
178 "task" => Ok(ObjectType::Task),
179 "invocation" => Ok(ObjectType::ToolInvocation),
180 _ => Err(GitError::InvalidObjectType(s.to_string())),
181 }
182 }
183
184 pub fn to_data(self) -> Result<Vec<u8>, GitError> {
186 match self {
187 ObjectType::Blob => Ok(vec![0x62, 0x6c, 0x6f, 0x62]), ObjectType::Tree => Ok(vec![0x74, 0x72, 0x65, 0x65]), ObjectType::Commit => Ok(vec![0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74]), ObjectType::Tag => Ok(vec![0x74, 0x61, 0x67]), ObjectType::ContextSnapshot => Ok(vec![0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74]), ObjectType::Decision => Ok(vec![0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e]), ObjectType::Evidence => Ok(vec![0x65, 0x76, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65]), ObjectType::PatchSet => Ok(vec![0x70, 0x61, 0x74, 0x63, 0x68, 0x73, 0x65, 0x74]), ObjectType::Plan => Ok(vec![0x70, 0x6c, 0x61, 0x6e]), ObjectType::Provenance => Ok(vec![
197 0x70, 0x72, 0x6f, 0x76, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x65,
198 ]), ObjectType::Run => Ok(vec![0x72, 0x75, 0x6e]), ObjectType::Task => Ok(vec![0x74, 0x61, 0x73, 0x6b]), ObjectType::ToolInvocation => Ok(vec![
202 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
203 ]), _ => Err(GitError::InvalidObjectType(self.to_string())),
205 }
206 }
207
208 pub fn to_u8(&self) -> u8 {
210 match self {
211 ObjectType::Commit => 1,
212 ObjectType::Tree => 2,
213 ObjectType::Blob => 3,
214 ObjectType::Tag => 4,
215 ObjectType::OffsetZstdelta => 5, ObjectType::OffsetDelta => 6,
217 ObjectType::HashDelta => 7,
218 ObjectType::ContextSnapshot => 8,
219 ObjectType::Decision => 9,
220 ObjectType::Evidence => 10,
221 ObjectType::PatchSet => 11,
222 ObjectType::Plan => 12,
223 ObjectType::Provenance => 13,
224 ObjectType::Run => 14,
225 ObjectType::Task => 15,
226 ObjectType::ToolInvocation => 16,
227 }
228 }
229
230 pub fn from_u8(number: u8) -> Result<ObjectType, GitError> {
232 match number {
233 1 => Ok(ObjectType::Commit),
234 2 => Ok(ObjectType::Tree),
235 3 => Ok(ObjectType::Blob),
236 4 => Ok(ObjectType::Tag),
237 5 => Ok(ObjectType::OffsetZstdelta),
238 6 => Ok(ObjectType::OffsetDelta),
239 7 => Ok(ObjectType::HashDelta),
240 8 => Ok(ObjectType::ContextSnapshot),
241 9 => Ok(ObjectType::Decision),
242 10 => Ok(ObjectType::Evidence),
243 11 => Ok(ObjectType::PatchSet),
244 12 => Ok(ObjectType::Plan),
245 13 => Ok(ObjectType::Provenance),
246 14 => Ok(ObjectType::Run),
247 15 => Ok(ObjectType::Task),
248 16 => Ok(ObjectType::ToolInvocation),
249 _ => Err(GitError::InvalidObjectType(format!(
250 "Invalid object type number: {number}"
251 ))),
252 }
253 }
254
255 pub fn is_base(&self) -> bool {
256 match self {
257 ObjectType::Commit => true,
258 ObjectType::Tree => true,
259 ObjectType::Blob => true,
260 ObjectType::Tag => true,
261 ObjectType::HashDelta => false,
262 ObjectType::OffsetZstdelta => false,
263 ObjectType::OffsetDelta => false,
264 ObjectType::ContextSnapshot => true,
265 ObjectType::Decision => true,
266 ObjectType::Evidence => true,
267 ObjectType::PatchSet => true,
268 ObjectType::Plan => true,
269 ObjectType::Provenance => true,
270 ObjectType::Run => true,
271 ObjectType::Task => true,
272 ObjectType::ToolInvocation => true,
273 }
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
279#[serde(rename_all = "snake_case")]
280pub enum ActorKind {
281 Human,
282 Agent,
283 System,
284 McpClient,
285 #[serde(untagged)]
286 Other(String),
287}
288
289impl fmt::Display for ActorKind {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 match self {
292 ActorKind::Human => write!(f, "human"),
293 ActorKind::Agent => write!(f, "agent"),
294 ActorKind::System => write!(f, "system"),
295 ActorKind::McpClient => write!(f, "mcp_client"),
296 ActorKind::Other(s) => write!(f, "{}", s),
297 }
298 }
299}
300
301impl From<String> for ActorKind {
302 fn from(s: String) -> Self {
303 match s.as_str() {
304 "human" => ActorKind::Human,
305 "agent" => ActorKind::Agent,
306 "system" => ActorKind::System,
307 "mcp_client" => ActorKind::McpClient,
308 _ => ActorKind::Other(s),
309 }
310 }
311}
312
313impl From<&str> for ActorKind {
314 fn from(s: &str) -> Self {
315 match s {
316 "human" => ActorKind::Human,
317 "agent" => ActorKind::Agent,
318 "system" => ActorKind::System,
319 "mcp_client" => ActorKind::McpClient,
320 _ => ActorKind::Other(s.to_string()),
321 }
322 }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
327pub struct ActorRef {
328 kind: ActorKind,
330 id: String,
332 display_name: Option<String>,
334 auth_context: Option<String>,
336}
337
338impl ActorRef {
339 pub fn new(kind: impl Into<ActorKind>, id: impl Into<String>) -> Result<Self, String> {
341 let id_str = id.into();
342 if id_str.trim().is_empty() {
343 return Err("Actor ID cannot be empty".to_string());
344 }
345 Ok(Self {
346 kind: kind.into(),
347 id: id_str,
348 display_name: None,
349 auth_context: None,
350 })
351 }
352
353 pub fn new_for_mcp(id: impl Into<String>) -> Result<Self, String> {
355 Self::new(ActorKind::McpClient, id)
356 }
357
358 pub fn ensure_mcp_client(&self) -> Result<(), String> {
360 if self.kind != ActorKind::McpClient {
361 return Err("MCP writes must use mcp_client actor kind".to_string());
362 }
363 Ok(())
364 }
365
366 pub fn kind(&self) -> &ActorKind {
367 &self.kind
368 }
369
370 pub fn id(&self) -> &str {
371 &self.id
372 }
373
374 pub fn display_name(&self) -> Option<&str> {
375 self.display_name.as_deref()
376 }
377
378 pub fn auth_context(&self) -> Option<&str> {
379 self.auth_context.as_deref()
380 }
381
382 pub fn set_display_name(&mut self, display_name: Option<String>) {
383 self.display_name = display_name;
384 }
385
386 pub fn set_auth_context(&mut self, auth_context: Option<String>) {
387 self.auth_context = auth_context;
388 }
389
390 pub fn human(id: impl Into<String>) -> Result<Self, String> {
392 Self::new(ActorKind::Human, id)
393 }
394
395 pub fn agent(name: impl Into<String>) -> Result<Self, String> {
397 Self::new(ActorKind::Agent, name)
398 }
399
400 pub fn system(component: impl Into<String>) -> Result<Self, String> {
402 Self::new(ActorKind::System, component)
403 }
404
405 pub fn mcp_client(client_id: impl Into<String>) -> Result<Self, String> {
407 Self::new(ActorKind::McpClient, client_id)
408 }
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
413pub struct ArtifactRef {
414 store: String,
416 key: String,
418 content_type: Option<String>,
420 size_bytes: Option<u64>,
422 hash: Option<IntegrityHash>,
424 expires_at: Option<DateTime<Utc>>,
426}
427
428impl ArtifactRef {
429 pub fn new(store: impl Into<String>, key: impl Into<String>) -> Result<Self, String> {
430 let store = store.into();
431 let key = key.into();
432 if store.trim().is_empty() {
433 return Err("store cannot be empty".to_string());
434 }
435 if key.trim().is_empty() {
436 return Err("key cannot be empty".to_string());
437 }
438 Ok(Self {
439 store,
440 key,
441 content_type: None,
442 size_bytes: None,
443 hash: None,
444 expires_at: None,
445 })
446 }
447
448 pub fn store(&self) -> &str {
449 &self.store
450 }
451
452 pub fn key(&self) -> &str {
453 &self.key
454 }
455
456 pub fn content_type(&self) -> Option<&str> {
457 self.content_type.as_deref()
458 }
459
460 pub fn size_bytes(&self) -> Option<u64> {
461 self.size_bytes
462 }
463
464 pub fn hash(&self) -> Option<&IntegrityHash> {
465 self.hash.as_ref()
466 }
467
468 pub fn expires_at(&self) -> Option<DateTime<Utc>> {
469 self.expires_at
470 }
471
472 pub fn compute_hash(content: &[u8]) -> IntegrityHash {
474 IntegrityHash::compute(content)
475 }
476
477 pub fn with_hash(mut self, hash: IntegrityHash) -> Self {
479 self.hash = Some(hash);
480 self
481 }
482
483 pub fn with_hash_hex(mut self, hash: impl AsRef<str>) -> Result<Self, String> {
485 let hash = hash.as_ref().parse()?;
486 self.hash = Some(hash);
487 Ok(self)
488 }
489
490 pub fn set_content_type(&mut self, content_type: Option<String>) {
491 self.content_type = content_type;
492 }
493
494 pub fn set_size_bytes(&mut self, size_bytes: Option<u64>) {
495 self.size_bytes = size_bytes;
496 }
497
498 pub fn set_expires_at(&mut self, expires_at: Option<DateTime<Utc>>) {
499 self.expires_at = expires_at;
500 }
501
502 #[must_use = "handle integrity verification result"]
504 pub fn verify_integrity(&self, content: &[u8]) -> Result<bool, String> {
505 let stored_hash = self
506 .hash
507 .as_ref()
508 .ok_or_else(|| "No hash stored in ArtifactRef".to_string())?;
509
510 Ok(IntegrityHash::compute(content) == *stored_hash)
511 }
512
513 #[must_use]
515 pub fn content_eq(&self, other: &Self) -> Option<bool> {
516 match (&self.hash, &other.hash) {
517 (Some(a), Some(b)) => Some(a == b),
518 _ => None,
519 }
520 }
521
522 #[must_use]
524 pub fn is_expired(&self) -> bool {
525 if let Some(expires_at) = self.expires_at {
526 expires_at < Utc::now()
527 } else {
528 false
529 }
530 }
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
550pub struct Header {
551 object_id: Uuid,
553 object_type: ObjectType,
555 schema_version: u32,
557 repo_id: Uuid,
559 created_at: DateTime<Utc>,
561 created_by: ActorRef,
563 visibility: Visibility,
565 #[serde(default)]
567 tags: HashMap<String, String>,
568 #[serde(default)]
570 external_ids: HashMap<String, String>,
571 #[serde(default)]
573 checksum: Option<IntegrityHash>,
574}
575
576impl Header {
577 pub fn new(
585 object_type: ObjectType,
586 repo_id: Uuid,
587 created_by: ActorRef,
588 ) -> Result<Self, String> {
589 Ok(Self {
590 object_id: Uuid::now_v7(),
591 object_type,
592 schema_version: 1,
593 repo_id,
594 created_at: Utc::now(),
595 created_by,
596 visibility: Visibility::Private,
597 tags: HashMap::new(),
598 external_ids: HashMap::new(),
599 checksum: None,
600 })
601 }
602
603 pub fn object_id(&self) -> Uuid {
604 self.object_id
605 }
606
607 pub fn object_type(&self) -> &ObjectType {
608 &self.object_type
609 }
610
611 pub fn schema_version(&self) -> u32 {
612 self.schema_version
613 }
614
615 pub fn repo_id(&self) -> Uuid {
616 self.repo_id
617 }
618
619 pub fn created_at(&self) -> DateTime<Utc> {
620 self.created_at
621 }
622
623 pub fn created_by(&self) -> &ActorRef {
624 &self.created_by
625 }
626
627 pub fn visibility(&self) -> &Visibility {
628 &self.visibility
629 }
630
631 pub fn tags(&self) -> &HashMap<String, String> {
632 &self.tags
633 }
634
635 pub fn tags_mut(&mut self) -> &mut HashMap<String, String> {
636 &mut self.tags
637 }
638
639 pub fn external_ids(&self) -> &HashMap<String, String> {
640 &self.external_ids
641 }
642
643 pub fn external_ids_mut(&mut self) -> &mut HashMap<String, String> {
644 &mut self.external_ids
645 }
646
647 pub fn set_object_id(&mut self, object_id: Uuid) {
648 self.object_id = object_id;
649 }
650
651 pub fn set_object_type(&mut self, object_type: ObjectType) -> Result<(), String> {
652 self.object_type = object_type;
653 Ok(())
654 }
655
656 pub fn set_schema_version(&mut self, schema_version: u32) -> Result<(), String> {
657 if schema_version == 0 {
658 return Err("schema_version must be greater than 0".to_string());
659 }
660 self.schema_version = schema_version;
661 Ok(())
662 }
663
664 pub fn set_created_at(&mut self, created_at: DateTime<Utc>) {
665 self.created_at = created_at;
666 }
667
668 pub fn set_visibility(&mut self, visibility: Visibility) {
669 self.visibility = visibility;
670 }
671
672 pub fn checksum(&self) -> Option<&IntegrityHash> {
674 self.checksum.as_ref()
675 }
676
677 pub fn seal<T: Serialize>(&mut self, object: &T) -> Result<(), serde_json::Error> {
682 let previous = self.checksum.take();
683 match compute_integrity_hash(object) {
684 Ok(checksum) => {
685 self.checksum = Some(checksum);
686 Ok(())
687 }
688 Err(err) => {
689 self.checksum = previous;
690 Err(err)
691 }
692 }
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use chrono::{DateTime, Utc};
699 use uuid::Uuid;
700
701 use crate::internal::object::types::{
702 ActorKind, ActorRef, ArtifactRef, Header, IntegrityHash, ObjectType,
703 };
704
705 #[test]
707 fn test_object_type_to_data() {
708 let blob = ObjectType::Blob;
709 let blob_bytes = blob.to_data().unwrap();
710 assert_eq!(blob_bytes, vec![0x62, 0x6c, 0x6f, 0x62]);
711 }
712
713 #[test]
715 fn test_object_type_from_string() {
716 assert_eq!(ObjectType::from_string("blob").unwrap(), ObjectType::Blob);
717 assert_eq!(ObjectType::from_string("tree").unwrap(), ObjectType::Tree);
718 assert_eq!(
719 ObjectType::from_string("commit").unwrap(),
720 ObjectType::Commit
721 );
722 assert_eq!(ObjectType::from_string("tag").unwrap(), ObjectType::Tag);
723 assert_eq!(
724 ObjectType::from_string("snapshot").unwrap(),
725 ObjectType::ContextSnapshot
726 );
727 assert_eq!(
728 ObjectType::from_string("decision").unwrap(),
729 ObjectType::Decision
730 );
731 assert_eq!(
732 ObjectType::from_string("evidence").unwrap(),
733 ObjectType::Evidence
734 );
735 assert_eq!(
736 ObjectType::from_string("patchset").unwrap(),
737 ObjectType::PatchSet
738 );
739 assert_eq!(ObjectType::from_string("plan").unwrap(), ObjectType::Plan);
740 assert_eq!(
741 ObjectType::from_string("provenance").unwrap(),
742 ObjectType::Provenance
743 );
744 assert_eq!(ObjectType::from_string("run").unwrap(), ObjectType::Run);
745 assert_eq!(ObjectType::from_string("task").unwrap(), ObjectType::Task);
746 assert_eq!(
747 ObjectType::from_string("invocation").unwrap(),
748 ObjectType::ToolInvocation
749 );
750
751 assert!(ObjectType::from_string("invalid_type").is_err());
752 }
753
754 #[test]
756 fn test_object_type_to_u8() {
757 let commit = ObjectType::Commit;
758 let commit_number = commit.to_u8();
759 assert_eq!(commit_number, 1);
760 }
761
762 #[test]
764 fn test_object_type_from_u8() {
765 let tag_number = 4;
766 let tag = ObjectType::from_u8(tag_number).unwrap();
767 assert_eq!(tag, ObjectType::Tag);
768 }
769
770 #[test]
771 fn test_header_serialization() {
772 let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
773 let actor = ActorRef::human("jackie").expect("actor");
774 let header = Header::new(ObjectType::Task, repo_id, actor).expect("header");
775
776 let json = serde_json::to_string(&header).unwrap();
777 let deserialized: Header = serde_json::from_str(&json).unwrap();
778
779 assert_eq!(header.object_id(), deserialized.object_id());
780 assert_eq!(header.object_type(), deserialized.object_type());
781 assert_eq!(header.repo_id(), deserialized.repo_id());
782 }
783
784 #[test]
785 fn test_actor_ref() {
786 let actor = ActorRef::agent("coder").expect("actor");
787 assert_eq!(actor.kind(), &ActorKind::Agent);
788 assert_eq!(actor.id(), "coder");
789
790 let sys = ActorRef::system("scheduler").expect("system");
791 assert_eq!(sys.kind(), &ActorKind::System);
792
793 let client = ActorRef::mcp_client("vscode").expect("client");
794 assert_eq!(client.kind(), &ActorKind::McpClient);
795 assert!(client.ensure_mcp_client().is_ok());
796
797 let non_mcp = ActorRef::human("jackie").expect("actor");
798 assert!(non_mcp.ensure_mcp_client().is_err());
799 }
800
801 #[test]
802 fn test_actor_kind_serialization() {
803 let k = ActorKind::McpClient;
804 let s = serde_json::to_string(&k).unwrap();
805 assert_eq!(s, "\"mcp_client\"");
806
807 let k2: ActorKind = serde_json::from_str("\"system\"").unwrap();
808 assert_eq!(k2, ActorKind::System);
809 }
810
811 #[test]
812 fn test_header_checksum() {
813 let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
814 let actor = ActorRef::human("jackie").expect("actor");
815 let mut header = Header::new(ObjectType::Task, repo_id, actor).expect("header");
816 header.set_created_at(
818 DateTime::parse_from_rfc3339("2026-02-10T00:00:00Z")
819 .unwrap()
820 .with_timezone(&Utc),
821 );
822 header.set_object_id(Uuid::from_u128(0x00000000000000000000000000000001));
823
824 let checksum =
825 crate::internal::object::integrity::compute_integrity_hash(&header).expect("checksum");
826 assert_eq!(checksum.to_hex().len(), 64); header
830 .set_object_type(ObjectType::Run)
831 .expect("object_type");
832 let checksum2 =
833 crate::internal::object::integrity::compute_integrity_hash(&header).expect("checksum");
834 assert_ne!(checksum, checksum2);
835 }
836
837 #[test]
838 fn test_artifact_checksum() {
839 let content = b"hello world";
840 let hash = ArtifactRef::compute_hash(content);
841 let expected_str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
843 assert_eq!(hash.to_hex(), expected_str);
844
845 let artifact = ArtifactRef::new("s3", "key")
846 .expect("artifact")
847 .with_hash(hash);
848 assert_eq!(artifact.hash(), Some(&hash));
849
850 assert!(artifact.verify_integrity(content).unwrap());
852 assert!(!artifact.verify_integrity(b"wrong").unwrap());
853
854 let artifact2 = ArtifactRef::new("local", "other/path")
856 .expect("artifact")
857 .with_hash(IntegrityHash::compute(content));
858 assert_eq!(artifact.content_eq(&artifact2), Some(true));
859
860 let artifact3 = ArtifactRef::new("s3", "diff")
861 .expect("artifact")
862 .with_hash(ArtifactRef::compute_hash(b"diff"));
863 assert_eq!(artifact.content_eq(&artifact3), Some(false));
864 }
865
866 #[test]
867 fn test_invalid_checksum() {
868 let result = ArtifactRef::new("s3", "key")
869 .expect("artifact")
870 .with_hash_hex("bad_hash");
871 assert!(result.is_err());
872 }
873
874 #[test]
875 fn test_header_seal() {
876 let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
877 let actor = ActorRef::human("jackie").expect("actor");
878 let mut header = Header::new(ObjectType::Task, repo_id, actor).expect("header");
879
880 let content = serde_json::json!({"key": "value"});
881 header.seal(&content).expect("seal");
882
883 assert!(header.checksum().is_some());
884 let expected =
885 crate::internal::object::integrity::compute_integrity_hash(&content).expect("checksum");
886 assert_eq!(header.checksum().expect("checksum"), &expected);
887 }
888
889 #[test]
890 fn test_empty_actor_id() {
891 let result = ActorRef::new(ActorKind::Human, " ");
892 assert!(result.is_err());
893 }
894
895 #[test]
896 fn test_artifact_expiration() {
897 let mut artifact = ArtifactRef::new("s3", "key").expect("artifact");
898 assert!(!artifact.is_expired());
899
900 artifact.set_expires_at(Some(Utc::now() - chrono::Duration::hours(1)));
901 assert!(artifact.is_expired());
902
903 artifact.set_expires_at(Some(Utc::now() + chrono::Duration::hours(1)));
904 assert!(!artifact.is_expired());
905 }
906}