arcature-cli 2026.2.0

Developer lifecycle CLI for Arcature applications.
Documentation
//! Zero-touch onboarding tests (RV2.14).
//!
//! Tests that adding a new public crate requires only a `Cargo.toml`
//! with release metadata — no workflow edit, no manual topo insertion
//! (ADR-0005 invariant 13).
//!
//! The workflow redesign (RV2.10) replaced hard-coded per-crate publish
//! jobs with a graph-driven workflow. The publish sequence is derived
//! from the committed Release Plan (which comes from cargo metadata),
//! not from a hard-coded list. Adding a crate with
//! `[package.metadata.arcature]` automatically makes it discoverable by
//! the release engine.
//!
//! These tests simulate adding a new "arcature-future" crate to the
//! workspace by constructing mock `cargo metadata` output and verifying
//! that discovery, graph building, and plan validation all pick it up
//! automatically.

#![cfg(test)]

use super::super::discovered::{CargoMetadata, Dependency, Package, Target, discover};
use super::super::graph::{build_crate_graph, build_unit_graph};
use super::super::metadata::{CrateMetadata, Role};
use super::super::validate::validate;
use super::plan::{LoadedEntry, LoadedPlan};
use super::validate::validate_plan;
use crate::release::version::Ybf;
use std::collections::BTreeMap;

const SHA: &str = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0";

/// Build a mock `cargo metadata` document with a set of packages.
fn mock_metadata(packages: Vec<Package>) -> CargoMetadata {
    CargoMetadata { packages }
}

/// Build a publishable package with standard release metadata.
fn publishable_package(
    name: &str,
    version: &str,
    unit: &str,
    dependencies: Vec<Dependency>,
) -> Package {
    let metadata = serde_json::json!({
        "arcature": {
            "publish": true,
            "role": "subsystem",
            "release-unit": unit,
        }
    });

    Package {
        name: name.to_string(),
        version: Some(version.to_string()),
        manifest_path: format!("crates/{name}/Cargo.toml"),
        publish: Some(vec!["crates-io".to_string()]),
        metadata: Some(metadata),
        targets: vec![Target {
            kind: vec!["lib".to_string()],
        }],
        dependencies,
    }
}

/// Build a non-publishable package (publish = false).
fn non_publishable_package(name: &str) -> Package {
    let metadata = serde_json::json!({
        "arcature": {
            "publish": false,
        }
    });

    Package {
        name: name.to_string(),
        version: Some("2026.1.0".to_string()),
        manifest_path: format!("crates/{name}/Cargo.toml"),
        publish: Some(vec![]),
        metadata: Some(metadata),
        targets: vec![Target {
            kind: vec!["lib".to_string()],
        }],
        dependencies: vec![],
    }
}

fn dep_on(name: &str) -> Dependency {
    Dependency {
        name: name.to_string(),
        kind: None,
        optional: None,
        req: Some(format!("~{name}")),
        path: None,
        rename: None,
    }
}

// =============================================================
// 1. New crate auto-discovered from cargo metadata
// =============================================================

#[test]
fn new_crate_auto_discovered() {
    // Simulate adding a new "arcature-future" crate to the workspace.
    let metadata = mock_metadata(vec![
        publishable_package("arcature-inertia", "2026.1.0", "arcature-inertia", vec![]),
        publishable_package("arcature-future", "2026.1.0", "arcature-future", vec![]),
    ]);

    let crates = discover(&metadata);

    // Both crates are discovered — including the new one.
    assert_eq!(crates.len(), 2);
    let future = crates.iter().find(|c| c.name == "arcature-future");
    assert!(future.is_some(), "arcature-future should be discovered");
    assert!(future.unwrap().publishable);

    // The new crate's metadata is valid.
    match &future.unwrap().metadata {
        CrateMetadata::Valid(md) => {
            assert!(md.publish);
            assert_eq!(md.release_unit.as_deref(), Some("arcature-future"));
            assert_eq!(md.role, Some(Role::Subsystem));
        }
        other => panic!("expected valid metadata, got {other:?}"),
    }
}

// =============================================================
// 2. New crate appears in the dependency graph
// =============================================================

#[test]
fn new_crate_in_dependency_graph() {
    let metadata = mock_metadata(vec![
        publishable_package("arcature-inertia", "2026.1.0", "arcature-inertia", vec![]),
        publishable_package(
            "arcature-future",
            "2026.1.0",
            "arcature-future",
            vec![dep_on("arcature-inertia")],
        ),
    ]);

    let crates = discover(&metadata);
    let crate_graph = build_crate_graph(&metadata, &crates);
    let unit_graph = build_unit_graph(&crate_graph);

    // The new crate's unit appears in the graph.
    assert!(unit_graph.nodes.contains_key("arcature-future"));
    // And its dependency on arcature-inertia is represented.
    let future_node = &unit_graph.nodes["arcature-future"];
    assert!(future_node.dependencies.contains_key("arcature-inertia"));
}

// =============================================================
// 3. New crate passes validation
// =============================================================

#[test]
fn new_crate_passes_validation() {
    let metadata = mock_metadata(vec![publishable_package(
        "arcature-future",
        "2026.1.0",
        "arcature-future",
        vec![],
    )]);

    let crates = discover(&metadata);
    let findings = validate(&crates);
    assert!(
        findings.is_ok(),
        "new crate should pass validation: {findings:?}"
    );
}

// =============================================================
// 4. New crate accepted in Release Plan validation
// =============================================================

#[test]
fn new_crate_accepted_in_plan_validation() {
    let metadata = mock_metadata(vec![publishable_package(
        "arcature-future",
        "2026.1.0",
        "arcature-future",
        vec![],
    )]);

    let crates = discover(&metadata);
    let crate_graph = build_crate_graph(&metadata, &crates);
    let unit_graph = build_unit_graph(&crate_graph);

    // A plan that includes the new crate should validate successfully.
    let plan = LoadedPlan {
        transaction_id: "2026-08-15.01".to_string(),
        commit: SHA.to_string(),
        entries: {
            let mut m = BTreeMap::new();
            m.insert(
                "arcature-future".to_string(),
                LoadedEntry {
                    crate_name: "arcature-future".to_string(),
                    version: Ybf::parse("2026.1.1").unwrap(),
                    order: 1,
                },
            );
            m
        },
    };

    let findings = validate_plan(&plan, SHA, &crates, &unit_graph);
    assert!(
        findings.is_empty(),
        "plan with new crate should validate: {findings:?}"
    );
}

// =============================================================
// 5. Non-publishable crate not in publish graph
// =============================================================

#[test]
fn non_publishable_crate_excluded_from_graph() {
    let metadata = mock_metadata(vec![
        publishable_package("arcature-inertia", "2026.1.0", "arcature-inertia", vec![]),
        non_publishable_package("arcature-test-fixture"),
    ]);

    let crates = discover(&metadata);

    // The non-publishable crate is discovered but not publishable.
    let fixture = crates.iter().find(|c| c.name == "arcature-test-fixture");
    assert!(fixture.is_some());
    assert!(!fixture.unwrap().publishable);

    // The graph only includes publishable crates.
    let crate_graph = build_crate_graph(&metadata, &crates);
    assert!(crate_graph.nodes.contains_key("arcature-inertia"));
    assert!(!crate_graph.nodes.contains_key("arcature-test-fixture"));
}

// =============================================================
// 6. No workflow architecture change needed (structural proof)
// =============================================================

#[test]
fn no_workflow_architecture_change_needed() {
    // The release-transaction.yml workflow (RV2.10) has NO hard-coded
    // per-crate publish jobs. The publish sequence is derived from the
    // committed Release Plan, which comes from cargo metadata. Adding
    // a new crate does not require editing any workflow file.
    //
    // This test verifies the structural property: the discovery and
    // graph building are entirely data-driven from cargo metadata. If
    // a new crate appears in cargo metadata, it is automatically part
    // of the release system.
    let before = mock_metadata(vec![publishable_package(
        "arcature-inertia",
        "2026.1.0",
        "arcature-inertia",
        vec![],
    )]);
    let crates_before = discover(&before);
    let graph_before = build_crate_graph(&before, &crates_before);
    let units_before = build_unit_graph(&graph_before);
    let count_before = units_before.nodes.len();

    // Add a new crate — no code change, just metadata.
    let after = mock_metadata(vec![
        publishable_package("arcature-inertia", "2026.1.0", "arcature-inertia", vec![]),
        publishable_package("arcature-new", "2026.1.0", "arcature-new", vec![]),
    ]);
    let crates_after = discover(&after);
    let graph_after = build_crate_graph(&after, &crates_after);
    let units_after = build_unit_graph(&graph_after);
    let count_after = units_after.nodes.len();

    // The new crate's unit appears automatically — no manual insertion.
    assert_eq!(count_after, count_before + 1);
    assert!(units_after.nodes.contains_key("arcature-new"));
    assert!(!units_before.nodes.contains_key("arcature-new"));
}

// =============================================================
// 7. New crate with dependencies gets correct topo order
// =============================================================

#[test]
fn new_crate_gets_correct_topo_order() {
    // A new crate that depends on arcature-inertia should come after
    // arcature-inertia in the topological order.
    let metadata = mock_metadata(vec![
        publishable_package("arcature-inertia", "2026.1.0", "arcature-inertia", vec![]),
        publishable_package(
            "arcature-new",
            "2026.1.0",
            "arcature-new",
            vec![dep_on("arcature-inertia")],
        ),
    ]);

    let crates = discover(&metadata);
    let crate_graph = build_crate_graph(&metadata, &crates);
    let unit_graph = build_unit_graph(&crate_graph);

    let order = unit_graph.topological_order().expect("no cycles");
    let inertia_pos = order.iter().position(|u| u == "arcature-inertia").unwrap();
    let new_pos = order.iter().position(|u| u == "arcature-new").unwrap();
    assert!(
        inertia_pos < new_pos,
        "inertia must come before new in topo order"
    );
}