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(PartialEq, Eq, Hash, Debug, Clone, Copy, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum ObjectType {
41 Commit = 1,
43 Tree,
45 Blob,
47 Tag,
49 OffsetZstdelta,
51 OffsetDelta,
53 HashDelta,
55 ContextSnapshot,
57 Decision,
59 Evidence,
61 PatchSet,
63 Plan,
65 Provenance,
67 Run,
69 Task,
71 Intent,
73 ToolInvocation,
75 ContextFrame,
77 IntentEvent,
79 TaskEvent,
81 RunEvent,
83 PlanStepEvent,
85 RunUsage,
87}
88
89const COMMIT_OBJECT_TYPE: &[u8] = b"commit";
93const TREE_OBJECT_TYPE: &[u8] = b"tree";
94const BLOB_OBJECT_TYPE: &[u8] = b"blob";
95const TAG_OBJECT_TYPE: &[u8] = b"tag";
96const CONTEXT_SNAPSHOT_OBJECT_TYPE: &[u8] = b"snapshot";
97const DECISION_OBJECT_TYPE: &[u8] = b"decision";
98const EVIDENCE_OBJECT_TYPE: &[u8] = b"evidence";
99const PATCH_SET_OBJECT_TYPE: &[u8] = b"patchset";
100const PLAN_OBJECT_TYPE: &[u8] = b"plan";
101const PROVENANCE_OBJECT_TYPE: &[u8] = b"provenance";
102const RUN_OBJECT_TYPE: &[u8] = b"run";
103const TASK_OBJECT_TYPE: &[u8] = b"task";
104const INTENT_OBJECT_TYPE: &[u8] = b"intent";
105const TOOL_INVOCATION_OBJECT_TYPE: &[u8] = b"invocation";
106const CONTEXT_FRAME_OBJECT_TYPE: &[u8] = b"context_frame";
107const INTENT_EVENT_OBJECT_TYPE: &[u8] = b"intent_event";
108const TASK_EVENT_OBJECT_TYPE: &[u8] = b"task_event";
109const RUN_EVENT_OBJECT_TYPE: &[u8] = b"run_event";
110const PLAN_STEP_EVENT_OBJECT_TYPE: &[u8] = b"plan_step_event";
111const RUN_USAGE_OBJECT_TYPE: &[u8] = b"run_usage";
112
113impl Display for ObjectType {
114 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
115 match self {
116 ObjectType::Blob => write!(f, "blob"),
117 ObjectType::Tree => write!(f, "tree"),
118 ObjectType::Commit => write!(f, "commit"),
119 ObjectType::Tag => write!(f, "tag"),
120 ObjectType::OffsetZstdelta => write!(f, "OffsetZstdelta"),
121 ObjectType::OffsetDelta => write!(f, "OffsetDelta"),
122 ObjectType::HashDelta => write!(f, "HashDelta"),
123 ObjectType::ContextSnapshot => write!(f, "snapshot"),
124 ObjectType::Decision => write!(f, "decision"),
125 ObjectType::Evidence => write!(f, "evidence"),
126 ObjectType::PatchSet => write!(f, "patchset"),
127 ObjectType::Plan => write!(f, "plan"),
128 ObjectType::Provenance => write!(f, "provenance"),
129 ObjectType::Run => write!(f, "run"),
130 ObjectType::Task => write!(f, "task"),
131 ObjectType::Intent => write!(f, "intent"),
132 ObjectType::ToolInvocation => write!(f, "invocation"),
133 ObjectType::ContextFrame => write!(f, "context_frame"),
134 ObjectType::IntentEvent => write!(f, "intent_event"),
135 ObjectType::TaskEvent => write!(f, "task_event"),
136 ObjectType::RunEvent => write!(f, "run_event"),
137 ObjectType::PlanStepEvent => write!(f, "plan_step_event"),
138 ObjectType::RunUsage => write!(f, "run_usage"),
139 }
140 }
141}
142
143impl ObjectType {
144 pub fn to_pack_type_u8(&self) -> Result<u8, GitError> {
149 match self {
150 ObjectType::Commit => Ok(1),
151 ObjectType::Tree => Ok(2),
152 ObjectType::Blob => Ok(3),
153 ObjectType::Tag => Ok(4),
154 ObjectType::OffsetZstdelta => Ok(5),
155 ObjectType::OffsetDelta => Ok(6),
156 ObjectType::HashDelta => Ok(7),
157 _ => Err(GitError::PackEncodeError(format!(
158 "object type `{}` cannot be encoded in pack header type bits",
159 self
160 ))),
161 }
162 }
163
164 pub fn from_pack_type_u8(number: u8) -> Result<ObjectType, GitError> {
166 match number {
167 1 => Ok(ObjectType::Commit),
168 2 => Ok(ObjectType::Tree),
169 3 => Ok(ObjectType::Blob),
170 4 => Ok(ObjectType::Tag),
171 5 => Ok(ObjectType::OffsetZstdelta),
172 6 => Ok(ObjectType::OffsetDelta),
173 7 => Ok(ObjectType::HashDelta),
174 _ => Err(GitError::InvalidObjectType(format!(
175 "Invalid pack object type number: {number}"
176 ))),
177 }
178 }
179
180 pub fn to_bytes(&self) -> Option<&[u8]> {
185 match self {
186 ObjectType::Commit => Some(COMMIT_OBJECT_TYPE),
187 ObjectType::Tree => Some(TREE_OBJECT_TYPE),
188 ObjectType::Blob => Some(BLOB_OBJECT_TYPE),
189 ObjectType::Tag => Some(TAG_OBJECT_TYPE),
190 ObjectType::ContextSnapshot => Some(CONTEXT_SNAPSHOT_OBJECT_TYPE),
191 ObjectType::Decision => Some(DECISION_OBJECT_TYPE),
192 ObjectType::Evidence => Some(EVIDENCE_OBJECT_TYPE),
193 ObjectType::PatchSet => Some(PATCH_SET_OBJECT_TYPE),
194 ObjectType::Plan => Some(PLAN_OBJECT_TYPE),
195 ObjectType::Provenance => Some(PROVENANCE_OBJECT_TYPE),
196 ObjectType::Run => Some(RUN_OBJECT_TYPE),
197 ObjectType::Task => Some(TASK_OBJECT_TYPE),
198 ObjectType::Intent => Some(INTENT_OBJECT_TYPE),
199 ObjectType::ToolInvocation => Some(TOOL_INVOCATION_OBJECT_TYPE),
200 ObjectType::ContextFrame => Some(CONTEXT_FRAME_OBJECT_TYPE),
201 ObjectType::IntentEvent => Some(INTENT_EVENT_OBJECT_TYPE),
202 ObjectType::TaskEvent => Some(TASK_EVENT_OBJECT_TYPE),
203 ObjectType::RunEvent => Some(RUN_EVENT_OBJECT_TYPE),
204 ObjectType::PlanStepEvent => Some(PLAN_STEP_EVENT_OBJECT_TYPE),
205 ObjectType::RunUsage => Some(RUN_USAGE_OBJECT_TYPE),
206 ObjectType::OffsetDelta | ObjectType::HashDelta | ObjectType::OffsetZstdelta => None,
207 }
208 }
209
210 pub fn from_string(s: &str) -> Result<ObjectType, GitError> {
212 match s {
213 "blob" => Ok(ObjectType::Blob),
214 "tree" => Ok(ObjectType::Tree),
215 "commit" => Ok(ObjectType::Commit),
216 "tag" => Ok(ObjectType::Tag),
217 "snapshot" => Ok(ObjectType::ContextSnapshot),
218 "decision" => Ok(ObjectType::Decision),
219 "evidence" => Ok(ObjectType::Evidence),
220 "patchset" => Ok(ObjectType::PatchSet),
221 "plan" => Ok(ObjectType::Plan),
222 "provenance" => Ok(ObjectType::Provenance),
223 "run" => Ok(ObjectType::Run),
224 "task" => Ok(ObjectType::Task),
225 "intent" => Ok(ObjectType::Intent),
226 "invocation" => Ok(ObjectType::ToolInvocation),
227 "context_frame" => Ok(ObjectType::ContextFrame),
228 "intent_event" => Ok(ObjectType::IntentEvent),
229 "task_event" => Ok(ObjectType::TaskEvent),
230 "run_event" => Ok(ObjectType::RunEvent),
231 "plan_step_event" => Ok(ObjectType::PlanStepEvent),
232 "run_usage" => Ok(ObjectType::RunUsage),
233 _ => Err(GitError::InvalidObjectType(s.to_string())),
234 }
235 }
236
237 pub fn to_data(self) -> Result<Vec<u8>, GitError> {
241 match self {
242 ObjectType::Blob => Ok(b"blob".to_vec()),
243 ObjectType::Tree => Ok(b"tree".to_vec()),
244 ObjectType::Commit => Ok(b"commit".to_vec()),
245 ObjectType::Tag => Ok(b"tag".to_vec()),
246 ObjectType::ContextSnapshot => Ok(b"snapshot".to_vec()),
247 ObjectType::Decision => Ok(b"decision".to_vec()),
248 ObjectType::Evidence => Ok(b"evidence".to_vec()),
249 ObjectType::PatchSet => Ok(b"patchset".to_vec()),
250 ObjectType::Plan => Ok(b"plan".to_vec()),
251 ObjectType::Provenance => Ok(b"provenance".to_vec()),
252 ObjectType::Run => Ok(b"run".to_vec()),
253 ObjectType::Task => Ok(b"task".to_vec()),
254 ObjectType::Intent => Ok(b"intent".to_vec()),
255 ObjectType::ToolInvocation => Ok(b"invocation".to_vec()),
256 ObjectType::ContextFrame => Ok(b"context_frame".to_vec()),
257 ObjectType::IntentEvent => Ok(b"intent_event".to_vec()),
258 ObjectType::TaskEvent => Ok(b"task_event".to_vec()),
259 ObjectType::RunEvent => Ok(b"run_event".to_vec()),
260 ObjectType::PlanStepEvent => Ok(b"plan_step_event".to_vec()),
261 ObjectType::RunUsage => Ok(b"run_usage".to_vec()),
262 _ => Err(GitError::InvalidObjectType(self.to_string())),
263 }
264 }
265
266 pub fn to_u8(&self) -> u8 {
271 match self {
272 ObjectType::Commit => 1,
273 ObjectType::Tree => 2,
274 ObjectType::Blob => 3,
275 ObjectType::Tag => 4,
276 ObjectType::OffsetZstdelta => 5,
277 ObjectType::OffsetDelta => 6,
278 ObjectType::HashDelta => 7,
279 ObjectType::ContextSnapshot => 8,
280 ObjectType::Decision => 9,
281 ObjectType::Evidence => 10,
282 ObjectType::PatchSet => 11,
283 ObjectType::Plan => 12,
284 ObjectType::Provenance => 13,
285 ObjectType::Run => 14,
286 ObjectType::Task => 15,
287 ObjectType::Intent => 16,
288 ObjectType::ToolInvocation => 17,
289 ObjectType::ContextFrame => 18,
290 ObjectType::IntentEvent => 19,
291 ObjectType::TaskEvent => 20,
292 ObjectType::RunEvent => 21,
293 ObjectType::PlanStepEvent => 22,
294 ObjectType::RunUsage => 23,
295 }
296 }
297
298 pub fn from_u8(number: u8) -> Result<ObjectType, GitError> {
300 match number {
301 1 => Ok(ObjectType::Commit),
302 2 => Ok(ObjectType::Tree),
303 3 => Ok(ObjectType::Blob),
304 4 => Ok(ObjectType::Tag),
305 5 => Ok(ObjectType::OffsetZstdelta),
306 6 => Ok(ObjectType::OffsetDelta),
307 7 => Ok(ObjectType::HashDelta),
308 8 => Ok(ObjectType::ContextSnapshot),
309 9 => Ok(ObjectType::Decision),
310 10 => Ok(ObjectType::Evidence),
311 11 => Ok(ObjectType::PatchSet),
312 12 => Ok(ObjectType::Plan),
313 13 => Ok(ObjectType::Provenance),
314 14 => Ok(ObjectType::Run),
315 15 => Ok(ObjectType::Task),
316 16 => Ok(ObjectType::Intent),
317 17 => Ok(ObjectType::ToolInvocation),
318 18 => Ok(ObjectType::ContextFrame),
319 19 => Ok(ObjectType::IntentEvent),
320 20 => Ok(ObjectType::TaskEvent),
321 21 => Ok(ObjectType::RunEvent),
322 22 => Ok(ObjectType::PlanStepEvent),
323 23 => Ok(ObjectType::RunUsage),
324 _ => Err(GitError::InvalidObjectType(format!(
325 "Invalid object type number: {number}"
326 ))),
327 }
328 }
329
330 pub fn is_base(&self) -> bool {
332 matches!(
333 self,
334 ObjectType::Commit | ObjectType::Tree | ObjectType::Blob | ObjectType::Tag
335 )
336 }
337
338 pub fn is_ai_object(&self) -> bool {
340 matches!(
341 self,
342 ObjectType::ContextSnapshot
343 | ObjectType::Decision
344 | ObjectType::Evidence
345 | ObjectType::PatchSet
346 | ObjectType::Plan
347 | ObjectType::Provenance
348 | ObjectType::Run
349 | ObjectType::Task
350 | ObjectType::Intent
351 | ObjectType::ToolInvocation
352 | ObjectType::ContextFrame
353 | ObjectType::IntentEvent
354 | ObjectType::TaskEvent
355 | ObjectType::RunEvent
356 | ObjectType::PlanStepEvent
357 | ObjectType::RunUsage
358 )
359 }
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
364#[serde(rename_all = "snake_case")]
365pub enum ActorKind {
366 Human,
368 Agent,
370 System,
372 McpClient,
374 #[serde(untagged)]
376 Other(String),
377}
378
379impl fmt::Display for ActorKind {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 match self {
382 ActorKind::Human => write!(f, "human"),
383 ActorKind::Agent => write!(f, "agent"),
384 ActorKind::System => write!(f, "system"),
385 ActorKind::McpClient => write!(f, "mcp_client"),
386 ActorKind::Other(s) => write!(f, "{}", s),
387 }
388 }
389}
390
391impl From<String> for ActorKind {
392 fn from(s: String) -> Self {
393 match s.as_str() {
394 "human" => ActorKind::Human,
395 "agent" => ActorKind::Agent,
396 "system" => ActorKind::System,
397 "mcp_client" => ActorKind::McpClient,
398 _ => ActorKind::Other(s),
399 }
400 }
401}
402
403impl From<&str> for ActorKind {
404 fn from(s: &str) -> Self {
405 match s {
406 "human" => ActorKind::Human,
407 "agent" => ActorKind::Agent,
408 "system" => ActorKind::System,
409 "mcp_client" => ActorKind::McpClient,
410 _ => ActorKind::Other(s.to_string()),
411 }
412 }
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
417#[serde(deny_unknown_fields)]
418pub struct ActorRef {
419 kind: ActorKind,
421 id: String,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
425 display_name: Option<String>,
426}
427
428impl ActorRef {
429 pub fn new(kind: impl Into<ActorKind>, id: impl Into<String>) -> Result<Self, String> {
434 let id = id.into();
435 if id.trim().is_empty() {
436 return Err("actor id cannot be empty".to_string());
437 }
438 Ok(Self {
439 kind: kind.into(),
440 id,
441 display_name: None,
442 })
443 }
444
445 pub fn human(id: impl Into<String>) -> Result<Self, String> {
447 Self::new(ActorKind::Human, id)
448 }
449
450 pub fn agent(id: impl Into<String>) -> Result<Self, String> {
452 Self::new(ActorKind::Agent, id)
453 }
454
455 pub fn system(id: impl Into<String>) -> Result<Self, String> {
457 Self::new(ActorKind::System, id)
458 }
459
460 pub fn mcp_client(id: impl Into<String>) -> Result<Self, String> {
462 Self::new(ActorKind::McpClient, id)
463 }
464
465 pub fn kind(&self) -> &ActorKind {
467 &self.kind
468 }
469
470 pub fn id(&self) -> &str {
472 &self.id
473 }
474
475 pub fn display_name(&self) -> Option<&str> {
477 self.display_name.as_deref()
478 }
479
480 pub fn set_display_name(&mut self, display_name: Option<String>) {
482 self.display_name = display_name;
483 }
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
488#[serde(deny_unknown_fields)]
489pub struct ArtifactRef {
490 store: String,
492 key: String,
494}
495
496impl ArtifactRef {
497 pub fn new(store: impl Into<String>, key: impl Into<String>) -> Result<Self, String> {
502 let store = store.into();
503 let key = key.into();
504 if store.trim().is_empty() {
505 return Err("artifact store cannot be empty".to_string());
506 }
507 if key.trim().is_empty() {
508 return Err("artifact key cannot be empty".to_string());
509 }
510 Ok(Self { store, key })
511 }
512
513 pub fn store(&self) -> &str {
515 &self.store
516 }
517
518 pub fn key(&self) -> &str {
520 &self.key
521 }
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
526#[serde(deny_unknown_fields)]
527pub struct Header {
528 object_id: Uuid,
530 object_type: ObjectType,
532 version: u8,
534 created_at: DateTime<Utc>,
536 created_by: ActorRef,
538}
539
540const CURRENT_HEADER_VERSION: u8 = 1;
542
543impl Header {
544 pub fn new(object_type: ObjectType, created_by: ActorRef) -> Result<Self, String> {
546 Ok(Self {
547 object_id: Uuid::now_v7(),
548 object_type,
549 version: CURRENT_HEADER_VERSION,
550 created_at: Utc::now(),
551 created_by,
552 })
553 }
554
555 pub fn object_id(&self) -> Uuid {
557 self.object_id
558 }
559
560 pub fn object_type(&self) -> &ObjectType {
562 &self.object_type
563 }
564
565 pub fn version(&self) -> u8 {
567 self.version
568 }
569
570 pub fn created_at(&self) -> DateTime<Utc> {
572 self.created_at
573 }
574
575 pub fn created_by(&self) -> &ActorRef {
577 &self.created_by
578 }
579
580 pub fn set_version(&mut self, version: u8) -> Result<(), String> {
585 if version == 0 {
586 return Err("header version must be non-zero".to_string());
587 }
588 self.version = version;
589 Ok(())
590 }
591
592 pub fn checksum(&self) -> IntegrityHash {
597 let bytes = serde_json::to_vec(self).expect("header serialization");
598 IntegrityHash::compute(&bytes)
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
613 fn test_actor_kind_serialization() {
614 let value = serde_json::to_string(&ActorKind::McpClient).expect("serialize");
617 assert_eq!(value, "\"mcp_client\"");
618 }
619
620 #[test]
621 fn test_actor_ref() {
622 let mut actor = ActorRef::human("alice").expect("actor");
625 actor.set_display_name(Some("Alice".to_string()));
626
627 assert_eq!(actor.kind(), &ActorKind::Human);
628 assert_eq!(actor.id(), "alice");
629 assert_eq!(actor.display_name(), Some("Alice"));
630 }
631
632 #[test]
633 fn test_empty_actor_id() {
634 let err = ActorRef::human(" ").expect_err("empty actor id must fail");
637 assert!(err.contains("actor id"));
638 }
639
640 #[test]
641 fn test_header_serialization() {
642 let actor = ActorRef::human("alice").expect("actor");
645 let header = Header::new(ObjectType::Intent, actor).expect("header");
646 let json = serde_json::to_value(&header).expect("serialize");
647
648 assert_eq!(json["object_type"], "intent");
649 assert_eq!(json["version"], 1);
650 }
651
652 #[test]
653 fn test_header_version_new_uses_current() {
654 let actor = ActorRef::human("alice").expect("actor");
657 let header = Header::new(ObjectType::Plan, actor).expect("header");
658 assert_eq!(header.version(), CURRENT_HEADER_VERSION);
659 }
660
661 #[test]
662 fn test_header_version_setter_rejects_zero() {
663 let actor = ActorRef::human("alice").expect("actor");
666 let mut header = Header::new(ObjectType::Task, actor).expect("header");
667 let err = header.set_version(0).expect_err("zero must fail");
668 assert!(err.contains("non-zero"));
669 }
670
671 #[test]
672 fn test_header_checksum() {
673 let actor = ActorRef::human("alice").expect("actor");
676 let header = Header::new(ObjectType::Run, actor).expect("header");
677 assert!(!header.checksum().to_hex().is_empty());
678 }
679
680 #[test]
681 fn test_object_type_from_u8() {
682 assert_eq!(
685 ObjectType::from_u8(18).expect("type"),
686 ObjectType::ContextFrame
687 );
688 }
689
690 #[test]
691 fn test_object_type_to_u8() {
692 assert_eq!(ObjectType::RunUsage.to_u8(), 23);
695 }
696
697 #[test]
698 fn test_object_type_from_string() {
699 assert_eq!(
702 ObjectType::from_string("plan_step_event").expect("type"),
703 ObjectType::PlanStepEvent
704 );
705 }
706
707 #[test]
708 fn test_object_type_to_data() {
709 assert_eq!(
712 ObjectType::IntentEvent.to_data().expect("data"),
713 b"intent_event".to_vec()
714 );
715 }
716
717 const ALL_VARIANTS: &[ObjectType] = &[
722 ObjectType::Commit,
723 ObjectType::Tree,
724 ObjectType::Blob,
725 ObjectType::Tag,
726 ObjectType::OffsetZstdelta,
727 ObjectType::OffsetDelta,
728 ObjectType::HashDelta,
729 ObjectType::ContextSnapshot,
730 ObjectType::Decision,
731 ObjectType::Evidence,
732 ObjectType::PatchSet,
733 ObjectType::Plan,
734 ObjectType::Provenance,
735 ObjectType::Run,
736 ObjectType::Task,
737 ObjectType::Intent,
738 ObjectType::ToolInvocation,
739 ObjectType::ContextFrame,
740 ObjectType::IntentEvent,
741 ObjectType::TaskEvent,
742 ObjectType::RunEvent,
743 ObjectType::PlanStepEvent,
744 ObjectType::RunUsage,
745 ];
746
747 #[test]
748 fn test_to_u8_from_u8_round_trip() {
749 for variant in ALL_VARIANTS {
752 let n = variant.to_u8();
753 let recovered = ObjectType::from_u8(n)
754 .unwrap_or_else(|_| panic!("from_u8({n}) failed for {variant}"));
755 assert_eq!(
756 *variant, recovered,
757 "to_u8/from_u8 round-trip mismatch for {variant}"
758 );
759 }
760 }
761
762 #[test]
763 fn test_display_from_string_round_trip() {
764 let skip = [
769 ObjectType::OffsetZstdelta,
770 ObjectType::OffsetDelta,
771 ObjectType::HashDelta,
772 ];
773 for variant in ALL_VARIANTS {
774 if skip.contains(variant) {
775 continue;
776 }
777 let s = variant.to_string();
778 let recovered = ObjectType::from_string(&s)
779 .unwrap_or_else(|_| panic!("from_string({s:?}) failed for {variant}"));
780 assert_eq!(
781 *variant, recovered,
782 "Display/from_string round-trip mismatch for {variant}"
783 );
784 }
785 }
786
787 #[test]
788 fn test_to_bytes_to_data_consistency() {
789 for variant in ALL_VARIANTS {
792 if let Some(bytes) = variant.to_bytes() {
793 let data = variant
794 .to_data()
795 .unwrap_or_else(|_| panic!("to_data failed for {variant}"));
796 assert_eq!(bytes, &data[..], "to_bytes/to_data mismatch for {variant}");
797 }
798 }
799 }
800
801 #[test]
802 fn test_all_variants_count() {
803 assert_eq!(
809 ALL_VARIANTS.len(),
810 23,
811 "ALL_VARIANTS count mismatch — did you add a new ObjectType variant?"
812 );
813 }
814
815 #[test]
816 fn test_invalid_checksum() {
817 let err = ObjectType::from_string("unknown").expect_err("must fail");
820 assert!(matches!(err, GitError::InvalidObjectType(_)));
821 }
822
823 #[test]
824 fn test_artifact_checksum() {
825 let artifact = ArtifactRef::new("local", "artifact-key").expect("artifact");
828 assert_eq!(artifact.store(), "local");
829 assert_eq!(artifact.key(), "artifact-key");
830 }
831
832 #[test]
833 fn test_artifact_expiration() {
834 let artifact = ArtifactRef::new("s3", "bucket/key").expect("artifact");
837 assert_eq!(artifact.store(), "s3");
838 assert_eq!(artifact.key(), "bucket/key");
839 }
840}