#![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";
fn mock_metadata(packages: Vec<Package>) -> CargoMetadata {
CargoMetadata { packages }
}
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,
}
}
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,
}
}
#[test]
fn new_crate_auto_discovered() {
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);
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);
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:?}"),
}
}
#[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);
assert!(unit_graph.nodes.contains_key("arcature-future"));
let future_node = &unit_graph.nodes["arcature-future"];
assert!(future_node.dependencies.contains_key("arcature-inertia"));
}
#[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:?}"
);
}
#[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);
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:?}"
);
}
#[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);
let fixture = crates.iter().find(|c| c.name == "arcature-test-fixture");
assert!(fixture.is_some());
assert!(!fixture.unwrap().publishable);
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"));
}
#[test]
fn no_workflow_architecture_change_needed() {
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();
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();
assert_eq!(count_after, count_before + 1);
assert!(units_after.nodes.contains_key("arcature-new"));
assert!(!units_before.nodes.contains_key("arcature-new"));
}
#[test]
fn new_crate_gets_correct_topo_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"
);
}