arcature-cli 2026.2.0

Developer lifecycle CLI for Arcature applications.
Documentation
//! Registry inspection boundary (RV2.9).
//!
//! crates.io inspection is abstracted behind a narrow, testable trait so
//! the publish engine's pure logic can be unit-tested without a network
//! (ADR-0005 invariant 15). The real implementation calls the crates.io
//! registry's sparse-index endpoint; tests use a fake that records what
//! was queried and returns canned results.
//!
//! `CARGO_REGISTRY_TOKEN` is never involved in *inspection*: checking
//! whether a version exists on the registry is an unauthenticated GET to
//! the index, not a publish. The token is only used by `cargo publish`
//! itself, which runs inside GitHub Actions / Trusted Publishing, never
//! on the maintainer workstation (ADR-0005 Decision ยง7).

use crate::release::version::Ybf;

/// The result of querying whether a specific crate version is published.
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RegistryStatus {
    /// The version exists on the registry and matches the plan.
    Published,
    /// The version is absent from the registry โ€” it needs publishing.
    Absent,
    /// The version exists but does not match the plan's version. This is
    /// a collision: the version was already used for a different release.
    /// This should never happen for immutable published versions and is
    /// a hard failure (ADR-0005 invariant 11 โ€” no republish of immutable
    /// versions).
    VersionMismatch { expected: Ybf, found: String },
}

/// A narrow, testable boundary for inspecting the crate registry.
///
/// The real implementation shells out to `cargo search` or queries the
/// sparse index; tests supply a fake. Neither path ever touches
/// `CARGO_REGISTRY_TOKEN` (that credential is for publishing, not
/// inspection).
#[allow(dead_code)]
pub(crate) trait RegistryInspector {
    /// Check whether `crate_name` at `version` is published.
    fn check_version(&mut self, crate_name: &str, version: &Ybf) -> RegistryStatus;
}

/// A fake registry inspector for unit tests. Records every query and
/// returns canned results.
#[cfg(test)]
#[derive(Debug, Default)]
pub(crate) struct FakeRegistry {
    /// Map of `(crate_name, version_string)` โ†’ published or not.
    pub published: std::collections::BTreeMap<(String, String), bool>,
    /// Every query received, in order.
    pub queries: Vec<(String, String)>,
}

#[cfg(test)]
impl RegistryInspector for FakeRegistry {
    fn check_version(&mut self, crate_name: &str, version: &Ybf) -> RegistryStatus {
        let key = (crate_name.to_string(), version.to_string());
        self.queries.push(key.clone());
        match self.published.get(&key) {
            Some(true) => RegistryStatus::Published,
            Some(false) => RegistryStatus::Absent,
            None => RegistryStatus::Absent,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fake_registry_reports_published() {
        let mut fake = FakeRegistry::default();
        fake.published
            .insert(("arcature-auth".to_string(), "2026.1.7".to_string()), true);

        let v = Ybf::parse("2026.1.7").unwrap();
        assert_eq!(
            fake.check_version("arcature-auth", &v),
            RegistryStatus::Published
        );
        assert_eq!(fake.queries.len(), 1);
    }

    #[test]
    fn fake_registry_reports_absent() {
        let mut fake = FakeRegistry::default();
        let v = Ybf::parse("2026.1.7").unwrap();
        assert_eq!(
            fake.check_version("arcature-auth", &v),
            RegistryStatus::Absent
        );
    }

    #[test]
    fn fake_registry_records_queries_in_order() {
        let mut fake = FakeRegistry::default();
        let v1 = Ybf::parse("2026.1.7").unwrap();
        let v2 = Ybf::parse("2026.1.9").unwrap();
        fake.check_version("arcature-auth", &v1);
        fake.check_version("arcature-cli", &v2);
        assert_eq!(fake.queries.len(), 2);
        assert_eq!(fake.queries[0].0, "arcature-auth");
        assert_eq!(fake.queries[1].0, "arcature-cli");
    }
}