1use std::fmt::{self, Display};
26
27use chrono::{DateTime, Utc};
28use serde::{Deserialize, Serialize};
29use uuid::Uuid;
30
31use super::integrity::IntegrityHash;
32use crate::errors::GitError;
33
34#[derive(
39 PartialEq,
40 Eq,
41 Hash,
42 Debug,
43 Clone,
44 Copy,
45 serde::Serialize,
46 serde::Deserialize,
47 rkyv::Archive,
48 rkyv::Serialize,
49 rkyv::Deserialize,
50)]
51#[serde(rename_all = "snake_case")]
52pub enum ObjectType {
53 Commit = 1,
55 Tree,
57 Blob,
59 Tag,
61 OffsetZstdelta,
63 OffsetDelta,
65 HashDelta,
67 ContextSnapshot,
69 Decision,
71 Evidence,
73 PatchSet,
75 Plan,
77 Provenance,
79 Run,
81 Task,
83 Intent,
85 ToolInvocation,
87 ContextFrame,
89 IntentEvent,
91 TaskEvent,
93 RunEvent,
95 PlanStepEvent,
97 RunUsage,
99}
100
101const COMMIT_OBJECT_TYPE: &[u8] = b"commit";
105const TREE_OBJECT_TYPE: &[u8] = b"tree";
106const BLOB_OBJECT_TYPE: &[u8] = b"blob";
107const TAG_OBJECT_TYPE: &[u8] = b"tag";
108const CONTEXT_SNAPSHOT_OBJECT_TYPE: &[u8] = b"snapshot";
109const DECISION_OBJECT_TYPE: &[u8] = b"decision";
110const EVIDENCE_OBJECT_TYPE: &[u8] = b"evidence";
111const PATCH_SET_OBJECT_TYPE: &[u8] = b"patchset";
112const PLAN_OBJECT_TYPE: &[u8] = b"plan";
113const PROVENANCE_OBJECT_TYPE: &[u8] = b"provenance";
114const RUN_OBJECT_TYPE: &[u8] = b"run";
115const TASK_OBJECT_TYPE: &[u8] = b"task";
116const INTENT_OBJECT_TYPE: &[u8] = b"intent";
117const TOOL_INVOCATION_OBJECT_TYPE: &[u8] = b"invocation";
118const CONTEXT_FRAME_OBJECT_TYPE: &[u8] = b"context_frame";
119const INTENT_EVENT_OBJECT_TYPE: &[u8] = b"intent_event";
120const TASK_EVENT_OBJECT_TYPE: &[u8] = b"task_event";
121const RUN_EVENT_OBJECT_TYPE: &[u8] = b"run_event";
122const PLAN_STEP_EVENT_OBJECT_TYPE: &[u8] = b"plan_step_event";
123const RUN_USAGE_OBJECT_TYPE: &[u8] = b"run_usage";
124
125impl Display for ObjectType {
126 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
127 match self {
128 ObjectType::Blob => write!(f, "blob"),
129 ObjectType::Tree => write!(f, "tree"),
130 ObjectType::Commit => write!(f, "commit"),
131 ObjectType::Tag => write!(f, "tag"),
132 ObjectType::OffsetZstdelta => write!(f, "OffsetZstdelta"),
133 ObjectType::OffsetDelta => write!(f, "OffsetDelta"),
134 ObjectType::HashDelta => write!(f, "HashDelta"),
135 ObjectType::ContextSnapshot => write!(f, "snapshot"),
136 ObjectType::Decision => write!(f, "decision"),
137 ObjectType::Evidence => write!(f, "evidence"),
138 ObjectType::PatchSet => write!(f, "patchset"),
139 ObjectType::Plan => write!(f, "plan"),
140 ObjectType::Provenance => write!(f, "provenance"),
141 ObjectType::Run => write!(f, "run"),
142 ObjectType::Task => write!(f, "task"),
143 ObjectType::Intent => write!(f, "intent"),
144 ObjectType::ToolInvocation => write!(f, "invocation"),
145 ObjectType::ContextFrame => write!(f, "context_frame"),
146 ObjectType::IntentEvent => write!(f, "intent_event"),
147 ObjectType::TaskEvent => write!(f, "task_event"),
148 ObjectType::RunEvent => write!(f, "run_event"),
149 ObjectType::PlanStepEvent => write!(f, "plan_step_event"),
150 ObjectType::RunUsage => write!(f, "run_usage"),
151 }
152 }
153}
154
155impl ObjectType {
156 pub fn to_pack_type_u8(&self) -> Result<u8, GitError> {
161 match self {
162 ObjectType::Commit => Ok(1),
163 ObjectType::Tree => Ok(2),
164 ObjectType::Blob => Ok(3),
165 ObjectType::Tag => Ok(4),
166 ObjectType::OffsetZstdelta => Ok(5),
167 ObjectType::OffsetDelta => Ok(6),
168 ObjectType::HashDelta => Ok(7),
169 _ => Err(GitError::PackEncodeError(format!(
170 "object type `{}` cannot be encoded in pack header type bits",
171 self
172 ))),
173 }
174 }
175
176 pub fn from_pack_type_u8(number: u8) -> Result<ObjectType, GitError> {
178 match number {
179 1 => Ok(ObjectType::Commit),
180 2 => Ok(ObjectType::Tree),
181 3 => Ok(ObjectType::Blob),
182 4 => Ok(ObjectType::Tag),
183 5 => Ok(ObjectType::OffsetZstdelta),
184 6 => Ok(ObjectType::OffsetDelta),
185 7 => Ok(ObjectType::HashDelta),
186 _ => Err(GitError::InvalidObjectType(format!(
187 "Invalid pack object type number: {number}"
188 ))),
189 }
190 }
191
192 pub fn to_bytes(&self) -> Option<&[u8]> {
197 match self {
198 ObjectType::Commit => Some(COMMIT_OBJECT_TYPE),
199 ObjectType::Tree => Some(TREE_OBJECT_TYPE),
200 ObjectType::Blob => Some(BLOB_OBJECT_TYPE),
201 ObjectType::Tag => Some(TAG_OBJECT_TYPE),
202 ObjectType::ContextSnapshot => Some(CONTEXT_SNAPSHOT_OBJECT_TYPE),
203 ObjectType::Decision => Some(DECISION_OBJECT_TYPE),
204 ObjectType::Evidence => Some(EVIDENCE_OBJECT_TYPE),
205 ObjectType::PatchSet => Some(PATCH_SET_OBJECT_TYPE),
206 ObjectType::Plan => Some(PLAN_OBJECT_TYPE),
207 ObjectType::Provenance => Some(PROVENANCE_OBJECT_TYPE),
208 ObjectType::Run => Some(RUN_OBJECT_TYPE),
209 ObjectType::Task => Some(TASK_OBJECT_TYPE),
210 ObjectType::Intent => Some(INTENT_OBJECT_TYPE),
211 ObjectType::ToolInvocation => Some(TOOL_INVOCATION_OBJECT_TYPE),
212 ObjectType::ContextFrame => Some(CONTEXT_FRAME_OBJECT_TYPE),
213 ObjectType::IntentEvent => Some(INTENT_EVENT_OBJECT_TYPE),
214 ObjectType::TaskEvent => Some(TASK_EVENT_OBJECT_TYPE),
215 ObjectType::RunEvent => Some(RUN_EVENT_OBJECT_TYPE),
216 ObjectType::PlanStepEvent => Some(PLAN_STEP_EVENT_OBJECT_TYPE),
217 ObjectType::RunUsage => Some(RUN_USAGE_OBJECT_TYPE),
218 ObjectType::OffsetDelta | ObjectType::HashDelta | ObjectType::OffsetZstdelta => None,
219 }
220 }
221
222 pub fn from_string(s: &str) -> Result<ObjectType, GitError> {
224 match s {
225 "blob" => Ok(ObjectType::Blob),
226 "tree" => Ok(ObjectType::Tree),
227 "commit" => Ok(ObjectType::Commit),
228 "tag" => Ok(ObjectType::Tag),
229 "snapshot" => Ok(ObjectType::ContextSnapshot),
230 "decision" => Ok(ObjectType::Decision),
231 "evidence" => Ok(ObjectType::Evidence),
232 "patchset" => Ok(ObjectType::PatchSet),
233 "plan" => Ok(ObjectType::Plan),
234 "provenance" => Ok(ObjectType::Provenance),
235 "run" => Ok(ObjectType::Run),
236 "task" => Ok(ObjectType::Task),
237 "intent" => Ok(ObjectType::Intent),
238 "invocation" => Ok(ObjectType::ToolInvocation),
239 "context_frame" => Ok(ObjectType::ContextFrame),
240 "intent_event" => Ok(ObjectType::IntentEvent),
241 "task_event" => Ok(ObjectType::TaskEvent),
242 "run_event" => Ok(ObjectType::RunEvent),
243 "plan_step_event" => Ok(ObjectType::PlanStepEvent),
244 "run_usage" => Ok(ObjectType::RunUsage),
245 _ => Err(GitError::InvalidObjectType(s.to_string())),
246 }
247 }
248
249 pub fn to_data(self) -> Result<Vec<u8>, GitError> {
253 match self {
254 ObjectType::Blob => Ok(b"blob".to_vec()),
255 ObjectType::Tree => Ok(b"tree".to_vec()),
256 ObjectType::Commit => Ok(b"commit".to_vec()),
257 ObjectType::Tag => Ok(b"tag".to_vec()),
258 ObjectType::ContextSnapshot => Ok(b"snapshot".to_vec()),
259 ObjectType::Decision => Ok(b"decision".to_vec()),
260 ObjectType::Evidence => Ok(b"evidence".to_vec()),
261 ObjectType::PatchSet => Ok(b"patchset".to_vec()),
262 ObjectType::Plan => Ok(b"plan".to_vec()),
263 ObjectType::Provenance => Ok(b"provenance".to_vec()),
264 ObjectType::Run => Ok(b"run".to_vec()),
265 ObjectType::Task => Ok(b"task".to_vec()),
266 ObjectType::Intent => Ok(b"intent".to_vec()),
267 ObjectType::ToolInvocation => Ok(b"invocation".to_vec()),
268 ObjectType::ContextFrame => Ok(b"context_frame".to_vec()),
269 ObjectType::IntentEvent => Ok(b"intent_event".to_vec()),
270 ObjectType::TaskEvent => Ok(b"task_event".to_vec()),
271 ObjectType::RunEvent => Ok(b"run_event".to_vec()),
272 ObjectType::PlanStepEvent => Ok(b"plan_step_event".to_vec()),
273 ObjectType::RunUsage => Ok(b"run_usage".to_vec()),
274 _ => Err(GitError::InvalidObjectType(self.to_string())),
275 }
276 }
277
278 pub fn to_u8(&self) -> u8 {
283 match self {
284 ObjectType::Commit => 1,
285 ObjectType::Tree => 2,
286 ObjectType::Blob => 3,
287 ObjectType::Tag => 4,
288 ObjectType::OffsetZstdelta => 5,
289 ObjectType::OffsetDelta => 6,
290 ObjectType::HashDelta => 7,
291 ObjectType::ContextSnapshot => 8,
292 ObjectType::Decision => 9,
293 ObjectType::Evidence => 10,
294 ObjectType::PatchSet => 11,
295 ObjectType::Plan => 12,
296 ObjectType::Provenance => 13,
297 ObjectType::Run => 14,
298 ObjectType::Task => 15,
299 ObjectType::Intent => 16,
300 ObjectType::ToolInvocation => 17,
301 ObjectType::ContextFrame => 18,
302 ObjectType::IntentEvent => 19,
303 ObjectType::TaskEvent => 20,
304 ObjectType::RunEvent => 21,
305 ObjectType::PlanStepEvent => 22,
306 ObjectType::RunUsage => 23,
307 }
308 }
309
310 pub fn from_u8(number: u8) -> Result<ObjectType, GitError> {
312 match number {
313 1 => Ok(ObjectType::Commit),
314 2 => Ok(ObjectType::Tree),
315 3 => Ok(ObjectType::Blob),
316 4 => Ok(ObjectType::Tag),
317 5 => Ok(ObjectType::OffsetZstdelta),
318 6 => Ok(ObjectType::OffsetDelta),
319 7 => Ok(ObjectType::HashDelta),
320 8 => Ok(ObjectType::ContextSnapshot),
321 9 => Ok(ObjectType::Decision),
322 10 => Ok(ObjectType::Evidence),
323 11 => Ok(ObjectType::PatchSet),
324 12 => Ok(ObjectType::Plan),
325 13 => Ok(ObjectType::Provenance),
326 14 => Ok(ObjectType::Run),
327 15 => Ok(ObjectType::Task),
328 16 => Ok(ObjectType::Intent),
329 17 => Ok(ObjectType::ToolInvocation),
330 18 => Ok(ObjectType::ContextFrame),
331 19 => Ok(ObjectType::IntentEvent),
332 20 => Ok(ObjectType::TaskEvent),
333 21 => Ok(ObjectType::RunEvent),
334 22 => Ok(ObjectType::PlanStepEvent),
335 23 => Ok(ObjectType::RunUsage),
336 _ => Err(GitError::InvalidObjectType(format!(
337 "Invalid object type number: {number}"
338 ))),
339 }
340 }
341
342 pub fn is_base(&self) -> bool {
344 matches!(
345 self,
346 ObjectType::Commit | ObjectType::Tree | ObjectType::Blob | ObjectType::Tag
347 )
348 }
349
350 pub fn is_ai_object(&self) -> bool {
352 matches!(
353 self,
354 ObjectType::ContextSnapshot
355 | ObjectType::Decision
356 | ObjectType::Evidence
357 | ObjectType::PatchSet
358 | ObjectType::Plan
359 | ObjectType::Provenance
360 | ObjectType::Run
361 | ObjectType::Task
362 | ObjectType::Intent
363 | ObjectType::ToolInvocation
364 | ObjectType::ContextFrame
365 | ObjectType::IntentEvent
366 | ObjectType::TaskEvent
367 | ObjectType::RunEvent
368 | ObjectType::PlanStepEvent
369 | ObjectType::RunUsage
370 )
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
376#[serde(rename_all = "snake_case")]
377pub enum ActorKind {
378 Human,
380 Agent,
382 System,
384 McpClient,
386 #[serde(untagged)]
388 Other(String),
389}
390
391impl fmt::Display for ActorKind {
392 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393 match self {
394 ActorKind::Human => write!(f, "human"),
395 ActorKind::Agent => write!(f, "agent"),
396 ActorKind::System => write!(f, "system"),
397 ActorKind::McpClient => write!(f, "mcp_client"),
398 ActorKind::Other(s) => write!(f, "{}", s),
399 }
400 }
401}
402
403impl From<String> for ActorKind {
404 fn from(s: String) -> Self {
405 match s.as_str() {
406 "human" => ActorKind::Human,
407 "agent" => ActorKind::Agent,
408 "system" => ActorKind::System,
409 "mcp_client" => ActorKind::McpClient,
410 _ => ActorKind::Other(s),
411 }
412 }
413}
414
415impl From<&str> for ActorKind {
416 fn from(s: &str) -> Self {
417 match s {
418 "human" => ActorKind::Human,
419 "agent" => ActorKind::Agent,
420 "system" => ActorKind::System,
421 "mcp_client" => ActorKind::McpClient,
422 _ => ActorKind::Other(s.to_string()),
423 }
424 }
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
429#[serde(deny_unknown_fields)]
430pub struct ActorRef {
431 kind: ActorKind,
433 id: String,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
437 display_name: Option<String>,
438}
439
440impl ActorRef {
441 pub fn new(kind: impl Into<ActorKind>, id: impl Into<String>) -> Result<Self, String> {
446 let id = id.into();
447 if id.trim().is_empty() {
448 return Err("actor id cannot be empty".to_string());
449 }
450 Ok(Self {
451 kind: kind.into(),
452 id,
453 display_name: None,
454 })
455 }
456
457 pub fn human(id: impl Into<String>) -> Result<Self, String> {
459 Self::new(ActorKind::Human, id)
460 }
461
462 pub fn agent(id: impl Into<String>) -> Result<Self, String> {
464 Self::new(ActorKind::Agent, id)
465 }
466
467 pub fn system(id: impl Into<String>) -> Result<Self, String> {
469 Self::new(ActorKind::System, id)
470 }
471
472 pub fn mcp_client(id: impl Into<String>) -> Result<Self, String> {
474 Self::new(ActorKind::McpClient, id)
475 }
476
477 pub fn kind(&self) -> &ActorKind {
479 &self.kind
480 }
481
482 pub fn id(&self) -> &str {
484 &self.id
485 }
486
487 pub fn display_name(&self) -> Option<&str> {
489 self.display_name.as_deref()
490 }
491
492 pub fn set_display_name(&mut self, display_name: Option<String>) {
494 self.display_name = display_name;
495 }
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
500#[serde(deny_unknown_fields)]
501pub struct ArtifactRef {
502 store: String,
504 key: String,
506}
507
508impl ArtifactRef {
509 pub fn new(store: impl Into<String>, key: impl Into<String>) -> Result<Self, String> {
514 let store = store.into();
515 let key = key.into();
516 if store.trim().is_empty() {
517 return Err("artifact store cannot be empty".to_string());
518 }
519 if key.trim().is_empty() {
520 return Err("artifact key cannot be empty".to_string());
521 }
522 Ok(Self { store, key })
523 }
524
525 pub fn store(&self) -> &str {
527 &self.store
528 }
529
530 pub fn key(&self) -> &str {
532 &self.key
533 }
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
538#[serde(deny_unknown_fields)]
539pub struct Header {
540 object_id: Uuid,
542 object_type: ObjectType,
544 version: u8,
546 created_at: DateTime<Utc>,
548 created_by: ActorRef,
550}
551
552const CURRENT_HEADER_VERSION: u8 = 1;
554
555impl Header {
556 pub fn new(object_type: ObjectType, created_by: ActorRef) -> Result<Self, String> {
558 Ok(Self {
559 object_id: Uuid::now_v7(),
560 object_type,
561 version: CURRENT_HEADER_VERSION,
562 created_at: Utc::now(),
563 created_by,
564 })
565 }
566
567 pub fn object_id(&self) -> Uuid {
569 self.object_id
570 }
571
572 pub fn object_type(&self) -> &ObjectType {
574 &self.object_type
575 }
576
577 pub fn version(&self) -> u8 {
579 self.version
580 }
581
582 pub fn created_at(&self) -> DateTime<Utc> {
584 self.created_at
585 }
586
587 pub fn created_by(&self) -> &ActorRef {
589 &self.created_by
590 }
591
592 pub fn set_version(&mut self, version: u8) -> Result<(), String> {
597 if version == 0 {
598 return Err("header version must be non-zero".to_string());
599 }
600 self.version = version;
601 Ok(())
602 }
603
604 pub fn checksum(&self) -> IntegrityHash {
609 let bytes = serde_json::to_vec(self).expect("header serialization");
610 IntegrityHash::compute(&bytes)
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 #[test]
625 fn test_actor_kind_serialization() {
626 let value = serde_json::to_string(&ActorKind::McpClient).expect("serialize");
629 assert_eq!(value, "\"mcp_client\"");
630 }
631
632 #[test]
633 fn test_actor_ref() {
634 let mut actor = ActorRef::human("alice").expect("actor");
637 actor.set_display_name(Some("Alice".to_string()));
638
639 assert_eq!(actor.kind(), &ActorKind::Human);
640 assert_eq!(actor.id(), "alice");
641 assert_eq!(actor.display_name(), Some("Alice"));
642 }
643
644 #[test]
645 fn test_empty_actor_id() {
646 let err = ActorRef::human(" ").expect_err("empty actor id must fail");
649 assert!(err.contains("actor id"));
650 }
651
652 #[test]
653 fn test_header_serialization() {
654 let actor = ActorRef::human("alice").expect("actor");
657 let header = Header::new(ObjectType::Intent, actor).expect("header");
658 let json = serde_json::to_value(&header).expect("serialize");
659
660 assert_eq!(json["object_type"], "intent");
661 assert_eq!(json["version"], 1);
662 }
663
664 #[test]
665 fn test_header_version_new_uses_current() {
666 let actor = ActorRef::human("alice").expect("actor");
669 let header = Header::new(ObjectType::Plan, actor).expect("header");
670 assert_eq!(header.version(), CURRENT_HEADER_VERSION);
671 }
672
673 #[test]
674 fn test_header_version_setter_rejects_zero() {
675 let actor = ActorRef::human("alice").expect("actor");
678 let mut header = Header::new(ObjectType::Task, actor).expect("header");
679 let err = header.set_version(0).expect_err("zero must fail");
680 assert!(err.contains("non-zero"));
681 }
682
683 #[test]
684 fn test_header_checksum() {
685 let actor = ActorRef::human("alice").expect("actor");
688 let header = Header::new(ObjectType::Run, actor).expect("header");
689 assert!(!header.checksum().to_hex().is_empty());
690 }
691
692 #[test]
693 fn test_object_type_from_u8() {
694 assert_eq!(
697 ObjectType::from_u8(18).expect("type"),
698 ObjectType::ContextFrame
699 );
700 }
701
702 #[test]
703 fn test_object_type_to_u8() {
704 assert_eq!(ObjectType::RunUsage.to_u8(), 23);
707 }
708
709 #[test]
710 fn test_object_type_from_string() {
711 assert_eq!(
714 ObjectType::from_string("plan_step_event").expect("type"),
715 ObjectType::PlanStepEvent
716 );
717 }
718
719 #[test]
720 fn test_object_type_to_data() {
721 assert_eq!(
724 ObjectType::IntentEvent.to_data().expect("data"),
725 b"intent_event".to_vec()
726 );
727 }
728
729 const ALL_VARIANTS: &[ObjectType] = &[
734 ObjectType::Commit,
735 ObjectType::Tree,
736 ObjectType::Blob,
737 ObjectType::Tag,
738 ObjectType::OffsetZstdelta,
739 ObjectType::OffsetDelta,
740 ObjectType::HashDelta,
741 ObjectType::ContextSnapshot,
742 ObjectType::Decision,
743 ObjectType::Evidence,
744 ObjectType::PatchSet,
745 ObjectType::Plan,
746 ObjectType::Provenance,
747 ObjectType::Run,
748 ObjectType::Task,
749 ObjectType::Intent,
750 ObjectType::ToolInvocation,
751 ObjectType::ContextFrame,
752 ObjectType::IntentEvent,
753 ObjectType::TaskEvent,
754 ObjectType::RunEvent,
755 ObjectType::PlanStepEvent,
756 ObjectType::RunUsage,
757 ];
758
759 #[test]
760 fn test_to_u8_from_u8_round_trip() {
761 for variant in ALL_VARIANTS {
764 let n = variant.to_u8();
765 let recovered = ObjectType::from_u8(n)
766 .unwrap_or_else(|_| panic!("from_u8({n}) failed for {variant}"));
767 assert_eq!(
768 *variant, recovered,
769 "to_u8/from_u8 round-trip mismatch for {variant}"
770 );
771 }
772 }
773
774 #[test]
775 fn test_display_from_string_round_trip() {
776 let skip = [
781 ObjectType::OffsetZstdelta,
782 ObjectType::OffsetDelta,
783 ObjectType::HashDelta,
784 ];
785 for variant in ALL_VARIANTS {
786 if skip.contains(variant) {
787 continue;
788 }
789 let s = variant.to_string();
790 let recovered = ObjectType::from_string(&s)
791 .unwrap_or_else(|_| panic!("from_string({s:?}) failed for {variant}"));
792 assert_eq!(
793 *variant, recovered,
794 "Display/from_string round-trip mismatch for {variant}"
795 );
796 }
797 }
798
799 #[test]
800 fn test_to_bytes_to_data_consistency() {
801 for variant in ALL_VARIANTS {
804 if let Some(bytes) = variant.to_bytes() {
805 let data = variant
806 .to_data()
807 .unwrap_or_else(|_| panic!("to_data failed for {variant}"));
808 assert_eq!(bytes, &data[..], "to_bytes/to_data mismatch for {variant}");
809 }
810 }
811 }
812
813 #[test]
814 fn test_all_variants_count() {
815 assert_eq!(
821 ALL_VARIANTS.len(),
822 23,
823 "ALL_VARIANTS count mismatch — did you add a new ObjectType variant?"
824 );
825 }
826
827 #[test]
828 fn test_invalid_checksum() {
829 let err = ObjectType::from_string("unknown").expect_err("must fail");
832 assert!(matches!(err, GitError::InvalidObjectType(_)));
833 }
834
835 #[test]
836 fn test_artifact_checksum() {
837 let artifact = ArtifactRef::new("local", "artifact-key").expect("artifact");
840 assert_eq!(artifact.store(), "local");
841 assert_eq!(artifact.key(), "artifact-key");
842 }
843
844 #[test]
845 fn test_artifact_expiration() {
846 let artifact = ArtifactRef::new("s3", "bucket/key").expect("artifact");
849 assert_eq!(artifact.store(), "s3");
850 assert_eq!(artifact.key(), "bucket/key");
851 }
852}