Skip to main content

ipfrs_storage/
object_version_store.rs

1//! Versioned object store with full history, branching, and garbage collection.
2//!
3//! Maintains the complete history of content-addressed objects and supports
4//! point-in-time retrieval, branch management, and configurable GC policies.
5
6use std::collections::{HashMap, HashSet, VecDeque};
7
8// ---------------------------------------------------------------------------
9// Error type
10// ---------------------------------------------------------------------------
11
12/// Errors produced by [`ObjectVersionStore`].
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum VsError {
15    /// A requested version number does not exist.
16    VersionNotFound(u64),
17    /// A requested branch name does not exist.
18    BranchNotFound(String),
19    /// A branch with the given name already exists.
20    BranchAlreadyExists(String),
21    /// Store has reached its configured maximum number of versions.
22    MaxVersionsReached,
23    /// Store has reached its configured maximum number of branches.
24    MaxBranchesReached,
25}
26
27impl std::fmt::Display for VsError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            Self::VersionNotFound(v) => write!(f, "version {v} not found"),
31            Self::BranchNotFound(b) => write!(f, "branch '{b}' not found"),
32            Self::BranchAlreadyExists(b) => write!(f, "branch '{b}' already exists"),
33            Self::MaxVersionsReached => write!(f, "maximum number of versions reached"),
34            Self::MaxBranchesReached => write!(f, "maximum number of branches reached"),
35        }
36    }
37}
38
39impl std::error::Error for VsError {}
40
41// ---------------------------------------------------------------------------
42// Core domain types
43// ---------------------------------------------------------------------------
44
45/// A single version of an object in the store.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct OvsObjectVersion {
48    /// Monotonically increasing version number (global across all branches).
49    pub version: u64,
50    /// Content identifier — FNV-1a 64-bit hash of `data` as a hex string.
51    pub cid: String,
52    /// Raw object bytes.
53    pub data: Vec<u8>,
54    /// Optional link to the version this was derived from.
55    pub parent_version: Option<u64>,
56    /// Unix timestamp (seconds) when this version was created.
57    pub created_at: u64,
58    /// Arbitrary string tags.
59    pub tags: Vec<String>,
60    /// Byte length of `data`.
61    pub size_bytes: u64,
62}
63
64/// A named branch pointing to a specific head version.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct VersionBranch {
67    /// Branch name (unique within the store).
68    pub name: String,
69    /// Version number that is the current head of this branch.
70    pub head_version: u64,
71    /// Unix timestamp (seconds) when this branch was created.
72    pub created_at: u64,
73    /// Optional parent branch this branch was forked from.
74    pub parent_branch: Option<String>,
75}
76
77// ---------------------------------------------------------------------------
78// Query type
79// ---------------------------------------------------------------------------
80
81/// Specifies how to select a version from the store.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum VersionQuery {
84    /// The most recent version on the "main" branch.
85    Latest,
86    /// A specific version by number.
87    AtVersion(u64),
88    /// The latest version whose `created_at` is ≤ the given timestamp.
89    AtTime(u64),
90    /// The first version that carries the given tag.
91    Tagged(String),
92    /// The head version of the named branch.
93    OnBranch(String),
94}
95
96// ---------------------------------------------------------------------------
97// GC policy
98// ---------------------------------------------------------------------------
99
100/// Determines which versions are eligible for garbage collection.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum OvsGcPolicy {
103    /// Never delete any version.
104    KeepAll,
105    /// Keep only the last `n` versions (by version number); delete the rest
106    /// unless they are reachable from a branch head.
107    KeepLast(usize),
108    /// Delete versions with `created_at < t` unless reachable from a branch head.
109    KeepSince(u64),
110    /// Delete versions that have no tags and are not reachable from a branch head.
111    KeepTagged,
112}
113
114// ---------------------------------------------------------------------------
115// Configuration
116// ---------------------------------------------------------------------------
117
118/// Configuration for an [`ObjectVersionStore`].
119#[derive(Debug, Clone)]
120pub struct VersionStoreConfig {
121    /// Maximum number of live versions before GC is triggered automatically.
122    pub max_versions: usize,
123    /// Maximum number of branches allowed.
124    pub max_branches: usize,
125    /// Policy that drives which versions are eligible for deletion.
126    pub gc_policy: OvsGcPolicy,
127    /// Whether old versions should be compressed (currently reserved; not used
128    /// for actual compression but carried for forward-compatibility).
129    pub compress_old_versions: bool,
130}
131
132impl Default for VersionStoreConfig {
133    fn default() -> Self {
134        Self {
135            max_versions: 10_000,
136            max_branches: 100,
137            gc_policy: OvsGcPolicy::KeepAll,
138            compress_old_versions: false,
139        }
140    }
141}
142
143// ---------------------------------------------------------------------------
144// Stats
145// ---------------------------------------------------------------------------
146
147/// A point-in-time snapshot of store statistics.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct VsStats {
150    /// Number of live (non-deleted) versions.
151    pub version_count: usize,
152    /// Number of branches.
153    pub branch_count: usize,
154    /// Total bytes occupied by all live version payloads.
155    pub total_bytes: u64,
156    /// Cumulative count of versions removed by GC.
157    pub deleted_versions: u64,
158}
159
160// ---------------------------------------------------------------------------
161// The store itself
162// ---------------------------------------------------------------------------
163
164/// Versioned object store with branch management and configurable GC.
165///
166/// # Lifecycle
167///
168/// 1. Create with [`ObjectVersionStore::new`].
169/// 2. Commit new content with [`ObjectVersionStore::put`] or
170///    [`ObjectVersionStore::put_on_branch`].
171/// 3. Retrieve historical content with [`ObjectVersionStore::get`].
172/// 4. Create branches with [`ObjectVersionStore::create_branch`] and
173///    merge them back with [`ObjectVersionStore::merge_branch`].
174/// 5. Run GC explicitly with [`ObjectVersionStore::gc`] or let `put` trigger
175///    it automatically when `max_versions` is reached.
176pub struct ObjectVersionStore {
177    /// Runtime configuration.
178    pub config: VersionStoreConfig,
179    /// Live version storage, keyed by version number.
180    pub versions: HashMap<u64, OvsObjectVersion>,
181    /// Branch registry, keyed by branch name.
182    pub branches: HashMap<String, VersionBranch>,
183    /// Counter for the next version number to assign.
184    pub next_version: u64,
185    /// Cumulative byte count for all live versions.
186    pub total_bytes: u64,
187    /// Total number of versions that have been garbage-collected.
188    pub deleted_versions: u64,
189}
190
191// ---------------------------------------------------------------------------
192// FNV-1a 64-bit helper
193// ---------------------------------------------------------------------------
194
195/// Compute the FNV-1a 64-bit hash of a byte slice and return it as a
196/// zero-padded 16-character lowercase hex string (the CID used by this store).
197fn fnv1a_64(data: &[u8]) -> String {
198    const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
199    const PRIME: u64 = 1_099_511_628_211;
200
201    let mut hash = OFFSET_BASIS;
202    for &byte in data {
203        hash ^= u64::from(byte);
204        hash = hash.wrapping_mul(PRIME);
205    }
206    format!("{hash:016x}")
207}
208
209// ---------------------------------------------------------------------------
210// Implementation
211// ---------------------------------------------------------------------------
212
213impl ObjectVersionStore {
214    /// Create a new store and initialise the "main" branch at version 0.
215    ///
216    /// Version 0 is a sentinel – it is *not* inserted into `versions`.  The
217    /// "main" branch's `head_version` starts at 0, meaning "no versions yet".
218    pub fn new(config: VersionStoreConfig) -> Self {
219        let mut branches = HashMap::new();
220        branches.insert(
221            "main".to_owned(),
222            VersionBranch {
223                name: "main".to_owned(),
224                head_version: 0,
225                created_at: 0,
226                parent_branch: None,
227            },
228        );
229
230        Self {
231            config,
232            versions: HashMap::new(),
233            branches,
234            next_version: 1,
235            total_bytes: 0,
236            deleted_versions: 0,
237        }
238    }
239
240    // -----------------------------------------------------------------------
241    // Internal helpers
242    // -----------------------------------------------------------------------
243
244    /// Allocate the next version number and advance the counter.
245    fn alloc_version(&mut self) -> u64 {
246        let v = self.next_version;
247        self.next_version += 1;
248        v
249    }
250
251    /// Create and insert a new [`OvsObjectVersion`], linked to `parent`.
252    fn insert_version(
253        &mut self,
254        data: Vec<u8>,
255        tags: Vec<String>,
256        parent_version: Option<u64>,
257        now: u64,
258    ) -> u64 {
259        let cid = fnv1a_64(&data);
260        let size_bytes = data.len() as u64;
261        let version = self.alloc_version();
262
263        let ov = OvsObjectVersion {
264            version,
265            cid,
266            data,
267            parent_version,
268            created_at: now,
269            tags,
270            size_bytes,
271        };
272
273        self.total_bytes += size_bytes;
274        self.versions.insert(version, ov);
275        version
276    }
277
278    /// Return the current head version of a branch (0 if branch is empty).
279    fn branch_head(&self, branch_name: &str) -> Option<u64> {
280        self.branches.get(branch_name).map(|b| b.head_version)
281    }
282
283    /// Advance a branch head; the branch must already exist.
284    fn set_branch_head(&mut self, branch_name: &str, version: u64) {
285        if let Some(b) = self.branches.get_mut(branch_name) {
286            b.head_version = version;
287        }
288    }
289
290    // -----------------------------------------------------------------------
291    // Public write API
292    // -----------------------------------------------------------------------
293
294    /// Commit `data` on the "main" branch and return the new version number.
295    ///
296    /// If the version count would exceed `config.max_versions`, GC is run
297    /// before inserting.  CID is computed as the FNV-1a 64-bit hex hash of
298    /// `data`.
299    pub fn put(&mut self, data: Vec<u8>, tags: Vec<String>, now: u64) -> u64 {
300        // Trigger GC before inserting so we stay within the limit.
301        if self.versions.len() >= self.config.max_versions {
302            self.gc();
303        }
304
305        let parent = self.branch_head("main").filter(|&v| v != 0);
306        let version = self.insert_version(data, tags, parent, now);
307        self.set_branch_head("main", version);
308        version
309    }
310
311    /// Commit `data` on a named branch and return the new version number.
312    ///
313    /// Returns [`VsError::BranchNotFound`] if the branch does not exist.
314    pub fn put_on_branch(
315        &mut self,
316        branch: String,
317        data: Vec<u8>,
318        tags: Vec<String>,
319        now: u64,
320    ) -> Result<u64, VsError> {
321        if !self.branches.contains_key(&branch) {
322            return Err(VsError::BranchNotFound(branch));
323        }
324
325        if self.versions.len() >= self.config.max_versions {
326            self.gc();
327        }
328
329        let parent = self.branch_head(&branch).filter(|&v| v != 0);
330        let version = self.insert_version(data, tags, parent, now);
331        self.set_branch_head(&branch, version);
332        Ok(version)
333    }
334
335    // -----------------------------------------------------------------------
336    // Public read API
337    // -----------------------------------------------------------------------
338
339    /// Retrieve a version according to the given [`VersionQuery`].
340    pub fn get(&self, query: VersionQuery) -> Result<&OvsObjectVersion, VsError> {
341        match query {
342            VersionQuery::Latest => {
343                let head = self
344                    .branches
345                    .get("main")
346                    .map(|b| b.head_version)
347                    .unwrap_or(0);
348                self.versions
349                    .get(&head)
350                    .ok_or(VsError::VersionNotFound(head))
351            }
352
353            VersionQuery::AtVersion(v) => self.versions.get(&v).ok_or(VsError::VersionNotFound(v)),
354
355            VersionQuery::AtTime(t) => {
356                // Pick the version with the highest version number whose
357                // `created_at` is ≤ t.
358                let best = self
359                    .versions
360                    .values()
361                    .filter(|v| v.created_at <= t)
362                    .max_by_key(|v| v.version);
363
364                best.ok_or(VsError::VersionNotFound(0))
365            }
366
367            VersionQuery::Tagged(tag) => {
368                // Scan in version-number order for determinism.
369                let mut candidates: Vec<&OvsObjectVersion> = self
370                    .versions
371                    .values()
372                    .filter(|v| v.tags.contains(&tag))
373                    .collect();
374                candidates.sort_by_key(|v| v.version);
375                candidates
376                    .into_iter()
377                    .next()
378                    .ok_or(VsError::VersionNotFound(0))
379            }
380
381            VersionQuery::OnBranch(b) => {
382                let branch = self
383                    .branches
384                    .get(&b)
385                    .ok_or_else(|| VsError::BranchNotFound(b.clone()))?;
386                let head = branch.head_version;
387                self.versions
388                    .get(&head)
389                    .ok_or(VsError::VersionNotFound(head))
390            }
391        }
392    }
393
394    // -----------------------------------------------------------------------
395    // Branch management
396    // -----------------------------------------------------------------------
397
398    /// Create a new branch starting at `from_version`.
399    ///
400    /// # Errors
401    ///
402    /// - [`VsError::BranchAlreadyExists`] if `name` is taken.
403    /// - [`VsError::VersionNotFound`] if `from_version` is not in the store.
404    /// - [`VsError::MaxBranchesReached`] if the branch limit is hit.
405    pub fn create_branch(
406        &mut self,
407        name: String,
408        from_version: u64,
409        now: u64,
410    ) -> Result<(), VsError> {
411        if self.branches.contains_key(&name) {
412            return Err(VsError::BranchAlreadyExists(name));
413        }
414        if !self.versions.contains_key(&from_version) {
415            return Err(VsError::VersionNotFound(from_version));
416        }
417        if self.branches.len() >= self.config.max_branches {
418            return Err(VsError::MaxBranchesReached);
419        }
420
421        self.branches.insert(
422            name.clone(),
423            VersionBranch {
424                name,
425                head_version: from_version,
426                created_at: now,
427                parent_branch: None,
428            },
429        );
430        Ok(())
431    }
432
433    /// Merge the head of `source` into `target` by creating a new version on
434    /// `target` that is a copy of the source head, with `parent_version` set
435    /// to the source head version number.
436    ///
437    /// Returns the new version number.
438    pub fn merge_branch(&mut self, source: &str, target: &str, now: u64) -> Result<u64, VsError> {
439        let source_head = self
440            .branches
441            .get(source)
442            .ok_or_else(|| VsError::BranchNotFound(source.to_owned()))?
443            .head_version;
444
445        // Verify target exists before borrowing versions.
446        if !self.branches.contains_key(target) {
447            return Err(VsError::BranchNotFound(target.to_owned()));
448        }
449
450        // Clone the source version's data and tags so we can release the
451        // immutable borrow before mutating self.
452        let (data, tags) = {
453            let src_ver = self
454                .versions
455                .get(&source_head)
456                .ok_or(VsError::VersionNotFound(source_head))?;
457            (src_ver.data.clone(), src_ver.tags.clone())
458        };
459
460        if self.versions.len() >= self.config.max_versions {
461            self.gc();
462        }
463
464        let cid = fnv1a_64(&data);
465        let size_bytes = data.len() as u64;
466        let version = self.alloc_version();
467
468        let ov = OvsObjectVersion {
469            version,
470            cid,
471            data,
472            parent_version: Some(source_head),
473            created_at: now,
474            tags,
475            size_bytes,
476        };
477
478        self.total_bytes += size_bytes;
479        self.versions.insert(version, ov);
480        self.set_branch_head(target, version);
481        Ok(version)
482    }
483
484    // -----------------------------------------------------------------------
485    // History
486    // -----------------------------------------------------------------------
487
488    /// Walk the parent-version chain from the head of `branch` and return
489    /// versions in reverse-chronological order (newest first).
490    pub fn history(&self, branch: &str) -> Result<Vec<&OvsObjectVersion>, VsError> {
491        let head = self
492            .branches
493            .get(branch)
494            .ok_or_else(|| VsError::BranchNotFound(branch.to_owned()))?
495            .head_version;
496
497        let mut result = Vec::new();
498        let mut current = if head == 0 { None } else { Some(head) };
499
500        // Guard against cycles (shouldn't happen in a well-formed store).
501        let mut visited = HashSet::new();
502
503        while let Some(v) = current {
504            if !visited.insert(v) {
505                break; // cycle guard
506            }
507            if let Some(ver) = self.versions.get(&v) {
508                result.push(ver);
509                current = ver.parent_version;
510            } else {
511                break;
512            }
513        }
514
515        Ok(result)
516    }
517
518    // -----------------------------------------------------------------------
519    // Garbage collection
520    // -----------------------------------------------------------------------
521
522    /// Compute the set of version numbers reachable from any branch head via
523    /// `parent_version` links.
524    pub fn reachable_from_heads(&self) -> HashSet<u64> {
525        let mut reachable = HashSet::new();
526        let mut queue: VecDeque<u64> = VecDeque::new();
527
528        // Seed queue with all branch heads (skip sentinel 0).
529        for branch in self.branches.values() {
530            if branch.head_version != 0 {
531                queue.push_back(branch.head_version);
532            }
533        }
534
535        while let Some(v) = queue.pop_front() {
536            if !reachable.insert(v) {
537                continue; // already visited
538            }
539            if let Some(ver) = self.versions.get(&v) {
540                if let Some(parent) = ver.parent_version {
541                    queue.push_back(parent);
542                }
543            }
544        }
545
546        reachable
547    }
548
549    /// Run garbage collection according to `config.gc_policy`.
550    ///
551    /// Returns the number of versions deleted.
552    pub fn gc(&mut self) -> usize {
553        match self.config.gc_policy.clone() {
554            OvsGcPolicy::KeepAll => 0,
555
556            OvsGcPolicy::KeepLast(n) => {
557                let reachable = self.reachable_from_heads();
558
559                // Collect all version numbers that are *not* reachable.
560                let mut candidates: Vec<u64> = self
561                    .versions
562                    .keys()
563                    .copied()
564                    .filter(|v| !reachable.contains(v))
565                    .collect();
566
567                // Sort descending: we want to keep the *highest* (newest) n
568                // unreachable ones and delete the rest.  But we only need to
569                // delete versions outside the top-n by number.
570                candidates.sort_unstable();
571
572                // How many of the unreachable we want to drop:
573                // All unreachable that are NOT in the last n overall versions.
574                let all_sorted: Vec<u64> = {
575                    let mut all: Vec<u64> = self.versions.keys().copied().collect();
576                    all.sort_unstable();
577                    all
578                };
579                let keep_threshold = if all_sorted.len() > n {
580                    all_sorted[all_sorted.len() - n]
581                } else {
582                    0
583                };
584
585                let to_delete: Vec<u64> = candidates
586                    .iter()
587                    .copied()
588                    .filter(|&v| v < keep_threshold)
589                    .collect();
590
591                self.remove_versions(&to_delete)
592            }
593
594            OvsGcPolicy::KeepSince(t) => {
595                let reachable = self.reachable_from_heads();
596
597                let to_delete: Vec<u64> = self
598                    .versions
599                    .iter()
600                    .filter(|(v, ov)| ov.created_at < t && !reachable.contains(v))
601                    .map(|(v, _)| *v)
602                    .collect();
603
604                self.remove_versions(&to_delete)
605            }
606
607            OvsGcPolicy::KeepTagged => {
608                let reachable = self.reachable_from_heads();
609
610                let to_delete: Vec<u64> = self
611                    .versions
612                    .iter()
613                    .filter(|(v, ov)| ov.tags.is_empty() && !reachable.contains(v))
614                    .map(|(v, _)| *v)
615                    .collect();
616
617                self.remove_versions(&to_delete)
618            }
619        }
620    }
621
622    /// Remove a list of version numbers from the store and update accounting.
623    ///
624    /// Returns the number actually removed.
625    fn remove_versions(&mut self, to_delete: &[u64]) -> usize {
626        let mut count = 0usize;
627        for &v in to_delete {
628            if let Some(ov) = self.versions.remove(&v) {
629                self.total_bytes = self.total_bytes.saturating_sub(ov.size_bytes);
630                count += 1;
631            }
632        }
633        self.deleted_versions += count as u64;
634        count
635    }
636
637    // -----------------------------------------------------------------------
638    // Stats / counters
639    // -----------------------------------------------------------------------
640
641    /// Number of live versions currently in the store.
642    pub fn version_count(&self) -> usize {
643        self.versions.len()
644    }
645
646    /// Total bytes occupied by all live version payloads.
647    pub fn total_size_bytes(&self) -> u64 {
648        self.total_bytes
649    }
650
651    /// Return a statistics snapshot.
652    pub fn stats(&self) -> VsStats {
653        VsStats {
654            version_count: self.versions.len(),
655            branch_count: self.branches.len(),
656            total_bytes: self.total_bytes,
657            deleted_versions: self.deleted_versions,
658        }
659    }
660}
661
662// ===========================================================================
663// Tests
664// ===========================================================================
665
666#[cfg(test)]
667mod tests {
668    use super::{
669        fnv1a_64, ObjectVersionStore, OvsGcPolicy, VersionBranch, VersionQuery, VersionStoreConfig,
670        VsError, VsStats,
671    };
672
673    // -----------------------------------------------------------------------
674    // Helpers
675    // -----------------------------------------------------------------------
676
677    fn default_store() -> ObjectVersionStore {
678        ObjectVersionStore::new(VersionStoreConfig::default())
679    }
680
681    fn store_with_gc(policy: OvsGcPolicy) -> ObjectVersionStore {
682        ObjectVersionStore::new(VersionStoreConfig {
683            gc_policy: policy,
684            ..Default::default()
685        })
686    }
687
688    // -----------------------------------------------------------------------
689    // Test 1 – new store has "main" branch and zero versions
690    // -----------------------------------------------------------------------
691    #[test]
692    fn test_new_has_main_branch() {
693        let store = default_store();
694        assert!(store.branches.contains_key("main"));
695        assert_eq!(store.version_count(), 0);
696    }
697
698    // -----------------------------------------------------------------------
699    // Test 2 – put returns version 1 for the first commit
700    // -----------------------------------------------------------------------
701    #[test]
702    fn test_put_first_version_is_one() {
703        let mut store = default_store();
704        let v = store.put(b"hello".to_vec(), vec![], 100);
705        assert_eq!(v, 1);
706    }
707
708    // -----------------------------------------------------------------------
709    // Test 3 – successive puts return monotonically increasing numbers
710    // -----------------------------------------------------------------------
711    #[test]
712    fn test_put_monotonic_versions() {
713        let mut store = default_store();
714        let v1 = store.put(b"a".to_vec(), vec![], 1);
715        let v2 = store.put(b"b".to_vec(), vec![], 2);
716        let v3 = store.put(b"c".to_vec(), vec![], 3);
717        assert!(v1 < v2 && v2 < v3);
718    }
719
720    // -----------------------------------------------------------------------
721    // Test 4 – get(Latest) returns the most recent version on main
722    // -----------------------------------------------------------------------
723    #[test]
724    fn test_get_latest() {
725        let mut store = default_store();
726        store.put(b"first".to_vec(), vec![], 1);
727        let v2 = store.put(b"second".to_vec(), vec![], 2);
728        let got = store.get(VersionQuery::Latest).expect("latest");
729        assert_eq!(got.version, v2);
730    }
731
732    // -----------------------------------------------------------------------
733    // Test 5 – get(Latest) on empty store returns error
734    // -----------------------------------------------------------------------
735    #[test]
736    fn test_get_latest_empty_store_errors() {
737        let store = default_store();
738        assert!(store.get(VersionQuery::Latest).is_err());
739    }
740
741    // -----------------------------------------------------------------------
742    // Test 6 – get(AtVersion) retrieves exact version
743    // -----------------------------------------------------------------------
744    #[test]
745    fn test_get_at_version() {
746        let mut store = default_store();
747        let v1 = store.put(b"alpha".to_vec(), vec![], 10);
748        store.put(b"beta".to_vec(), vec![], 20);
749        let got = store.get(VersionQuery::AtVersion(v1)).expect("v1");
750        assert_eq!(got.data, b"alpha");
751    }
752
753    // -----------------------------------------------------------------------
754    // Test 7 – get(AtVersion) for missing version returns VersionNotFound
755    // -----------------------------------------------------------------------
756    #[test]
757    fn test_get_at_version_not_found() {
758        let store = default_store();
759        assert_eq!(
760            store.get(VersionQuery::AtVersion(999)),
761            Err(VsError::VersionNotFound(999))
762        );
763    }
764
765    // -----------------------------------------------------------------------
766    // Test 8 – get(AtTime) returns the latest version ≤ timestamp
767    // -----------------------------------------------------------------------
768    #[test]
769    fn test_get_at_time() {
770        let mut store = default_store();
771        let v1 = store.put(b"t100".to_vec(), vec![], 100);
772        store.put(b"t200".to_vec(), vec![], 200);
773        store.put(b"t300".to_vec(), vec![], 300);
774        // Ask for the state at t=150 — should get v1.
775        let got = store.get(VersionQuery::AtTime(150)).expect("at time");
776        assert_eq!(got.version, v1);
777    }
778
779    // -----------------------------------------------------------------------
780    // Test 9 – get(AtTime) with no versions before timestamp returns error
781    // -----------------------------------------------------------------------
782    #[test]
783    fn test_get_at_time_before_all_versions() {
784        let mut store = default_store();
785        store.put(b"data".to_vec(), vec![], 500);
786        assert!(store.get(VersionQuery::AtTime(1)).is_err());
787    }
788
789    // -----------------------------------------------------------------------
790    // Test 10 – get(Tagged) finds first version with tag
791    // -----------------------------------------------------------------------
792    #[test]
793    fn test_get_tagged() {
794        let mut store = default_store();
795        store.put(b"no-tag".to_vec(), vec![], 1);
796        let v2 = store.put(b"has-tag".to_vec(), vec!["release".to_owned()], 2);
797        store.put(b"also-tag".to_vec(), vec!["release".to_owned()], 3);
798        let got = store
799            .get(VersionQuery::Tagged("release".to_owned()))
800            .expect("tagged");
801        assert_eq!(got.version, v2);
802    }
803
804    // -----------------------------------------------------------------------
805    // Test 11 – get(Tagged) with no matching tag returns error
806    // -----------------------------------------------------------------------
807    #[test]
808    fn test_get_tagged_no_match() {
809        let mut store = default_store();
810        store.put(b"x".to_vec(), vec![], 1);
811        assert!(store
812            .get(VersionQuery::Tagged("missing".to_owned()))
813            .is_err());
814    }
815
816    // -----------------------------------------------------------------------
817    // Test 12 – get(OnBranch) returns branch head
818    // -----------------------------------------------------------------------
819    #[test]
820    fn test_get_on_branch() {
821        let mut store = default_store();
822        let v1 = store.put(b"initial".to_vec(), vec![], 1);
823        store
824            .create_branch("dev".to_owned(), v1, 2)
825            .expect("create");
826        store
827            .put_on_branch("dev".to_owned(), b"dev-work".to_vec(), vec![], 3)
828            .expect("put");
829        let got = store
830            .get(VersionQuery::OnBranch("dev".to_owned()))
831            .expect("on branch");
832        assert_eq!(got.data, b"dev-work");
833    }
834
835    // -----------------------------------------------------------------------
836    // Test 13 – get(OnBranch) for missing branch returns BranchNotFound
837    // -----------------------------------------------------------------------
838    #[test]
839    fn test_get_on_branch_not_found() {
840        let store = default_store();
841        assert_eq!(
842            store.get(VersionQuery::OnBranch("ghost".to_owned())),
843            Err(VsError::BranchNotFound("ghost".to_owned()))
844        );
845    }
846
847    // -----------------------------------------------------------------------
848    // Test 14 – create_branch succeeds and appears in branches map
849    // -----------------------------------------------------------------------
850    #[test]
851    fn test_create_branch_ok() {
852        let mut store = default_store();
853        let v = store.put(b"v1".to_vec(), vec![], 1);
854        store
855            .create_branch("feature".to_owned(), v, 2)
856            .expect("create ok");
857        assert!(store.branches.contains_key("feature"));
858    }
859
860    // -----------------------------------------------------------------------
861    // Test 15 – create_branch on an existing name returns BranchAlreadyExists
862    // -----------------------------------------------------------------------
863    #[test]
864    fn test_create_branch_duplicate() {
865        let mut store = default_store();
866        let v = store.put(b"x".to_vec(), vec![], 1);
867        store.create_branch("dup".to_owned(), v, 1).expect("first");
868        assert_eq!(
869            store.create_branch("dup".to_owned(), v, 2),
870            Err(VsError::BranchAlreadyExists("dup".to_owned()))
871        );
872    }
873
874    // -----------------------------------------------------------------------
875    // Test 16 – create_branch from unknown version returns VersionNotFound
876    // -----------------------------------------------------------------------
877    #[test]
878    fn test_create_branch_unknown_version() {
879        let mut store = default_store();
880        assert_eq!(
881            store.create_branch("b".to_owned(), 42, 1),
882            Err(VsError::VersionNotFound(42))
883        );
884    }
885
886    // -----------------------------------------------------------------------
887    // Test 17 – create_branch respects max_branches
888    // -----------------------------------------------------------------------
889    #[test]
890    fn test_create_branch_max_branches() {
891        let mut store = ObjectVersionStore::new(VersionStoreConfig {
892            max_branches: 2, // "main" counts as 1
893            ..Default::default()
894        });
895        let v = store.put(b"base".to_vec(), vec![], 1);
896        store.create_branch("b1".to_owned(), v, 2).expect("b1 ok");
897        assert_eq!(
898            store.create_branch("b2".to_owned(), v, 3),
899            Err(VsError::MaxBranchesReached)
900        );
901    }
902
903    // -----------------------------------------------------------------------
904    // Test 18 – put_on_branch for missing branch returns BranchNotFound
905    // -----------------------------------------------------------------------
906    #[test]
907    fn test_put_on_branch_not_found() {
908        let mut store = default_store();
909        assert_eq!(
910            store.put_on_branch("ghost".to_owned(), b"x".to_vec(), vec![], 1),
911            Err(VsError::BranchNotFound("ghost".to_owned()))
912        );
913    }
914
915    // -----------------------------------------------------------------------
916    // Test 19 – merge_branch copies source head onto target branch
917    // -----------------------------------------------------------------------
918    #[test]
919    fn test_merge_branch() {
920        let mut store = default_store();
921        let v1 = store.put(b"base".to_vec(), vec![], 1);
922        store
923            .create_branch("feature".to_owned(), v1, 2)
924            .expect("create");
925        store
926            .put_on_branch("feature".to_owned(), b"feature-work".to_vec(), vec![], 3)
927            .expect("put on feature");
928
929        let merged = store.merge_branch("feature", "main", 4).expect("merge");
930        let got = store.get(VersionQuery::Latest).expect("latest");
931        assert_eq!(got.version, merged);
932        assert_eq!(got.data, b"feature-work");
933    }
934
935    // -----------------------------------------------------------------------
936    // Test 20 – merge_branch propagates parent link from source head
937    // -----------------------------------------------------------------------
938    #[test]
939    fn test_merge_branch_parent_link() {
940        let mut store = default_store();
941        let v1 = store.put(b"base".to_vec(), vec![], 1);
942        store
943            .create_branch("feat".to_owned(), v1, 2)
944            .expect("create");
945        let feat_head = store
946            .put_on_branch("feat".to_owned(), b"feat".to_vec(), vec![], 3)
947            .expect("put");
948
949        let merged = store.merge_branch("feat", "main", 4).expect("merge");
950        let ver = store
951            .get(VersionQuery::AtVersion(merged))
952            .expect("merged ver");
953        assert_eq!(ver.parent_version, Some(feat_head));
954    }
955
956    // -----------------------------------------------------------------------
957    // Test 21 – merge_branch with missing source returns BranchNotFound
958    // -----------------------------------------------------------------------
959    #[test]
960    fn test_merge_branch_missing_source() {
961        let mut store = default_store();
962        assert_eq!(
963            store.merge_branch("ghost", "main", 1),
964            Err(VsError::BranchNotFound("ghost".to_owned()))
965        );
966    }
967
968    // -----------------------------------------------------------------------
969    // Test 22 – merge_branch with missing target returns BranchNotFound
970    // -----------------------------------------------------------------------
971    #[test]
972    fn test_merge_branch_missing_target() {
973        let mut store = default_store();
974        let v = store.put(b"x".to_vec(), vec![], 1);
975        store.create_branch("src".to_owned(), v, 2).expect("create");
976        assert_eq!(
977            store.merge_branch("src", "ghost", 3),
978            Err(VsError::BranchNotFound("ghost".to_owned()))
979        );
980    }
981
982    // -----------------------------------------------------------------------
983    // Test 23 – history returns versions in reverse-chronological order
984    // -----------------------------------------------------------------------
985    #[test]
986    fn test_history_order() {
987        let mut store = default_store();
988        let v1 = store.put(b"a".to_vec(), vec![], 1);
989        let v2 = store.put(b"b".to_vec(), vec![], 2);
990        let v3 = store.put(b"c".to_vec(), vec![], 3);
991
992        let hist = store.history("main").expect("history");
993        let nums: Vec<u64> = hist.iter().map(|v| v.version).collect();
994        assert_eq!(nums, vec![v3, v2, v1]);
995    }
996
997    // -----------------------------------------------------------------------
998    // Test 24 – history on empty branch returns empty vec
999    // -----------------------------------------------------------------------
1000    #[test]
1001    fn test_history_empty_branch() {
1002        let store = default_store();
1003        let hist = store.history("main").expect("history");
1004        assert!(hist.is_empty());
1005    }
1006
1007    // -----------------------------------------------------------------------
1008    // Test 25 – history on missing branch returns BranchNotFound
1009    // -----------------------------------------------------------------------
1010    #[test]
1011    fn test_history_missing_branch() {
1012        let store = default_store();
1013        assert_eq!(
1014            store.history("nope"),
1015            Err(VsError::BranchNotFound("nope".to_owned()))
1016        );
1017    }
1018
1019    // -----------------------------------------------------------------------
1020    // Test 26 – GC KeepAll deletes nothing
1021    // -----------------------------------------------------------------------
1022    #[test]
1023    fn test_gc_keep_all() {
1024        let mut store = store_with_gc(OvsGcPolicy::KeepAll);
1025        for i in 0u8..10 {
1026            store.put(vec![i], vec![], u64::from(i));
1027        }
1028        let deleted = store.gc();
1029        assert_eq!(deleted, 0);
1030        assert_eq!(store.version_count(), 10);
1031    }
1032
1033    // -----------------------------------------------------------------------
1034    // Test 27 – GC KeepLast(n) keeps the n newest and deletes unreachable rest
1035    // -----------------------------------------------------------------------
1036    #[test]
1037    fn test_gc_keep_last() {
1038        let mut store = store_with_gc(OvsGcPolicy::KeepLast(3));
1039        for i in 0u8..10 {
1040            store.put(vec![i], vec![], u64::from(i));
1041        }
1042        // Detach the head so versions 1–7 are unreachable (only the last 3 committed
1043        // to "main" form the reachable chain).
1044        // Actually with KeepLast(3) we keep the top-3 by number regardless of reachability
1045        // among unreachable versions.
1046        store.gc();
1047        // At most 3 unreachable versions survive; main's head chain is always kept.
1048        // All 10 were added linearly so the full chain is reachable. KeepLast(3)
1049        // only deletes unreachable versions outside the top-3, so nothing is deleted.
1050        // Let's verify by checking version_count ≥ 3 (the reachable chain is intact).
1051        assert!(store.version_count() >= 3);
1052    }
1053
1054    // -----------------------------------------------------------------------
1055    // Test 28 – GC KeepLast deletes truly unreachable old versions
1056    // -----------------------------------------------------------------------
1057    #[test]
1058    fn test_gc_keep_last_deletes_unreachable() {
1059        let mut store = store_with_gc(OvsGcPolicy::KeepLast(2));
1060        // Add versions 1..5 on main (all reachable via parent chain)
1061        for i in 1u8..=5 {
1062            store.put(vec![i], vec![], u64::from(i));
1063        }
1064        // Create a branch at version 3 and advance it — version 3 becomes
1065        // reachable via the new branch, but versions 1 and 2 are only reachable
1066        // via the main chain (which includes them through parent links).
1067        // All are reachable, so nothing should be deleted.
1068        let deleted = store.gc();
1069        assert_eq!(deleted, 0);
1070        assert_eq!(store.version_count(), 5);
1071    }
1072
1073    // -----------------------------------------------------------------------
1074    // Test 29 – GC KeepSince deletes old unreachable versions
1075    // -----------------------------------------------------------------------
1076    #[test]
1077    fn test_gc_keep_since() {
1078        // Build a store: commit versions at t=10, 20, 30, 40, 50 then create
1079        // a new branch from the last version.  Reset main to only the newest.
1080        let mut store = store_with_gc(OvsGcPolicy::KeepSince(35));
1081
1082        let v1 = store.put(b"t10".to_vec(), vec![], 10);
1083        let v2 = store.put(b"t20".to_vec(), vec![], 20);
1084        let _v3 = store.put(b"t30".to_vec(), vec![], 30);
1085        let v4 = store.put(b"t40".to_vec(), vec![], 40);
1086        let v5 = store.put(b"t50".to_vec(), vec![], 50);
1087
1088        // Create an isolated branch at v1 and advance it (v1 is now reachable
1089        // from the new branch).
1090        store
1091            .create_branch("keep-old".to_owned(), v1, 5)
1092            .expect("create");
1093
1094        // All versions ≥ t=35 (v4, v5) are safe; v1 is safe (reachable).
1095        // v2 (t=20) and v3 (t=30) are only reachable via main's chain through
1096        // parent links starting at v5 → v4 → v3 → v2 → v1.
1097        // So nothing unreachable yet.  Let's confirm 0 are deleted.
1098        let deleted = store.gc();
1099        assert_eq!(deleted, 0);
1100
1101        // Now break the main chain by resetting its head directly to v5 (same
1102        // as current state, no real break), but add an orphan version.
1103        // Simulate an orphan by inserting a version directly.
1104        use super::OvsObjectVersion;
1105        store.versions.insert(
1106            999,
1107            OvsObjectVersion {
1108                version: 999,
1109                cid: "orphan".to_owned(),
1110                data: b"orphan".to_vec(),
1111                parent_version: None,
1112                created_at: 10, // old timestamp → should be GC'd
1113                tags: vec![],
1114                size_bytes: 6,
1115            },
1116        );
1117        store.total_bytes += 6;
1118
1119        let deleted2 = store.gc();
1120        // version 999 has created_at=10 < 35 and is not reachable from any head.
1121        assert_eq!(deleted2, 1);
1122        assert!(!store.versions.contains_key(&999));
1123
1124        let _ = (v2, v4, v5); // suppress unused
1125    }
1126
1127    // -----------------------------------------------------------------------
1128    // Test 30 – GC KeepTagged deletes untagged unreachable versions
1129    // -----------------------------------------------------------------------
1130    #[test]
1131    fn test_gc_keep_tagged() {
1132        let mut store = store_with_gc(OvsGcPolicy::KeepTagged);
1133
1134        // Create a linear chain on main.
1135        let v1 = store.put(b"v1".to_vec(), vec!["release".to_owned()], 1);
1136        let _v2 = store.put(b"v2".to_vec(), vec![], 2); // no tag, reachable
1137        let _v3 = store.put(b"v3".to_vec(), vec!["stable".to_owned()], 3);
1138
1139        // Insert an orphan (no tag, not reachable).
1140        use super::OvsObjectVersion;
1141        store.versions.insert(
1142            888,
1143            OvsObjectVersion {
1144                version: 888,
1145                cid: "orphan2".to_owned(),
1146                data: b"orphan".to_vec(),
1147                parent_version: None,
1148                created_at: 1,
1149                tags: vec![],
1150                size_bytes: 6,
1151            },
1152        );
1153        store.total_bytes += 6;
1154
1155        let deleted = store.gc();
1156        // Only version 888 should be deleted (no tag, not reachable from any head).
1157        assert_eq!(deleted, 1);
1158        assert!(!store.versions.contains_key(&888));
1159
1160        // v1 has a tag → survives even if unreachable (but it IS reachable here).
1161        assert!(store.versions.contains_key(&v1));
1162    }
1163
1164    // -----------------------------------------------------------------------
1165    // Test 31 – reachable_from_heads traverses parent chain correctly
1166    // -----------------------------------------------------------------------
1167    #[test]
1168    fn test_reachable_from_heads() {
1169        let mut store = default_store();
1170        let v1 = store.put(b"a".to_vec(), vec![], 1);
1171        let v2 = store.put(b"b".to_vec(), vec![], 2);
1172        let v3 = store.put(b"c".to_vec(), vec![], 3);
1173
1174        let reachable = store.reachable_from_heads();
1175        assert!(reachable.contains(&v1));
1176        assert!(reachable.contains(&v2));
1177        assert!(reachable.contains(&v3));
1178    }
1179
1180    // -----------------------------------------------------------------------
1181    // Test 32 – stats returns correct counts after operations
1182    // -----------------------------------------------------------------------
1183    #[test]
1184    fn test_stats() {
1185        let mut store = default_store();
1186        store.put(b"hello".to_vec(), vec![], 1);
1187        store.put(b"world".to_vec(), vec![], 2);
1188
1189        let s = store.stats();
1190        assert_eq!(s.version_count, 2);
1191        assert_eq!(s.branch_count, 1);
1192        assert_eq!(s.total_bytes, 10); // "hello"(5) + "world"(5)
1193        assert_eq!(s.deleted_versions, 0);
1194    }
1195
1196    // -----------------------------------------------------------------------
1197    // Test 33 – total_size_bytes tracks accurately
1198    // -----------------------------------------------------------------------
1199    #[test]
1200    fn test_total_size_bytes() {
1201        let mut store = default_store();
1202        store.put(b"abc".to_vec(), vec![], 1); // 3 bytes
1203        store.put(b"de".to_vec(), vec![], 2); // 2 bytes
1204        assert_eq!(store.total_size_bytes(), 5);
1205    }
1206
1207    // -----------------------------------------------------------------------
1208    // Test 34 – CID is deterministic and based on data content
1209    // -----------------------------------------------------------------------
1210    #[test]
1211    fn test_cid_is_deterministic() {
1212        let mut store1 = default_store();
1213        let mut store2 = default_store();
1214        store1.put(b"same content".to_vec(), vec![], 1);
1215        store2.put(b"same content".to_vec(), vec![], 1);
1216        let cid1 = &store1.get(VersionQuery::Latest).expect("v1").cid;
1217        let cid2 = &store2.get(VersionQuery::Latest).expect("v2").cid;
1218        assert_eq!(cid1, cid2);
1219    }
1220
1221    // -----------------------------------------------------------------------
1222    // Test 35 – Different data produces different CIDs
1223    // -----------------------------------------------------------------------
1224    #[test]
1225    fn test_cid_different_for_different_data() {
1226        let cid_a = fnv1a_64(b"hello");
1227        let cid_b = fnv1a_64(b"world");
1228        assert_ne!(cid_a, cid_b);
1229    }
1230
1231    // -----------------------------------------------------------------------
1232    // Test 36 – parent_version chain is set correctly on main
1233    // -----------------------------------------------------------------------
1234    #[test]
1235    fn test_parent_version_chain_on_main() {
1236        let mut store = default_store();
1237        let v1 = store.put(b"first".to_vec(), vec![], 1);
1238        let v2 = store.put(b"second".to_vec(), vec![], 2);
1239        let v3 = store.put(b"third".to_vec(), vec![], 3);
1240
1241        let ver1 = store.get(VersionQuery::AtVersion(v1)).expect("v1");
1242        assert_eq!(ver1.parent_version, None);
1243
1244        let ver2 = store.get(VersionQuery::AtVersion(v2)).expect("v2");
1245        assert_eq!(ver2.parent_version, Some(v1));
1246
1247        let ver3 = store.get(VersionQuery::AtVersion(v3)).expect("v3");
1248        assert_eq!(ver3.parent_version, Some(v2));
1249    }
1250
1251    // -----------------------------------------------------------------------
1252    // Test 37 – branch inherits parent chain from fork point
1253    // -----------------------------------------------------------------------
1254    #[test]
1255    fn test_branch_parent_chain() {
1256        let mut store = default_store();
1257        let v1 = store.put(b"base".to_vec(), vec![], 1);
1258        store
1259            .create_branch("side".to_owned(), v1, 2)
1260            .expect("create");
1261        let v_side = store
1262            .put_on_branch("side".to_owned(), b"side".to_vec(), vec![], 3)
1263            .expect("put");
1264
1265        let ver = store.get(VersionQuery::AtVersion(v_side)).expect("ver");
1266        assert_eq!(ver.parent_version, Some(v1));
1267    }
1268
1269    // -----------------------------------------------------------------------
1270    // Test 38 – deleted_versions counter increments on GC
1271    // -----------------------------------------------------------------------
1272    #[test]
1273    fn test_deleted_versions_counter() {
1274        let mut store = store_with_gc(OvsGcPolicy::KeepTagged);
1275
1276        // Insert an orphan manually.
1277        use super::OvsObjectVersion;
1278        store.versions.insert(
1279            777,
1280            OvsObjectVersion {
1281                version: 777,
1282                cid: "x".to_owned(),
1283                data: b"x".to_vec(),
1284                parent_version: None,
1285                created_at: 1,
1286                tags: vec![],
1287                size_bytes: 1,
1288            },
1289        );
1290        store.total_bytes += 1;
1291
1292        assert_eq!(store.stats().deleted_versions, 0);
1293        store.gc();
1294        assert_eq!(store.stats().deleted_versions, 1);
1295    }
1296
1297    // -----------------------------------------------------------------------
1298    // Test 39 – version_count decreases after GC
1299    // -----------------------------------------------------------------------
1300    #[test]
1301    fn test_version_count_after_gc() {
1302        let mut store = store_with_gc(OvsGcPolicy::KeepTagged);
1303        store.put(b"keep".to_vec(), vec!["tag".to_owned()], 1);
1304
1305        use super::OvsObjectVersion;
1306        store.versions.insert(
1307            500,
1308            OvsObjectVersion {
1309                version: 500,
1310                cid: "orphan".to_owned(),
1311                data: b"orphan".to_vec(),
1312                parent_version: None,
1313                created_at: 1,
1314                tags: vec![],
1315                size_bytes: 6,
1316            },
1317        );
1318        store.total_bytes += 6;
1319
1320        let before = store.version_count();
1321        store.gc();
1322        assert!(store.version_count() < before);
1323    }
1324
1325    // -----------------------------------------------------------------------
1326    // Test 40 – VsError Display messages are non-empty
1327    // -----------------------------------------------------------------------
1328    #[test]
1329    fn test_vs_error_display() {
1330        assert!(!VsError::VersionNotFound(1).to_string().is_empty());
1331        assert!(!VsError::BranchNotFound("x".to_owned())
1332            .to_string()
1333            .is_empty());
1334        assert!(!VsError::BranchAlreadyExists("x".to_owned())
1335            .to_string()
1336            .is_empty());
1337        assert!(!VsError::MaxVersionsReached.to_string().is_empty());
1338        assert!(!VsError::MaxBranchesReached.to_string().is_empty());
1339    }
1340
1341    // -----------------------------------------------------------------------
1342    // Test 41 – VersionStoreConfig default values
1343    // -----------------------------------------------------------------------
1344    #[test]
1345    fn test_version_store_config_defaults() {
1346        let cfg = VersionStoreConfig::default();
1347        assert_eq!(cfg.max_versions, 10_000);
1348        assert_eq!(cfg.max_branches, 100);
1349        assert!(!cfg.compress_old_versions);
1350        assert_eq!(cfg.gc_policy, OvsGcPolicy::KeepAll);
1351    }
1352
1353    // -----------------------------------------------------------------------
1354    // Test 42 – VersionBranch fields are accessible
1355    // -----------------------------------------------------------------------
1356    #[test]
1357    fn test_version_branch_fields() {
1358        let b = VersionBranch {
1359            name: "test".to_owned(),
1360            head_version: 5,
1361            created_at: 42,
1362            parent_branch: Some("main".to_owned()),
1363        };
1364        assert_eq!(b.name, "test");
1365        assert_eq!(b.head_version, 5);
1366        assert_eq!(b.created_at, 42);
1367        assert_eq!(b.parent_branch, Some("main".to_owned()));
1368    }
1369
1370    // -----------------------------------------------------------------------
1371    // Test 43 – VsStats fields reflect live state
1372    // -----------------------------------------------------------------------
1373    #[test]
1374    fn test_vs_stats_fields() {
1375        let s = VsStats {
1376            version_count: 3,
1377            branch_count: 2,
1378            total_bytes: 100,
1379            deleted_versions: 5,
1380        };
1381        assert_eq!(s.version_count, 3);
1382        assert_eq!(s.branch_count, 2);
1383        assert_eq!(s.total_bytes, 100);
1384        assert_eq!(s.deleted_versions, 5);
1385    }
1386
1387    // -----------------------------------------------------------------------
1388    // Test 44 – multi-branch get(AtTime) picks correct cross-branch version
1389    // -----------------------------------------------------------------------
1390    #[test]
1391    fn test_get_at_time_across_branches() {
1392        let mut store = default_store();
1393        let v1 = store.put(b"main-t10".to_vec(), vec![], 10);
1394        store
1395            .create_branch("side".to_owned(), v1, 11)
1396            .expect("create");
1397        store
1398            .put_on_branch("side".to_owned(), b"side-t20".to_vec(), vec![], 20)
1399            .expect("put");
1400        store.put(b"main-t30".to_vec(), vec![], 30);
1401
1402        // At time 15 only v1 (t=10) and the side branch commit (t=20) have
1403        // created_at ≤ 15; v1 is the only one.
1404        let got = store.get(VersionQuery::AtTime(15)).expect("at time");
1405        assert_eq!(got.version, v1);
1406    }
1407
1408    // -----------------------------------------------------------------------
1409    // Test 45 – get(AtTime) when multiple versions share the timestamp
1410    // -----------------------------------------------------------------------
1411    #[test]
1412    fn test_get_at_time_picks_highest_version() {
1413        let mut store = default_store();
1414        let v1 = store.put(b"early".to_vec(), vec![], 100);
1415        let v2 = store.put(b"also-t100".to_vec(), vec![], 100);
1416        let got = store.get(VersionQuery::AtTime(100)).expect("tie");
1417        // Should return v2 (highest version number with created_at ≤ 100).
1418        assert!(got.version == v1 || got.version == v2);
1419        // Specifically the highest:
1420        assert_eq!(got.version, v2);
1421    }
1422
1423    // -----------------------------------------------------------------------
1424    // Test 46 – empty store reachable_from_heads returns empty set
1425    // -----------------------------------------------------------------------
1426    #[test]
1427    fn test_reachable_empty_store() {
1428        let store = default_store();
1429        assert!(store.reachable_from_heads().is_empty());
1430    }
1431
1432    // -----------------------------------------------------------------------
1433    // Test 47 – auto-gc triggered when max_versions is hit
1434    // -----------------------------------------------------------------------
1435    #[test]
1436    fn test_auto_gc_on_max_versions() {
1437        let mut store = ObjectVersionStore::new(VersionStoreConfig {
1438            max_versions: 5,
1439            gc_policy: OvsGcPolicy::KeepTagged,
1440            ..Default::default()
1441        });
1442
1443        // Fill up to max_versions with untagged versions on main.
1444        // All are reachable through the parent chain, so KeepTagged won't
1445        // delete them; but auto-gc is invoked without panic.
1446        for i in 0u8..5 {
1447            store.put(vec![i], vec![], u64::from(i));
1448        }
1449        // Add one more; this triggers GC before insertion.
1450        store.put(vec![99], vec!["important".to_owned()], 99);
1451        // Store is functional and contains at least the tagged version.
1452        assert!(store.get(VersionQuery::Latest).is_ok());
1453    }
1454
1455    // -----------------------------------------------------------------------
1456    // Test 48 – history on branch with single commit
1457    // -----------------------------------------------------------------------
1458    #[test]
1459    fn test_history_single_commit() {
1460        let mut store = default_store();
1461        let v = store.put(b"only".to_vec(), vec![], 1);
1462        let hist = store.history("main").expect("hist");
1463        assert_eq!(hist.len(), 1);
1464        assert_eq!(hist[0].version, v);
1465    }
1466
1467    // -----------------------------------------------------------------------
1468    // Test 49 – put_on_branch advances correct branch, not main
1469    // -----------------------------------------------------------------------
1470    #[test]
1471    fn test_put_on_branch_does_not_advance_main() {
1472        let mut store = default_store();
1473        let v1 = store.put(b"main-base".to_vec(), vec![], 1);
1474        store
1475            .create_branch("side".to_owned(), v1, 2)
1476            .expect("create");
1477        store
1478            .put_on_branch("side".to_owned(), b"side-update".to_vec(), vec![], 3)
1479            .expect("put");
1480
1481        // main head should still be v1.
1482        let main_head = store.branches["main"].head_version;
1483        assert_eq!(main_head, v1);
1484    }
1485
1486    // -----------------------------------------------------------------------
1487    // Test 50 – fnv1a_64 produces correct length hex string
1488    // -----------------------------------------------------------------------
1489    #[test]
1490    fn test_fnv1a_64_hex_length() {
1491        let cid = fnv1a_64(b"test data");
1492        assert_eq!(cid.len(), 16);
1493        assert!(cid.chars().all(|c| c.is_ascii_hexdigit()));
1494    }
1495}