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(
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    /// A Git commit object.
54    Commit = 1,
55    /// A Git tree object.
56    Tree,
57    /// A Git blob object.
58    Blob,
59    /// A Git tag object.
60    Tag,
61    /// A pack entry encoded as a zstd-compressed offset delta.
62    OffsetZstdelta,
63    /// A pack entry encoded as an offset delta.
64    OffsetDelta,
65    /// A pack entry encoded as a reference delta.
66    HashDelta,
67    /// A captured slice of conversational or execution context.
68    ContextSnapshot,
69    /// A recorded decision made by an agent or system.
70    Decision,
71    /// Supporting evidence attached to a decision or plan.
72    Evidence,
73    /// A persisted set of file or content changes.
74    PatchSet,
75    /// A multi-step plan derived from an intent.
76    Plan,
77    /// Provenance metadata for generated outputs or workflow state.
78    Provenance,
79    /// A concrete run/execution of an agent workflow.
80    Run,
81    /// A task belonging to a run or plan.
82    Task,
83    /// An immutable revision of the user's request.
84    Intent,
85    /// A persisted record of a tool call.
86    ToolInvocation,
87    /// A frame of structured context injected into execution.
88    ContextFrame,
89    /// A lifecycle event attached to an intent.
90    IntentEvent,
91    /// A lifecycle event attached to a task.
92    TaskEvent,
93    /// A lifecycle event attached to a run.
94    RunEvent,
95    /// A lifecycle event attached to an individual plan step.
96    PlanStepEvent,
97    /// Usage/accounting information recorded for a run.
98    RunUsage,
99}
100
101// Canonical byte labels used when an object type needs a stable textual
102// wire representation. Delta variants intentionally do not have entries
103// here because they are pack-only encodings rather than named objects.
104const 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    /// Convert to the 3-bit pack header type used by Git pack entries.
157    ///
158    /// Only Git-native base objects and delta encodings are valid in
159    /// this space. AI objects do not have a pack-header representation.
160    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    /// Parse a Git pack header type number into an `ObjectType`.
177    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    /// Return the canonical borrowed byte label for named object types.
193    ///
194    /// Delta entries return `None` because they are represented by pack
195    /// type bits rather than textual object names.
196    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    /// Parse the canonical textual object name used in persisted data.
223    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    /// Return the canonical textual object name as owned bytes.
250    ///
251    /// This is the owned allocation counterpart to `to_bytes()`.
252    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    /// Convert to the crate-local stable numeric identifier.
279    ///
280    /// Unlike pack type bits, this mapping covers every variant in the
281    /// enum, including AI object kinds.
282    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    /// Parse the crate-local stable numeric identifier.
311    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    /// Return `true` when the type is one of the four base Git objects.
343    pub fn is_base(&self) -> bool {
344        matches!(
345            self,
346            ObjectType::Commit | ObjectType::Tree | ObjectType::Blob | ObjectType::Tag
347        )
348    }
349
350    /// Return `true` when the type belongs to the AI object family.
351    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/// High-level category of the actor that created an object.
375#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
376#[serde(rename_all = "snake_case")]
377pub enum ActorKind {
378    /// A human user.
379    Human,
380    /// An autonomous or semi-autonomous agent.
381    Agent,
382    /// A platform-managed system actor.
383    System,
384    /// An external MCP client acting through the platform.
385    McpClient,
386    /// A forward-compatible custom actor label.
387    #[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/// Reference to the actor that created or owns an object.
428#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
429#[serde(deny_unknown_fields)]
430pub struct ActorRef {
431    /// Coarse actor category used for routing and display.
432    kind: ActorKind,
433    /// Stable actor identifier within the actor kind namespace.
434    id: String,
435    /// Optional human-friendly label for logs or UIs.
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    display_name: Option<String>,
438}
439
440impl ActorRef {
441    /// Create a new actor reference.
442    ///
443    /// Empty or whitespace-only ids are rejected because object headers
444    /// rely on this identity being present and stable.
445    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    /// Convenience constructor for a human actor.
458    pub fn human(id: impl Into<String>) -> Result<Self, String> {
459        Self::new(ActorKind::Human, id)
460    }
461
462    /// Convenience constructor for an agent actor.
463    pub fn agent(id: impl Into<String>) -> Result<Self, String> {
464        Self::new(ActorKind::Agent, id)
465    }
466
467    /// Convenience constructor for a system actor.
468    pub fn system(id: impl Into<String>) -> Result<Self, String> {
469        Self::new(ActorKind::System, id)
470    }
471
472    /// Convenience constructor for an MCP client actor.
473    pub fn mcp_client(id: impl Into<String>) -> Result<Self, String> {
474        Self::new(ActorKind::McpClient, id)
475    }
476
477    /// Return the actor category.
478    pub fn kind(&self) -> &ActorKind {
479        &self.kind
480    }
481
482    /// Return the stable actor id.
483    pub fn id(&self) -> &str {
484        &self.id
485    }
486
487    /// Return the optional UI/display label.
488    pub fn display_name(&self) -> Option<&str> {
489        self.display_name.as_deref()
490    }
491
492    /// Set or clear the optional UI/display label.
493    pub fn set_display_name(&mut self, display_name: Option<String>) {
494        self.display_name = display_name;
495    }
496}
497
498/// Reference to an artifact stored outside the object database.
499#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
500#[serde(deny_unknown_fields)]
501pub struct ArtifactRef {
502    /// External store identifier, such as `s3` or `local`.
503    store: String,
504    /// Store-specific lookup key.
505    key: String,
506}
507
508impl ArtifactRef {
509    /// Create a new external artifact reference.
510    ///
511    /// Both store and key must be non-empty so the reference remains
512    /// resolvable after persistence.
513    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    /// Return the external store identifier.
526    pub fn store(&self) -> &str {
527        &self.store
528    }
529
530    /// Return the store-specific lookup key.
531    pub fn key(&self) -> &str {
532        &self.key
533    }
534}
535
536/// Shared immutable metadata header embedded into AI objects.
537#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
538#[serde(deny_unknown_fields)]
539pub struct Header {
540    /// Unique object id generated at creation time.
541    object_id: Uuid,
542    /// The persisted object kind.
543    object_type: ObjectType,
544    /// Schema/header version for forward compatibility.
545    version: u8,
546    /// Creation timestamp captured in UTC.
547    created_at: DateTime<Utc>,
548    /// Actor that created the object.
549    created_by: ActorRef,
550}
551
552/// Current header schema version written by new objects.
553const CURRENT_HEADER_VERSION: u8 = 1;
554
555impl Header {
556    /// Build a fresh header for a new AI object instance.
557    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    /// Return the immutable object id.
568    pub fn object_id(&self) -> Uuid {
569        self.object_id
570    }
571
572    /// Return the persisted object kind.
573    pub fn object_type(&self) -> &ObjectType {
574        &self.object_type
575    }
576
577    /// Return the header schema version.
578    pub fn version(&self) -> u8 {
579        self.version
580    }
581
582    /// Return the creation timestamp in UTC.
583    pub fn created_at(&self) -> DateTime<Utc> {
584        self.created_at
585    }
586
587    /// Return the actor that created the object.
588    pub fn created_by(&self) -> &ActorRef {
589        &self.created_by
590    }
591
592    /// Override the header schema version for migration/testing paths.
593    ///
594    /// Version `0` is reserved as invalid so callers cannot accidentally
595    /// produce an uninitialized-looking header.
596    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    /// Compute an integrity hash over the serialized header payload.
605    ///
606    /// This is useful when callers need a compact fingerprint of the
607    /// immutable metadata without hashing the full enclosing object.
608    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    // Coverage:
619    // - actor kind serde shape and actor reference validation
620    // - header defaults, serialization, version rules, and checksum generation
621    // - object-type conversions across string, bytes, pack ids, and internal ids
622    // - artifact reference construction for different backing stores
623
624    #[test]
625    fn test_actor_kind_serialization() {
626        // Scenario: built-in actor kinds must serialize to the canonical
627        // snake_case wire value expected by persisted JSON payloads.
628        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        // Scenario: a valid actor reference preserves kind/id and allows
635        // an optional display name to be attached for UI use.
636        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        // Scenario: whitespace-only actor ids are rejected so headers
647        // cannot be created with missing creator identity.
648        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        // Scenario: a serialized header emits the canonical object type
655        // string and the default schema version for new objects.
656        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        // Scenario: newly created headers always start at the current
667        // schema version constant rather than requiring manual setup.
668        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        // Scenario: callers cannot downgrade the header version to the
676        // reserved invalid value `0`.
677        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        // Scenario: checksum generation succeeds for a normal header and
686        // yields a non-empty digest string.
687        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        // Scenario: the internal numeric object type id maps back to the
695        // expected AI object variant.
696        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        // Scenario: AI-only variants still have a stable internal numeric
705        // id even though they are not valid Git pack header types.
706        assert_eq!(ObjectType::RunUsage.to_u8(), 23);
707    }
708
709    #[test]
710    fn test_object_type_from_string() {
711        // Scenario: persisted textual type labels decode to the matching
712        // enum variant for event-style AI objects.
713        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        // Scenario: textual serialization to owned bytes uses the same
722        // canonical label stored in object payloads.
723        assert_eq!(
724            ObjectType::IntentEvent.to_data().expect("data"),
725            b"intent_event".to_vec()
726        );
727    }
728
729    /// All `ObjectType` variants for exhaustive conversion coverage.
730    ///
731    /// Update this list whenever a new enum variant is added so the
732    /// round-trip tests continue to exercise the full surface area.
733    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        // Scenario: every variant can make a full round-trip through the
762        // crate-local numeric id without loss of information.
763        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        // Scenario: every named object type round-trips through
777        // Display/from_string; delta variants are excluded because they
778        // intentionally have no textual name parser.
779        // Delta types have no string representation in from_string, skip them.
780        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        // Scenario: borrowed and owned textual encodings stay identical
802        // for every object type that has a canonical byte label.
803        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        // Scenario: the exhaustive variant list stays in sync with the
816        // enum definition, preventing silent coverage gaps in the
817        // round-trip tests above.
818        // If you add a new ObjectType variant, add it to ALL_VARIANTS above
819        // and update this count.
820        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        // Scenario: unknown textual object type labels fail with the
830        // expected invalid-type error instead of defaulting silently.
831        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        // Scenario: an artifact reference backed by the local store keeps
838        // the caller-provided store/key pair unchanged after creation.
839        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        // Scenario: artifact references are storage-agnostic, so an S3
847        // style store/key pair is accepted and exposed unchanged.
848        let artifact = ArtifactRef::new("s3", "bucket/key").expect("artifact");
849        assert_eq!(artifact.store(), "s3");
850        assert_eq!(artifact.key(), "bucket/key");
851    }
852}