use super::*;
use crate::{
fleet_catalog::{FleetCatalogRequest, build_fleet_catalog_report},
fleet_install_plan::{
PlannedCanisterCreationFunding, PlannedFleetCoordinator, PlannedFleetSubnetRoot,
},
test_support::temp_dir,
};
use std::{fs, path::PathBuf};
use canic_core::{
bootstrap::parse_config_model,
cdk::types::Cycles,
dto::fleet_registry::{FleetRegistry, FleetSubnetRootEntry},
ids::{
AppId, CanonicalNetworkId, ComponentSpecAdmission, CyclesFundingBudget, FleetBinding,
FleetId, FleetKey, FleetSubnetRootLimits, FleetSubnetRootReleaseSet, ReleaseBuildId,
ReleaseBuildNonce, ReleaseSetDigest, SubnetId,
},
};
#[test]
fn exact_terminal_evidence_publishes_one_coordinator_row_and_retries_exactly() {
let fixture = fixture("exact");
let committed =
publish_terminal_fleet_catalog(fixture.request()).expect("publish terminal Fleet");
let repeated =
publish_terminal_fleet_catalog(fixture.request()).expect("repeat terminal Fleet");
assert!(committed.advanced);
assert!(!repeated.advanced);
assert_eq!(repeated.entry, committed.entry);
assert_eq!(repeated.catalog_hash, committed.catalog_hash);
assert_eq!(
committed.entry.coordinator_principal,
fixture.coordinator.to_text()
);
let report = build_fleet_catalog_report(&FleetCatalogRequest {
project_root: fixture.root.clone(),
environment: "staging".to_string(),
generated_at: "unix:54".to_string(),
})
.expect("read terminal Fleet catalog");
assert_eq!(report.entries, vec![committed.entry]);
fs::remove_dir_all(fixture.root).expect("remove fixture");
}
#[test]
fn publication_rejects_missing_or_contradictory_root_summaries_before_write() {
let mut fixture = fixture("root-evidence");
fixture.root_summaries.clear();
assert!(matches!(
publish_terminal_fleet_catalog(fixture.request()),
Err(TerminalFleetCatalogPublicationError::RootSetMismatch)
));
fixture.root_summaries = summaries(&fixture.registry);
fixture.root_summaries[0].component_canisters = 3;
assert!(matches!(
publish_terminal_fleet_catalog(fixture.request()),
Err(TerminalFleetCatalogPublicationError::RootSummaryMismatch { .. })
));
assert!(!fixture.catalog_path().exists());
fs::remove_dir_all(fixture.root).expect("remove fixture");
}
#[test]
fn publication_rejects_coordinator_and_registry_evidence_drift() {
let mut fixture = fixture("authority-drift");
fixture.coordinator = principal(55);
assert!(matches!(
publish_terminal_fleet_catalog(fixture.request()),
Err(TerminalFleetCatalogPublicationError::AuthorityMismatch)
));
fixture.coordinator = principal(33);
fixture.registry.version.content_hash = [99; 32];
assert!(matches!(
publish_terminal_fleet_catalog(fixture.request()),
Err(TerminalFleetCatalogPublicationError::RegistryEvidenceMismatch)
));
assert!(!fixture.catalog_path().exists());
fs::remove_dir_all(fixture.root).expect("remove fixture");
}
#[test]
fn publication_rejects_reserved_coordinator_identity() {
let mut fixture = fixture("reserved-coordinator");
fixture.coordinator = Principal::management_canister();
assert!(matches!(
publish_terminal_fleet_catalog(fixture.request()),
Err(TerminalFleetCatalogPublicationError::AuthorityMismatch)
));
assert!(!fixture.catalog_path().exists());
fs::remove_dir_all(fixture.root).expect("remove fixture");
}
struct Fixture {
root: PathBuf,
plan: FleetInstallPlan,
topology: ComponentTopology,
coordinator: Principal,
registry: FleetRegistrySnapshotResponse,
root_summaries: Vec<FleetSubnetRootCanisterSummary>,
}
impl Fixture {
fn request(&self) -> TerminalFleetCatalogPublicationRequest<'_> {
TerminalFleetCatalogPublicationRequest {
project_root: &self.root,
fleet_name: "toko-production".parse().expect("Fleet name"),
environment: "staging",
deployed_at_unix_secs: 54,
fleet_install_plan: &self.plan,
component_topology: &self.topology,
coordinator: self.coordinator,
registry: &self.registry,
root_summaries: &self.root_summaries,
}
}
fn catalog_path(&self) -> PathBuf {
self.root
.join(".canic")
.join("networks")
.join(self.plan.fleet.fleet.canonical_network_id.to_string())
.join("fleets/catalog.json")
}
}
fn fixture(name: &str) -> Fixture {
let root = temp_dir(&format!("terminal-fleet-catalog-{name}"));
fs::create_dir_all(&root).expect("create fixture");
fs::write(
root.join("icp.yaml"),
"environments:\n - name: staging\n network: ic\n",
)
.expect("write ICP config");
let topology = topology();
let release_build_id =
ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes([6; 32]));
let spec = topology
.component_specs
.first()
.expect("one Component Spec");
let admission = ComponentSpecAdmission {
component_spec: spec.component_spec.clone(),
spec_hash: spec.spec_hash,
maximum_root_instances: 2,
};
let limits = root_limits();
let release_set = FleetSubnetRootReleaseSet {
release_build_id,
manifest_digest: ReleaseSetDigest::from_bytes([5; 32]),
};
let topology_digest = topology
.project_for_admissions(std::slice::from_ref(&admission))
.expect("root topology")
.digest()
.expect("topology digest");
let coordinator = principal(33);
let fleet = FleetBinding {
fleet: FleetKey {
canonical_network_id: CanonicalNetworkId::public_ic(),
fleet_id: FleetId::from_generated_bytes([4; 32]),
},
app: AppId::from("toko"),
};
let plan = FleetInstallPlan {
fleet: fleet.clone(),
release_build_id,
application_artifact_union_digest: [3; 32],
coordinator: PlannedFleetCoordinator {
coordinator_subnet: subnet(1),
creation_funding: funding(),
},
fleet_subnet_roots: vec![PlannedFleetSubnetRoot {
placement_subnet: subnet(2),
component_admissions: vec![admission.clone()],
component_topology_digest: topology_digest,
initial_release_set: release_set,
limits: limits.clone(),
creation_funding: funding(),
}],
};
let authority = FleetRegistryAuthority {
binding: FleetCoordinatorBinding {
fleet,
coordinator_subnet: subnet(1),
coordinator,
},
epoch: 1,
};
let mut registry =
FleetRegistryOps::compile_genesis(&plan.fleet.app, authority, &topology).expect("genesis");
registry = FleetRegistryOps::compile_joining(
®istry.authority,
&topology,
®istry,
FleetSubnetRootEntry {
placement_subnet: subnet(2),
fleet_subnet_root: principal(44),
component_admissions: vec![admission],
component_topology_digest: topology_digest,
active_release_set: release_set,
limits,
status: FleetSubnetRootStatus::Joining,
},
)
.expect("Joining root");
registry = FleetRegistryOps::compile_active(®istry.authority, &topology, ®istry)
.expect("Active");
let registry = snapshot(registry, &topology);
let root_summaries = summaries(®istry);
Fixture {
root,
plan,
topology,
coordinator,
registry,
root_summaries,
}
}
fn topology() -> ComponentTopology {
parse_config_model(
r#"
[app]
name = "toko"
[roles.root]
kind = "root"
package = "root"
[roles.project]
kind = "canister"
package = "project"
[component_specs.projects]
component_role = "project"
maximum_instances = 2
"#,
)
.expect("valid config")
.compile_component_topology()
.expect("Component Topology")
}
fn snapshot(
registry: FleetRegistry,
topology: &ComponentTopology,
) -> FleetRegistrySnapshotResponse {
let manifest = FleetRegistryOps::manifest(®istry.authority, topology, ®istry)
.expect("Registry manifest");
let version =
FleetRegistryOps::version(®istry.authority, topology, ®istry).expect("version");
FleetRegistrySnapshotResponse {
registry,
manifest,
version,
}
}
fn summaries(registry: &FleetRegistrySnapshotResponse) -> Vec<FleetSubnetRootCanisterSummary> {
registry
.registry
.fleet_subnet_roots
.iter()
.map(|root| FleetSubnetRootCanisterSummary {
fleet_registry: registry.version.clone(),
placement_subnet: root.placement_subnet,
fleet_subnet_root: root.fleet_subnet_root,
status: root.status,
infrastructure_canisters: 2,
component_canisters: 2,
total_canisters: 4,
})
.collect()
}
fn principal(byte: u8) -> Principal {
Principal::from_slice(&[byte; 29])
}
fn subnet(byte: u8) -> SubnetId {
SubnetId::from_principal(principal(byte))
}
fn funding() -> PlannedCanisterCreationFunding {
PlannedCanisterCreationFunding::Cycles {
cycles: 2_000_000_000_000,
}
}
fn root_limits() -> FleetSubnetRootLimits {
FleetSubnetRootLimits {
maximum_component_instances: 2,
maximum_managed_canisters: 20_000,
maximum_registry_bytes: 2_097_152,
maximum_wasm_store_bytes: 268_435_456,
cycles_funding: CyclesFundingBudget {
window_secs: 3_600,
maximum_cycles: Cycles::new(2_000_000_000_000),
},
}
}