Skip to main content

git_internal/internal/object/
types.rs

1//! Object type, actor, artifact, and header primitives.
2//!
3//! This module centralizes the low-level metadata shared across the
4//! object layer:
5//!
6//! - `ObjectType` defines every persisted object family and the
7//!   conversions needed by pack encoding, loose-object style headers,
8//!   JSON payloads, and internal numeric ids.
9//! - `ActorKind` and `ActorRef` identify who created an object.
10//! - `ArtifactRef` links an object to content stored outside the Git
11//!   object database.
12//! - `Header` is the immutable metadata envelope embedded into AI
13//!   objects such as `Intent`, `Plan`, or `Run`.
14//!
15//! One important distinction in this module is that object types live in
16//! multiple identifier spaces:
17//!
18//! - Git pack header type bits support only core Git types and delta
19//!   entries.
20//! - String/byte names are used for textual encodings and AI object
21//!   persistence.
22//! - `to_u8` / `from_u8` provide a crate-local stable numeric mapping
23//!   that covers every variant.
24
25use 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/// Canonical object kind shared across Git-native and AI-native objects.
35///
36/// The first seven variants mirror Git pack semantics. The remaining
37/// variants describe the application's AI workflow objects.
38#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum ObjectType {
41    /// A Git commit object.
42    Commit = 1,
43    /// A Git tree object.
44    Tree,
45    /// A Git blob object.
46    Blob,
47    /// A Git tag object.
48    Tag,
49    /// A pack entry encoded as a zstd-compressed offset delta.
50    OffsetZstdelta,
51    /// A pack entry encoded as an offset delta.
52    OffsetDelta,
53    /// A pack entry encoded as a reference delta.
54    HashDelta,
55    /// A captured slice of conversational or execution context.
56    ContextSnapshot,
57    /// A recorded decision made by an agent or system.
58    Decision,
59    /// Supporting evidence attached to a decision or plan.
60    Evidence,
61    /// A persisted set of file or content changes.
62    PatchSet,
63    /// A multi-step plan derived from an intent.
64    Plan,
65    /// Provenance metadata for generated outputs or workflow state.
66    Provenance,
67    /// A concrete run/execution of an agent workflow.
68    Run,
69    /// A task belonging to a run or plan.
70    Task,
71    /// An immutable revision of the user's request.
72    Intent,
73    /// A persisted record of a tool call.
74    ToolInvocation,
75    /// A frame of structured context injected into execution.
76    ContextFrame,
77    /// A lifecycle event attached to an intent.
78    IntentEvent,
79    /// A lifecycle event attached to a task.
80    TaskEvent,
81    /// A lifecycle event attached to a run.
82    RunEvent,
83    /// A lifecycle event attached to an individual plan step.
84    PlanStepEvent,
85    /// Usage/accounting information recorded for a run.
86    RunUsage,
87}
88
89// Canonical byte labels used when an object type needs a stable textual
90// wire representation. Delta variants intentionally do not have entries
91// here because they are pack-only encodings rather than named objects.
92const 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    /// Convert to the 3-bit pack header type used by Git pack entries.
145    ///
146    /// Only Git-native base objects and delta encodings are valid in
147    /// this space. AI objects do not have a pack-header representation.
148    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    /// Parse a Git pack header type number into an `ObjectType`.
165    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    /// Return the canonical borrowed byte label for named object types.
181    ///
182    /// Delta entries return `None` because they are represented by pack
183    /// type bits rather than textual object names.
184    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    /// Parse the canonical textual object name used in persisted data.
211    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    /// Return the canonical textual object name as owned bytes.
238    ///
239    /// This is the owned allocation counterpart to `to_bytes()`.
240    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    /// Convert to the crate-local stable numeric identifier.
267    ///
268    /// Unlike pack type bits, this mapping covers every variant in the
269    /// enum, including AI object kinds.
270    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    /// Parse the crate-local stable numeric identifier.
299    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    /// Return `true` when the type is one of the four base Git objects.
331    pub fn is_base(&self) -> bool {
332        matches!(
333            self,
334            ObjectType::Commit | ObjectType::Tree | ObjectType::Blob | ObjectType::Tag
335        )
336    }
337
338    /// Return `true` when the type belongs to the AI object family.
339    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/// High-level category of the actor that created an object.
363#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
364#[serde(rename_all = "snake_case")]
365pub enum ActorKind {
366    /// A human user.
367    Human,
368    /// An autonomous or semi-autonomous agent.
369    Agent,
370    /// A platform-managed system actor.
371    System,
372    /// An external MCP client acting through the platform.
373    McpClient,
374    /// A forward-compatible custom actor label.
375    #[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/// Reference to the actor that created or owns an object.
416#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
417#[serde(deny_unknown_fields)]
418pub struct ActorRef {
419    /// Coarse actor category used for routing and display.
420    kind: ActorKind,
421    /// Stable actor identifier within the actor kind namespace.
422    id: String,
423    /// Optional human-friendly label for logs or UIs.
424    #[serde(default, skip_serializing_if = "Option::is_none")]
425    display_name: Option<String>,
426}
427
428impl ActorRef {
429    /// Create a new actor reference.
430    ///
431    /// Empty or whitespace-only ids are rejected because object headers
432    /// rely on this identity being present and stable.
433    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    /// Convenience constructor for a human actor.
446    pub fn human(id: impl Into<String>) -> Result<Self, String> {
447        Self::new(ActorKind::Human, id)
448    }
449
450    /// Convenience constructor for an agent actor.
451    pub fn agent(id: impl Into<String>) -> Result<Self, String> {
452        Self::new(ActorKind::Agent, id)
453    }
454
455    /// Convenience constructor for a system actor.
456    pub fn system(id: impl Into<String>) -> Result<Self, String> {
457        Self::new(ActorKind::System, id)
458    }
459
460    /// Convenience constructor for an MCP client actor.
461    pub fn mcp_client(id: impl Into<String>) -> Result<Self, String> {
462        Self::new(ActorKind::McpClient, id)
463    }
464
465    /// Return the actor category.
466    pub fn kind(&self) -> &ActorKind {
467        &self.kind
468    }
469
470    /// Return the stable actor id.
471    pub fn id(&self) -> &str {
472        &self.id
473    }
474
475    /// Return the optional UI/display label.
476    pub fn display_name(&self) -> Option<&str> {
477        self.display_name.as_deref()
478    }
479
480    /// Set or clear the optional UI/display label.
481    pub fn set_display_name(&mut self, display_name: Option<String>) {
482        self.display_name = display_name;
483    }
484}
485
486/// Reference to an artifact stored outside the object database.
487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
488#[serde(deny_unknown_fields)]
489pub struct ArtifactRef {
490    /// External store identifier, such as `s3` or `local`.
491    store: String,
492    /// Store-specific lookup key.
493    key: String,
494}
495
496impl ArtifactRef {
497    /// Create a new external artifact reference.
498    ///
499    /// Both store and key must be non-empty so the reference remains
500    /// resolvable after persistence.
501    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    /// Return the external store identifier.
514    pub fn store(&self) -> &str {
515        &self.store
516    }
517
518    /// Return the store-specific lookup key.
519    pub fn key(&self) -> &str {
520        &self.key
521    }
522}
523
524/// Shared immutable metadata header embedded into AI objects.
525#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
526#[serde(deny_unknown_fields)]
527pub struct Header {
528    /// Unique object id generated at creation time.
529    object_id: Uuid,
530    /// The persisted object kind.
531    object_type: ObjectType,
532    /// Schema/header version for forward compatibility.
533    version: u8,
534    /// Creation timestamp captured in UTC.
535    created_at: DateTime<Utc>,
536    /// Actor that created the object.
537    created_by: ActorRef,
538}
539
540/// Current header schema version written by new objects.
541const CURRENT_HEADER_VERSION: u8 = 1;
542
543impl Header {
544    /// Build a fresh header for a new AI object instance.
545    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    /// Return the immutable object id.
556    pub fn object_id(&self) -> Uuid {
557        self.object_id
558    }
559
560    /// Return the persisted object kind.
561    pub fn object_type(&self) -> &ObjectType {
562        &self.object_type
563    }
564
565    /// Return the header schema version.
566    pub fn version(&self) -> u8 {
567        self.version
568    }
569
570    /// Return the creation timestamp in UTC.
571    pub fn created_at(&self) -> DateTime<Utc> {
572        self.created_at
573    }
574
575    /// Return the actor that created the object.
576    pub fn created_by(&self) -> &ActorRef {
577        &self.created_by
578    }
579
580    /// Override the header schema version for migration/testing paths.
581    ///
582    /// Version `0` is reserved as invalid so callers cannot accidentally
583    /// produce an uninitialized-looking header.
584    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    /// Compute an integrity hash over the serialized header payload.
593    ///
594    /// This is useful when callers need a compact fingerprint of the
595    /// immutable metadata without hashing the full enclosing object.
596    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    // Coverage:
607    // - actor kind serde shape and actor reference validation
608    // - header defaults, serialization, version rules, and checksum generation
609    // - object-type conversions across string, bytes, pack ids, and internal ids
610    // - artifact reference construction for different backing stores
611
612    #[test]
613    fn test_actor_kind_serialization() {
614        // Scenario: built-in actor kinds must serialize to the canonical
615        // snake_case wire value expected by persisted JSON payloads.
616        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        // Scenario: a valid actor reference preserves kind/id and allows
623        // an optional display name to be attached for UI use.
624        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        // Scenario: whitespace-only actor ids are rejected so headers
635        // cannot be created with missing creator identity.
636        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        // Scenario: a serialized header emits the canonical object type
643        // string and the default schema version for new objects.
644        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        // Scenario: newly created headers always start at the current
655        // schema version constant rather than requiring manual setup.
656        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        // Scenario: callers cannot downgrade the header version to the
664        // reserved invalid value `0`.
665        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        // Scenario: checksum generation succeeds for a normal header and
674        // yields a non-empty digest string.
675        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        // Scenario: the internal numeric object type id maps back to the
683        // expected AI object variant.
684        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        // Scenario: AI-only variants still have a stable internal numeric
693        // id even though they are not valid Git pack header types.
694        assert_eq!(ObjectType::RunUsage.to_u8(), 23);
695    }
696
697    #[test]
698    fn test_object_type_from_string() {
699        // Scenario: persisted textual type labels decode to the matching
700        // enum variant for event-style AI objects.
701        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        // Scenario: textual serialization to owned bytes uses the same
710        // canonical label stored in object payloads.
711        assert_eq!(
712            ObjectType::IntentEvent.to_data().expect("data"),
713            b"intent_event".to_vec()
714        );
715    }
716
717    /// All `ObjectType` variants for exhaustive conversion coverage.
718    ///
719    /// Update this list whenever a new enum variant is added so the
720    /// round-trip tests continue to exercise the full surface area.
721    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        // Scenario: every variant can make a full round-trip through the
750        // crate-local numeric id without loss of information.
751        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        // Scenario: every named object type round-trips through
765        // Display/from_string; delta variants are excluded because they
766        // intentionally have no textual name parser.
767        // Delta types have no string representation in from_string, skip them.
768        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        // Scenario: borrowed and owned textual encodings stay identical
790        // for every object type that has a canonical byte label.
791        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        // Scenario: the exhaustive variant list stays in sync with the
804        // enum definition, preventing silent coverage gaps in the
805        // round-trip tests above.
806        // If you add a new ObjectType variant, add it to ALL_VARIANTS above
807        // and update this count.
808        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        // Scenario: unknown textual object type labels fail with the
818        // expected invalid-type error instead of defaulting silently.
819        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        // Scenario: an artifact reference backed by the local store keeps
826        // the caller-provided store/key pair unchanged after creation.
827        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        // Scenario: artifact references are storage-agnostic, so an S3
835        // style store/key pair is accepted and exposed unchanged.
836        let artifact = ArtifactRef::new("s3", "bucket/key").expect("artifact");
837        assert_eq!(artifact.store(), "s3");
838        assert_eq!(artifact.key(), "bucket/key");
839    }
840}