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 Intent,
66 ToolInvocation,
67 ContextPipeline,
68}
69
70const COMMIT_OBJECT_TYPE: &[u8] = b"commit";
71const TREE_OBJECT_TYPE: &[u8] = b"tree";
72const BLOB_OBJECT_TYPE: &[u8] = b"blob";
73const TAG_OBJECT_TYPE: &[u8] = b"tag";
74const CONTEXT_SNAPSHOT_OBJECT_TYPE: &[u8] = b"snapshot";
75const DECISION_OBJECT_TYPE: &[u8] = b"decision";
76const EVIDENCE_OBJECT_TYPE: &[u8] = b"evidence";
77const PATCH_SET_OBJECT_TYPE: &[u8] = b"patchset";
78const PLAN_OBJECT_TYPE: &[u8] = b"plan";
79const PROVENANCE_OBJECT_TYPE: &[u8] = b"provenance";
80const RUN_OBJECT_TYPE: &[u8] = b"run";
81const TASK_OBJECT_TYPE: &[u8] = b"task";
82const INTENT_OBJECT_TYPE: &[u8] = b"intent";
83const TOOL_INVOCATION_OBJECT_TYPE: &[u8] = b"invocation";
84const CONTEXT_PIPELINE_OBJECT_TYPE: &[u8] = b"pipeline";
85
86impl Display for ObjectType {
88 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
89 match self {
90 ObjectType::Blob => write!(f, "blob"),
91 ObjectType::Tree => write!(f, "tree"),
92 ObjectType::Commit => write!(f, "commit"),
93 ObjectType::Tag => write!(f, "tag"),
94 ObjectType::OffsetZstdelta => write!(f, "OffsetZstdelta"),
95 ObjectType::OffsetDelta => write!(f, "OffsetDelta"),
96 ObjectType::HashDelta => write!(f, "HashDelta"),
97 ObjectType::ContextSnapshot => write!(f, "snapshot"),
98 ObjectType::Decision => write!(f, "decision"),
99 ObjectType::Evidence => write!(f, "evidence"),
100 ObjectType::PatchSet => write!(f, "patchset"),
101 ObjectType::Plan => write!(f, "plan"),
102 ObjectType::Provenance => write!(f, "provenance"),
103 ObjectType::Run => write!(f, "run"),
104 ObjectType::Task => write!(f, "task"),
105 ObjectType::Intent => write!(f, "intent"),
106 ObjectType::ToolInvocation => write!(f, "invocation"),
107 ObjectType::ContextPipeline => write!(f, "pipeline"),
108 }
109 }
110}
111
112impl ObjectType {
114 pub fn to_pack_type_u8(&self) -> Result<u8, GitError> {
120 match self {
121 ObjectType::Commit => Ok(1),
122 ObjectType::Tree => Ok(2),
123 ObjectType::Blob => Ok(3),
124 ObjectType::Tag => Ok(4),
125 ObjectType::OffsetZstdelta => Ok(5),
126 ObjectType::OffsetDelta => Ok(6),
127 ObjectType::HashDelta => Ok(7),
128 _ => Err(GitError::PackEncodeError(format!(
129 "object type `{}` cannot be encoded in pack header type bits",
130 self
131 ))),
132 }
133 }
134
135 pub fn from_pack_type_u8(number: u8) -> Result<ObjectType, GitError> {
137 match number {
138 1 => Ok(ObjectType::Commit),
139 2 => Ok(ObjectType::Tree),
140 3 => Ok(ObjectType::Blob),
141 4 => Ok(ObjectType::Tag),
142 5 => Ok(ObjectType::OffsetZstdelta),
143 6 => Ok(ObjectType::OffsetDelta),
144 7 => Ok(ObjectType::HashDelta),
145 _ => Err(GitError::InvalidObjectType(format!(
146 "Invalid pack object type number: {number}"
147 ))),
148 }
149 }
150
151 pub fn to_bytes(&self) -> Option<&[u8]> {
157 match self {
158 ObjectType::Commit => Some(COMMIT_OBJECT_TYPE),
159 ObjectType::Tree => Some(TREE_OBJECT_TYPE),
160 ObjectType::Blob => Some(BLOB_OBJECT_TYPE),
161 ObjectType::Tag => Some(TAG_OBJECT_TYPE),
162 ObjectType::ContextSnapshot => Some(CONTEXT_SNAPSHOT_OBJECT_TYPE),
163 ObjectType::Decision => Some(DECISION_OBJECT_TYPE),
164 ObjectType::Evidence => Some(EVIDENCE_OBJECT_TYPE),
165 ObjectType::PatchSet => Some(PATCH_SET_OBJECT_TYPE),
166 ObjectType::Plan => Some(PLAN_OBJECT_TYPE),
167 ObjectType::Provenance => Some(PROVENANCE_OBJECT_TYPE),
168 ObjectType::Run => Some(RUN_OBJECT_TYPE),
169 ObjectType::Task => Some(TASK_OBJECT_TYPE),
170 ObjectType::Intent => Some(INTENT_OBJECT_TYPE),
171 ObjectType::ToolInvocation => Some(TOOL_INVOCATION_OBJECT_TYPE),
172 ObjectType::ContextPipeline => Some(CONTEXT_PIPELINE_OBJECT_TYPE),
173 ObjectType::OffsetDelta | ObjectType::HashDelta | ObjectType::OffsetZstdelta => None,
174 }
175 }
176
177 pub fn from_string(s: &str) -> Result<ObjectType, GitError> {
179 match s {
180 "blob" => Ok(ObjectType::Blob),
181 "tree" => Ok(ObjectType::Tree),
182 "commit" => Ok(ObjectType::Commit),
183 "tag" => Ok(ObjectType::Tag),
184 "snapshot" => Ok(ObjectType::ContextSnapshot),
185 "decision" => Ok(ObjectType::Decision),
186 "evidence" => Ok(ObjectType::Evidence),
187 "patchset" => Ok(ObjectType::PatchSet),
188 "plan" => Ok(ObjectType::Plan),
189 "provenance" => Ok(ObjectType::Provenance),
190 "run" => Ok(ObjectType::Run),
191 "task" => Ok(ObjectType::Task),
192 "intent" => Ok(ObjectType::Intent),
193 "invocation" => Ok(ObjectType::ToolInvocation),
194 "pipeline" => Ok(ObjectType::ContextPipeline),
195 _ => Err(GitError::InvalidObjectType(s.to_string())),
196 }
197 }
198
199 pub fn to_data(self) -> Result<Vec<u8>, GitError> {
201 match self {
202 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![
212 0x70, 0x72, 0x6f, 0x76, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x65,
213 ]), ObjectType::Run => Ok(vec![0x72, 0x75, 0x6e]), ObjectType::Task => Ok(vec![0x74, 0x61, 0x73, 0x6b]), ObjectType::Intent => Ok(vec![0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74]), ObjectType::ToolInvocation => Ok(vec![
218 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
219 ]), ObjectType::ContextPipeline => Ok(vec![0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65]), _ => Err(GitError::InvalidObjectType(self.to_string())),
222 }
223 }
224
225 pub fn to_u8(&self) -> u8 {
227 match self {
228 ObjectType::Commit => 1,
229 ObjectType::Tree => 2,
230 ObjectType::Blob => 3,
231 ObjectType::Tag => 4,
232 ObjectType::OffsetZstdelta => 5, ObjectType::OffsetDelta => 6,
234 ObjectType::HashDelta => 7,
235 ObjectType::ContextSnapshot => 8,
236 ObjectType::Decision => 9,
237 ObjectType::Evidence => 10,
238 ObjectType::PatchSet => 11,
239 ObjectType::Plan => 12,
240 ObjectType::Provenance => 13,
241 ObjectType::Run => 14,
242 ObjectType::Task => 15,
243 ObjectType::Intent => 16,
244 ObjectType::ToolInvocation => 17,
245 ObjectType::ContextPipeline => 18,
246 }
247 }
248
249 pub fn from_u8(number: u8) -> Result<ObjectType, GitError> {
251 match number {
252 1 => Ok(ObjectType::Commit),
253 2 => Ok(ObjectType::Tree),
254 3 => Ok(ObjectType::Blob),
255 4 => Ok(ObjectType::Tag),
256 5 => Ok(ObjectType::OffsetZstdelta),
257 6 => Ok(ObjectType::OffsetDelta),
258 7 => Ok(ObjectType::HashDelta),
259 8 => Ok(ObjectType::ContextSnapshot),
260 9 => Ok(ObjectType::Decision),
261 10 => Ok(ObjectType::Evidence),
262 11 => Ok(ObjectType::PatchSet),
263 12 => Ok(ObjectType::Plan),
264 13 => Ok(ObjectType::Provenance),
265 14 => Ok(ObjectType::Run),
266 15 => Ok(ObjectType::Task),
267 16 => Ok(ObjectType::Intent),
268 17 => Ok(ObjectType::ToolInvocation),
269 18 => Ok(ObjectType::ContextPipeline),
270 _ => Err(GitError::InvalidObjectType(format!(
271 "Invalid object type number: {number}"
272 ))),
273 }
274 }
275
276 pub fn is_base(&self) -> bool {
281 matches!(
282 self,
283 ObjectType::Commit | ObjectType::Tree | ObjectType::Blob | ObjectType::Tag
284 )
285 }
286
287 pub fn is_ai_object(&self) -> bool {
290 matches!(
291 self,
292 ObjectType::ContextSnapshot
293 | ObjectType::Decision
294 | ObjectType::Evidence
295 | ObjectType::PatchSet
296 | ObjectType::Plan
297 | ObjectType::Provenance
298 | ObjectType::Run
299 | ObjectType::Task
300 | ObjectType::Intent
301 | ObjectType::ToolInvocation
302 | ObjectType::ContextPipeline
303 )
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
309#[serde(rename_all = "snake_case")]
310pub enum ActorKind {
311 Human,
312 Agent,
313 System,
314 McpClient,
315 #[serde(untagged)]
316 Other(String),
317}
318
319impl fmt::Display for ActorKind {
320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321 match self {
322 ActorKind::Human => write!(f, "human"),
323 ActorKind::Agent => write!(f, "agent"),
324 ActorKind::System => write!(f, "system"),
325 ActorKind::McpClient => write!(f, "mcp_client"),
326 ActorKind::Other(s) => write!(f, "{}", s),
327 }
328 }
329}
330
331impl From<String> for ActorKind {
332 fn from(s: String) -> Self {
333 match s.as_str() {
334 "human" => ActorKind::Human,
335 "agent" => ActorKind::Agent,
336 "system" => ActorKind::System,
337 "mcp_client" => ActorKind::McpClient,
338 _ => ActorKind::Other(s),
339 }
340 }
341}
342
343impl From<&str> for ActorKind {
344 fn from(s: &str) -> Self {
345 match s {
346 "human" => ActorKind::Human,
347 "agent" => ActorKind::Agent,
348 "system" => ActorKind::System,
349 "mcp_client" => ActorKind::McpClient,
350 _ => ActorKind::Other(s.to_string()),
351 }
352 }
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
357pub struct ActorRef {
358 kind: ActorKind,
360 id: String,
362 display_name: Option<String>,
364 auth_context: Option<String>,
366}
367
368impl ActorRef {
369 pub fn new(kind: impl Into<ActorKind>, id: impl Into<String>) -> Result<Self, String> {
371 let id_str = id.into();
372 if id_str.trim().is_empty() {
373 return Err("Actor ID cannot be empty".to_string());
374 }
375 Ok(Self {
376 kind: kind.into(),
377 id: id_str,
378 display_name: None,
379 auth_context: None,
380 })
381 }
382
383 pub fn new_for_mcp(id: impl Into<String>) -> Result<Self, String> {
385 Self::new(ActorKind::McpClient, id)
386 }
387
388 pub fn ensure_mcp_client(&self) -> Result<(), String> {
390 if self.kind != ActorKind::McpClient {
391 return Err("MCP writes must use mcp_client actor kind".to_string());
392 }
393 Ok(())
394 }
395
396 pub fn kind(&self) -> &ActorKind {
397 &self.kind
398 }
399
400 pub fn id(&self) -> &str {
401 &self.id
402 }
403
404 pub fn display_name(&self) -> Option<&str> {
405 self.display_name.as_deref()
406 }
407
408 pub fn auth_context(&self) -> Option<&str> {
409 self.auth_context.as_deref()
410 }
411
412 pub fn set_display_name(&mut self, display_name: Option<String>) {
413 self.display_name = display_name;
414 }
415
416 pub fn set_auth_context(&mut self, auth_context: Option<String>) {
417 self.auth_context = auth_context;
418 }
419
420 pub fn human(id: impl Into<String>) -> Result<Self, String> {
422 Self::new(ActorKind::Human, id)
423 }
424
425 pub fn agent(name: impl Into<String>) -> Result<Self, String> {
427 Self::new(ActorKind::Agent, name)
428 }
429
430 pub fn system(component: impl Into<String>) -> Result<Self, String> {
432 Self::new(ActorKind::System, component)
433 }
434
435 pub fn mcp_client(client_id: impl Into<String>) -> Result<Self, String> {
437 Self::new(ActorKind::McpClient, client_id)
438 }
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
443pub struct ArtifactRef {
444 store: String,
446 key: String,
448 content_type: Option<String>,
450 size_bytes: Option<u64>,
452 hash: Option<IntegrityHash>,
454 expires_at: Option<DateTime<Utc>>,
456}
457
458impl ArtifactRef {
459 pub fn new(store: impl Into<String>, key: impl Into<String>) -> Result<Self, String> {
460 let store = store.into();
461 let key = key.into();
462 if store.trim().is_empty() {
463 return Err("store cannot be empty".to_string());
464 }
465 if key.trim().is_empty() {
466 return Err("key cannot be empty".to_string());
467 }
468 Ok(Self {
469 store,
470 key,
471 content_type: None,
472 size_bytes: None,
473 hash: None,
474 expires_at: None,
475 })
476 }
477
478 pub fn store(&self) -> &str {
479 &self.store
480 }
481
482 pub fn key(&self) -> &str {
483 &self.key
484 }
485
486 pub fn content_type(&self) -> Option<&str> {
487 self.content_type.as_deref()
488 }
489
490 pub fn size_bytes(&self) -> Option<u64> {
491 self.size_bytes
492 }
493
494 pub fn hash(&self) -> Option<&IntegrityHash> {
495 self.hash.as_ref()
496 }
497
498 pub fn expires_at(&self) -> Option<DateTime<Utc>> {
499 self.expires_at
500 }
501
502 pub fn compute_hash(content: &[u8]) -> IntegrityHash {
504 IntegrityHash::compute(content)
505 }
506
507 pub fn with_hash(mut self, hash: IntegrityHash) -> Self {
509 self.hash = Some(hash);
510 self
511 }
512
513 pub fn with_hash_hex(mut self, hash: impl AsRef<str>) -> Result<Self, String> {
515 let hash = hash.as_ref().parse()?;
516 self.hash = Some(hash);
517 Ok(self)
518 }
519
520 pub fn set_content_type(&mut self, content_type: Option<String>) {
521 self.content_type = content_type;
522 }
523
524 pub fn set_size_bytes(&mut self, size_bytes: Option<u64>) {
525 self.size_bytes = size_bytes;
526 }
527
528 pub fn set_expires_at(&mut self, expires_at: Option<DateTime<Utc>>) {
529 self.expires_at = expires_at;
530 }
531
532 #[must_use = "handle integrity verification result"]
534 pub fn verify_integrity(&self, content: &[u8]) -> Result<bool, String> {
535 let stored_hash = self
536 .hash
537 .as_ref()
538 .ok_or_else(|| "No hash stored in ArtifactRef".to_string())?;
539
540 Ok(IntegrityHash::compute(content) == *stored_hash)
541 }
542
543 #[must_use]
545 pub fn content_eq(&self, other: &Self) -> Option<bool> {
546 match (&self.hash, &other.hash) {
547 (Some(a), Some(b)) => Some(a == b),
548 _ => None,
549 }
550 }
551
552 #[must_use]
554 pub fn is_expired(&self) -> bool {
555 if let Some(expires_at) = self.expires_at {
556 expires_at < Utc::now()
557 } else {
558 false
559 }
560 }
561}
562
563fn default_header_version() -> u32 {
564 1
565}
566
567pub const CURRENT_HEADER_VERSION: u32 = 1;
569
570#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
588pub struct Header {
589 object_id: Uuid,
591 object_type: ObjectType,
593 #[serde(default = "default_header_version")]
596 header_version: u32,
597 schema_version: u32,
599 created_at: DateTime<Utc>,
601 updated_at: DateTime<Utc>,
607 created_by: ActorRef,
609 visibility: Visibility,
611 #[serde(default)]
613 tags: HashMap<String, String>,
614 #[serde(default)]
616 external_ids: HashMap<String, String>,
617 #[serde(default)]
619 checksum: Option<IntegrityHash>,
620}
621
622impl<'de> Deserialize<'de> for Header {
627 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
628 where
629 D: serde::Deserializer<'de>,
630 {
631 #[derive(Deserialize)]
632 struct RawHeader {
633 object_id: Uuid,
634 object_type: ObjectType,
635 #[serde(default = "default_header_version")]
636 header_version: u32,
637 schema_version: u32,
638 created_at: DateTime<Utc>,
639 updated_at: Option<DateTime<Utc>>,
640 created_by: ActorRef,
641 visibility: Visibility,
642 #[serde(default)]
643 tags: HashMap<String, String>,
644 #[serde(default)]
645 external_ids: HashMap<String, String>,
646 #[serde(default)]
647 checksum: Option<IntegrityHash>,
648 }
649
650 let raw = RawHeader::deserialize(deserializer)?;
651 Ok(Header {
652 object_id: raw.object_id,
653 object_type: raw.object_type,
654 header_version: raw.header_version,
655 schema_version: raw.schema_version,
656 created_at: raw.created_at,
657 updated_at: raw.updated_at.unwrap_or(raw.created_at),
658 created_by: raw.created_by,
659 visibility: raw.visibility,
660 tags: raw.tags,
661 external_ids: raw.external_ids,
662 checksum: raw.checksum,
663 })
664 }
665}
666
667impl Header {
668 pub fn new(object_type: ObjectType, created_by: ActorRef) -> Result<Self, String> {
675 let now = Utc::now();
676 Ok(Self {
677 object_id: Uuid::now_v7(),
678 object_type,
679 header_version: CURRENT_HEADER_VERSION,
680 schema_version: 1,
681 created_at: now,
682 updated_at: now,
683 created_by,
684 visibility: Visibility::Private,
685 tags: HashMap::new(),
686 external_ids: HashMap::new(),
687 checksum: None,
688 })
689 }
690
691 pub fn object_id(&self) -> Uuid {
692 self.object_id
693 }
694
695 pub fn object_type(&self) -> &ObjectType {
696 &self.object_type
697 }
698
699 pub fn header_version(&self) -> u32 {
700 self.header_version
701 }
702
703 pub fn schema_version(&self) -> u32 {
704 self.schema_version
705 }
706
707 pub fn created_at(&self) -> DateTime<Utc> {
708 self.created_at
709 }
710
711 pub fn updated_at(&self) -> DateTime<Utc> {
712 self.updated_at
713 }
714
715 pub fn created_by(&self) -> &ActorRef {
716 &self.created_by
717 }
718
719 pub fn visibility(&self) -> &Visibility {
720 &self.visibility
721 }
722
723 pub fn tags(&self) -> &HashMap<String, String> {
724 &self.tags
725 }
726
727 pub fn tags_mut(&mut self) -> &mut HashMap<String, String> {
728 &mut self.tags
729 }
730
731 pub fn external_ids(&self) -> &HashMap<String, String> {
732 &self.external_ids
733 }
734
735 pub fn external_ids_mut(&mut self) -> &mut HashMap<String, String> {
736 &mut self.external_ids
737 }
738
739 pub fn set_object_id(&mut self, object_id: Uuid) {
740 self.object_id = object_id;
741 }
742
743 pub fn set_object_type(&mut self, object_type: ObjectType) -> Result<(), String> {
744 self.object_type = object_type;
745 Ok(())
746 }
747
748 pub fn set_header_version(&mut self, header_version: u32) -> Result<(), String> {
749 if header_version == 0 {
750 return Err("header_version must be greater than 0".to_string());
751 }
752 self.header_version = header_version;
753 Ok(())
754 }
755
756 pub fn set_schema_version(&mut self, schema_version: u32) -> Result<(), String> {
757 if schema_version == 0 {
758 return Err("schema_version must be greater than 0".to_string());
759 }
760 self.schema_version = schema_version;
761 Ok(())
762 }
763
764 pub fn set_created_at(&mut self, created_at: DateTime<Utc>) {
765 self.created_at = created_at;
766 }
767
768 pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) {
769 self.updated_at = updated_at;
770 }
771
772 pub fn set_visibility(&mut self, visibility: Visibility) {
773 self.visibility = visibility;
774 }
775
776 pub fn checksum(&self) -> Option<&IntegrityHash> {
778 self.checksum.as_ref()
779 }
780
781 pub fn seal<T: Serialize>(&mut self, object: &T) -> Result<(), serde_json::Error> {
788 let previous_checksum = self.checksum.take();
789 match compute_integrity_hash(object) {
790 Ok(checksum) => {
791 self.checksum = Some(checksum);
792 self.updated_at = Utc::now();
793 Ok(())
794 }
795 Err(err) => {
796 self.checksum = previous_checksum;
797 Err(err)
798 }
799 }
800 }
801}
802
803#[cfg(test)]
804mod tests {
805 use chrono::{DateTime, Utc};
806 use uuid::Uuid;
807
808 use crate::internal::object::types::{
809 ActorKind, ActorRef, ArtifactRef, Header, IntegrityHash, ObjectType,
810 };
811
812 #[test]
814 fn test_object_type_to_data() {
815 let blob = ObjectType::Blob;
816 let blob_bytes = blob.to_data().unwrap();
817 assert_eq!(blob_bytes, vec![0x62, 0x6c, 0x6f, 0x62]);
818 }
819
820 #[test]
822 fn test_object_type_from_string() {
823 assert_eq!(ObjectType::from_string("blob").unwrap(), ObjectType::Blob);
824 assert_eq!(ObjectType::from_string("tree").unwrap(), ObjectType::Tree);
825 assert_eq!(
826 ObjectType::from_string("commit").unwrap(),
827 ObjectType::Commit
828 );
829 assert_eq!(ObjectType::from_string("tag").unwrap(), ObjectType::Tag);
830 assert_eq!(
831 ObjectType::from_string("snapshot").unwrap(),
832 ObjectType::ContextSnapshot
833 );
834 assert_eq!(
835 ObjectType::from_string("decision").unwrap(),
836 ObjectType::Decision
837 );
838 assert_eq!(
839 ObjectType::from_string("evidence").unwrap(),
840 ObjectType::Evidence
841 );
842 assert_eq!(
843 ObjectType::from_string("patchset").unwrap(),
844 ObjectType::PatchSet
845 );
846 assert_eq!(ObjectType::from_string("plan").unwrap(), ObjectType::Plan);
847 assert_eq!(
848 ObjectType::from_string("provenance").unwrap(),
849 ObjectType::Provenance
850 );
851 assert_eq!(ObjectType::from_string("run").unwrap(), ObjectType::Run);
852 assert_eq!(ObjectType::from_string("task").unwrap(), ObjectType::Task);
853 assert_eq!(
854 ObjectType::from_string("invocation").unwrap(),
855 ObjectType::ToolInvocation
856 );
857
858 assert!(ObjectType::from_string("invalid_type").is_err());
859 }
860
861 #[test]
863 fn test_object_type_to_u8() {
864 let commit = ObjectType::Commit;
865 let commit_number = commit.to_u8();
866 assert_eq!(commit_number, 1);
867 }
868
869 #[test]
871 fn test_object_type_from_u8() {
872 let tag_number = 4;
873 let tag = ObjectType::from_u8(tag_number).unwrap();
874 assert_eq!(tag, ObjectType::Tag);
875 }
876
877 #[test]
878 fn test_header_serialization() {
879 let actor = ActorRef::human("jackie").expect("actor");
880 let header = Header::new(ObjectType::Task, actor).expect("header");
881
882 let json = serde_json::to_string(&header).unwrap();
883 let deserialized: Header = serde_json::from_str(&json).unwrap();
884
885 assert_eq!(header.object_id(), deserialized.object_id());
886 assert_eq!(header.object_type(), deserialized.object_type());
887 assert_eq!(header.header_version(), deserialized.header_version());
888 }
889
890 #[test]
891 fn test_header_version_new_uses_current() {
892 let actor = ActorRef::human("jackie").expect("actor");
893 let header = Header::new(ObjectType::Task, actor).expect("header");
894 assert_eq!(
895 header.header_version(),
896 crate::internal::object::types::CURRENT_HEADER_VERSION
897 );
898 }
899
900 #[test]
901 fn test_header_version_defaults_on_missing() {
902 let json = r#"{
904 "object_id": "01234567-89ab-cdef-0123-456789abcdef",
905 "object_type": "task",
906 "schema_version": 1,
907 "created_at": "2026-01-01T00:00:00Z",
908 "updated_at": "2026-01-01T00:00:00Z",
909 "created_by": {"kind": "human", "id": "jackie"},
910 "visibility": "private"
911 }"#;
912 let header: Header = serde_json::from_str(json).unwrap();
913 assert_eq!(header.header_version(), 1);
914 }
915
916 #[test]
917 fn test_header_version_setter_rejects_zero() {
918 let actor = ActorRef::human("jackie").expect("actor");
919 let mut header = Header::new(ObjectType::Task, actor).expect("header");
920 assert!(header.set_header_version(0).is_err());
921 assert!(header.set_header_version(3).is_ok());
922 assert_eq!(header.header_version(), 3);
923 }
924
925 #[test]
926 fn test_actor_ref() {
927 let actor = ActorRef::agent("coder").expect("actor");
928 assert_eq!(actor.kind(), &ActorKind::Agent);
929 assert_eq!(actor.id(), "coder");
930
931 let sys = ActorRef::system("scheduler").expect("system");
932 assert_eq!(sys.kind(), &ActorKind::System);
933
934 let client = ActorRef::mcp_client("vscode").expect("client");
935 assert_eq!(client.kind(), &ActorKind::McpClient);
936 assert!(client.ensure_mcp_client().is_ok());
937
938 let non_mcp = ActorRef::human("jackie").expect("actor");
939 assert!(non_mcp.ensure_mcp_client().is_err());
940 }
941
942 #[test]
943 fn test_actor_kind_serialization() {
944 let k = ActorKind::McpClient;
945 let s = serde_json::to_string(&k).unwrap();
946 assert_eq!(s, "\"mcp_client\"");
947
948 let k2: ActorKind = serde_json::from_str("\"system\"").unwrap();
949 assert_eq!(k2, ActorKind::System);
950 }
951
952 #[test]
953 fn test_header_checksum() {
954 let actor = ActorRef::human("jackie").expect("actor");
955 let mut header = Header::new(ObjectType::Task, actor).expect("header");
956 header.set_created_at(
958 DateTime::parse_from_rfc3339("2026-02-10T00:00:00Z")
959 .unwrap()
960 .with_timezone(&Utc),
961 );
962 header.set_object_id(Uuid::from_u128(0x00000000000000000000000000000001));
963
964 let checksum =
965 crate::internal::object::integrity::compute_integrity_hash(&header).expect("checksum");
966 assert_eq!(checksum.to_hex().len(), 64); header
970 .set_object_type(ObjectType::Run)
971 .expect("object_type");
972 let checksum2 =
973 crate::internal::object::integrity::compute_integrity_hash(&header).expect("checksum");
974 assert_ne!(checksum, checksum2);
975 }
976
977 #[test]
978 fn test_artifact_checksum() {
979 let content = b"hello world";
980 let hash = ArtifactRef::compute_hash(content);
981 let expected_str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
983 assert_eq!(hash.to_hex(), expected_str);
984
985 let artifact = ArtifactRef::new("s3", "key")
986 .expect("artifact")
987 .with_hash(hash);
988 assert_eq!(artifact.hash(), Some(&hash));
989
990 assert!(artifact.verify_integrity(content).unwrap());
992 assert!(!artifact.verify_integrity(b"wrong").unwrap());
993
994 let artifact2 = ArtifactRef::new("local", "other/path")
996 .expect("artifact")
997 .with_hash(IntegrityHash::compute(content));
998 assert_eq!(artifact.content_eq(&artifact2), Some(true));
999
1000 let artifact3 = ArtifactRef::new("s3", "diff")
1001 .expect("artifact")
1002 .with_hash(ArtifactRef::compute_hash(b"diff"));
1003 assert_eq!(artifact.content_eq(&artifact3), Some(false));
1004 }
1005
1006 #[test]
1007 fn test_invalid_checksum() {
1008 let result = ArtifactRef::new("s3", "key")
1009 .expect("artifact")
1010 .with_hash_hex("bad_hash");
1011 assert!(result.is_err());
1012 }
1013
1014 #[test]
1015 fn test_header_seal() {
1016 let actor = ActorRef::human("jackie").expect("actor");
1017 let mut header = Header::new(ObjectType::Task, actor).expect("header");
1018
1019 let content = serde_json::json!({"key": "value"});
1020 header.seal(&content).expect("seal");
1021
1022 assert!(header.checksum().is_some());
1023 let expected =
1024 crate::internal::object::integrity::compute_integrity_hash(&content).expect("checksum");
1025 assert_eq!(header.checksum().expect("checksum"), &expected);
1026 }
1027
1028 #[test]
1029 fn test_header_updated_at_on_seal() {
1030 let actor = ActorRef::human("jackie").expect("actor");
1031 let mut header = Header::new(ObjectType::Task, actor).expect("header");
1032
1033 let before = header.updated_at();
1034 let content = serde_json::json!({"key": "value"});
1035
1036 header.seal(&content).expect("seal");
1037
1038 let after = header.updated_at();
1039 assert!(after >= before);
1040 }
1041
1042 #[test]
1043 fn test_empty_actor_id() {
1044 let result = ActorRef::new(ActorKind::Human, " ");
1045 assert!(result.is_err());
1046 }
1047
1048 #[test]
1049 fn test_artifact_expiration() {
1050 let mut artifact = ArtifactRef::new("s3", "key").expect("artifact");
1051 assert!(!artifact.is_expired());
1052
1053 artifact.set_expires_at(Some(Utc::now() - chrono::Duration::hours(1)));
1054 assert!(artifact.is_expired());
1055
1056 artifact.set_expires_at(Some(Utc::now() + chrono::Duration::hours(1)));
1057 assert!(!artifact.is_expired());
1058 }
1059}