use super::*;
use crate::fleet_ensure::{
model::{
CanisterPlan, CurrentFleetProtocolAction, CycleConservation, FLEET_ENSURE_SCHEMA_VERSION,
FleetEnsureCompletion, FleetEnsureContinuationAuthority, ReviewedDesiredFleetRecord,
},
ops::{read_journal, read_state},
tests::{MockError, MockPlatform},
};
use canic_core::cdk::types::Cycles;
use std::collections::BTreeMap;
const ROOT: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai";
#[test]
fn recovery_preview_tolerates_bounded_balance_movement_but_preserves_authority() {
use crate::fleet_ensure::model::{FleetRecoveryReview, PoolRecoveryFunding, RecoveryDiscovery};
let (desired, mut retained, _, _) = fixture();
retained.recovery_review = Some(Box::new(FleetRecoveryReview {
base_execution_burn_cycles: 10,
continuation_reserve_cycles: 90,
whole_continuation_ceiling_cycles: 100,
maximum_successor_actions: 10,
fixture_publication_retry_attempts: 0,
per_step_burn_cycles: 10,
startup_funding: Vec::new(),
discovery: RecoveryDiscovery::PendingCurrentProtocol,
known_pool_funding: vec![PoolRecoveryFunding {
principal: "pool".into(),
root: ROOT.into(),
amount_cycles: 4,
ledger_fee_cycles: 1,
funding_deficit_cycles: 1,
funding_margin_cycles: 3,
expected_post_cycles: 10,
}],
}));
let observation = super::super::tests::estate_funding_observation(None);
let mut current = retained.clone();
current.canisters[0].observed_cycles -= 1;
let review = current.recovery_review.as_mut().unwrap();
review.continuation_reserve_cycles -= 1;
review.known_pool_funding[0].amount_cycles += 1;
review.known_pool_funding[0].funding_deficit_cycles += 1;
let compatible = |current: &FleetEnsurePlan| {
super::super::compatible_after_bounded_observation(
&retained,
current,
&desired,
&observation,
)
};
assert!(compatible(¤t));
let mut changed_target = current.clone();
changed_target
.recovery_review
.as_mut()
.unwrap()
.known_pool_funding[0]
.principal = "different-pool".into();
assert!(!compatible(&changed_target));
let mut changed_fee = current.clone();
changed_fee
.recovery_review
.as_mut()
.unwrap()
.known_pool_funding[0]
.ledger_fee_cycles += 1;
assert!(!compatible(&changed_fee));
let mut changed_bound = current.clone();
changed_bound
.recovery_review
.as_mut()
.unwrap()
.per_step_burn_cycles += 1;
assert!(!compatible(&changed_bound));
let mut changed_steps = current.clone();
changed_steps
.recovery_review
.as_mut()
.unwrap()
.maximum_successor_actions += 1;
assert!(!compatible(&changed_steps));
current.canisters[0].observed_cycles -= 1;
assert!(!compatible(¤t));
}
fn fixture() -> (
DesiredFleet,
FleetEnsurePlan,
FleetEnsurePlan,
FleetEnsureJournalRecord,
) {
let desired: DesiredFleet = serde_json::from_value(serde_json::json!({
"canisters": [], "cycles_ledger": "um5iw-rqaaa-aaaaq-qaaba-cai",
"environment": "local", "fleet": "fleet", "ledger_fee_cycles": "0",
"management_creation_fee_cycles": "0", "material_cycle_threshold": "1",
"maximum_observation_burn_cycles": "1", "maximum_stalled_observations": 2,
"maximum_update_burn_cycles": "2", "operator": ROOT,
"schema_version": FLEET_ENSURE_SCHEMA_VERSION, "treasury": "root"
}))
.expect("bounded continuation policy");
let conservation = CycleConservation {
estate_funding_domains: Vec::new(),
expected_post_operation_cycles: 900,
maximum_execution_burn_cycles: 100,
maximum_new_funding_cycles: 0,
maximum_operator_debit_cycles: 0,
maximum_unavoidable_fee_cycles: 0,
observed_controlled_cycles: 1000,
retained_in_reused_canisters_cycles: 1000,
scheduled_transfer_cycles: 0,
};
let mut original = FleetEnsurePlan {
canisters: vec![CanisterPlan {
actions: Vec::new(),
disposition: CanisterDisposition::Reuse,
name: "root".into(),
observed_cycles: 1000,
principal: Some(ROOT.into()),
}],
continuation: Some(FleetEnsureContinuationAuthority {
fixture_publication_retry_attempts: 0,
app_config_sha256: "11".repeat(32),
application_artifact_union_sha256: "22".repeat(32),
coordinator_candid_sha256: "33".repeat(32),
maximum_successor_actions: 2,
root_candid_sha256: "44".repeat(32),
store_candid_sha256: "55".repeat(32),
}),
conservation,
desired_sha256: "66".repeat(32),
environment: "local".into(),
fleet: "fleet".into(),
operation_id: "77".repeat(32),
plan_sha256: String::new(),
planned_at_time: 1,
protocol_actions: Vec::new(),
recovery_review: None,
reinstall: None,
root_reinstall_bindings: Vec::new(),
root_start_authority: None,
reviewed_desired: Some(Box::new(ReviewedDesiredFleetRecord::capture(&desired))),
schema_version: FLEET_ENSURE_SCHEMA_VERSION,
scope: FleetEnsurePlanScope::Full,
terminal_inventory_operation_id: None,
};
original.plan_sha256 = expected_plan_sha256(&original);
let mut phase = original.clone();
phase.continuation = None;
phase.protocol_actions.push(EnsureAction::FleetProtocol {
action: Box::new(CurrentFleetProtocolAction::ObservePoolReadiness {
minimum_ready: 1,
readiness_floor: Cycles::new(5),
}),
candid: "root.did".into(),
candid_sha256: "44".repeat(32),
maximum_execution_burn_cycles: 0,
name: "readiness".into(),
principal: ROOT.into(),
});
phase.plan_sha256 = expected_plan_sha256(&phase);
let journal = FleetEnsureJournalRecord {
funding_reviews: Vec::new(),
successor_phases: Vec::new(),
completion: FleetEnsureCompletion::InProgress,
estate_funding_required: None,
effects: Vec::new(),
fleet: original.fleet.clone(),
initial_controlled_cycles: 1000,
initial_estate_funding_cycles_by_root: BTreeMap::new(),
initial_operator_cycles: 2000,
operation_id: original.operation_id.clone(),
plan_sha256: original.plan_sha256.clone(),
schema_version: FLEET_ENSURE_SCHEMA_VERSION,
stalled_observations: 0,
};
(desired, original, phase, journal)
}
fn observation() -> FleetObservation {
FleetObservation {
additional_controlled_cycles: BTreeMap::from([(ROOT.into(), 950)]),
canisters: BTreeMap::new(),
estate_funding_domains: BTreeMap::new(),
ledger_fee_cycles: 0,
operator_cycles: 2000,
protocol_ready: BTreeMap::new(),
}
}
#[test]
fn phase_is_durable_before_intent_and_hydrates_without_inline_actions_in_journal() {
let (desired, original, phase, mut journal) = fixture();
let root = crate::test_support::temp_dir("continuation-durable-phase");
std::fs::create_dir_all(&root).expect("create fixture directory");
let paths = EnsurePaths::under(&root, "local", "fleet");
let mut state = read_state(&paths, "fleet").expect("empty current state");
state.principals.insert("root".into(), ROOT.into());
let mut platform = MockPlatform::new(desired.clone(), []);
platform.set_fresh_protocol_actions(phase.protocol_actions.clone());
append(
&paths,
&desired,
&original,
&mut journal,
&state,
&observation(),
phase.clone(),
&mut platform,
)
.expect("bounded canonical successor");
assert!(journal.effects.is_empty());
assert_eq!(journal.plan_sha256, original.plan_sha256);
assert_eq!(journal.initial_controlled_cycles, 1000);
let restored = read_journal(&paths)
.expect("read persisted phase")
.expect("journal");
assert_eq!(restored, journal);
assert_eq!(restored.successor_phases[0].execution_burn_before_phase, 50);
assert_eq!(restored.successor_phases[0].plan.as_deref(), Some(&phase));
verify_records::<MockError>(&original, &restored).expect("exact restored authority");
let json: serde_json::Value =
serde_json::from_slice(&std::fs::read(&paths.journal).expect("journal bytes"))
.expect("journal JSON");
assert!(json["successor_phases"][0].get("plan").is_none());
std::fs::remove_dir_all(root).expect("remove fixture");
}
#[test]
fn insufficient_remaining_budget_rejects_before_phase_or_intent_persistence() {
let (desired, mut original, phase, mut journal) = fixture();
original.conservation.maximum_execution_burn_cycles = 52;
original.plan_sha256 = expected_plan_sha256(&original);
journal.plan_sha256 = original.plan_sha256.clone();
let root = crate::test_support::temp_dir("continuation-budget");
std::fs::create_dir_all(&root).expect("create fixture directory");
let paths = EnsurePaths::under(&root, "local", "fleet");
let mut state = read_state(&paths, "fleet").expect("empty current state");
state.principals.insert("root".into(), ROOT.into());
let mut platform = MockPlatform::new(desired.clone(), []);
platform.set_fresh_protocol_actions(phase.protocol_actions.clone());
let error = append(
&paths,
&desired,
&original,
&mut journal,
&state,
&observation(),
phase,
&mut platform,
)
.expect_err("past burn consumes the original shared budget");
assert!(matches!(
error,
EnsureWorkflowError::SuccessorReviewRequired {
reason: FleetEnsureSuccessorReviewReason::BudgetExceeded,
..
}
));
assert!(journal.successor_phases.is_empty());
assert!(journal.effects.is_empty());
assert!(!paths.journal.exists());
std::fs::remove_dir_all(root).expect("remove fixture");
}
#[test]
fn a_rehashed_successor_cannot_add_a_canister_effect_or_change_protocol_authority() {
for additional_effect in [false, true] {
let (desired, original, mut phase, mut journal) = fixture();
let root = crate::test_support::temp_dir("continuation-altered-authority");
std::fs::create_dir_all(&root).expect("create fixture directory");
let paths = EnsurePaths::under(&root, "local", "fleet");
let mut state = read_state(&paths, "fleet").expect("empty current state");
state.principals.insert("root".into(), ROOT.into());
let mut platform = MockPlatform::new(desired.clone(), []);
platform.set_fresh_protocol_actions(phase.protocol_actions.clone());
if additional_effect {
phase.canisters[0].actions.push(EnsureAction::Start {
name: "root".into(),
principal: ROOT.into(),
});
} else if let EnsureAction::FleetProtocol { action, .. } = &mut phase.protocol_actions[0] {
**action = CurrentFleetProtocolAction::ObservePoolReadiness {
minimum_ready: 2,
readiness_floor: Cycles::new(5),
};
}
phase.plan_sha256 = expected_plan_sha256(&phase);
let error = append(
&paths,
&desired,
&original,
&mut journal,
&state,
&observation(),
phase,
&mut platform,
)
.expect_err("a new digest grants no new authority");
let expected = if additional_effect {
FleetEnsureSuccessorReviewReason::AdditionalEffect
} else {
FleetEnsureSuccessorReviewReason::ProtocolAuthority
};
assert!(
matches!(error, EnsureWorkflowError::SuccessorReviewRequired { reason, .. } if reason == expected)
);
assert!(journal.successor_phases.is_empty());
assert!(!paths.journal.exists());
std::fs::remove_dir_all(root).expect("remove fixture");
}
}
#[test]
fn successor_rejects_changed_principal_and_exhausted_phase_authority() {
for exceeded in [false, true] {
let (desired, mut original, mut phase, mut journal) = fixture();
let root = crate::test_support::temp_dir("continuation-exact-principals");
std::fs::create_dir_all(&root).expect("create fixture directory");
let paths = EnsurePaths::under(&root, "local", "fleet");
let mut state = read_state(&paths, "fleet").expect("empty current state");
state.principals.insert("root".into(), ROOT.into());
let mut platform = MockPlatform::new(desired.clone(), []);
platform.set_fresh_protocol_actions(phase.protocol_actions.clone());
if exceeded {
original
.continuation
.as_mut()
.expect("authority")
.maximum_successor_actions = 0;
original.plan_sha256 = expected_plan_sha256(&original);
journal.plan_sha256 = original.plan_sha256.clone();
} else {
phase.canisters[0].principal = Some("aaaaa-aa".into());
phase.plan_sha256 = expected_plan_sha256(&phase);
}
let error = append(
&paths,
&desired,
&original,
&mut journal,
&state,
&observation(),
phase,
&mut platform,
)
.expect_err("reviewed authority cannot widen");
let expected = if exceeded {
FleetEnsureSuccessorReviewReason::PhaseBound
} else {
FleetEnsureSuccessorReviewReason::ProtocolAuthority
};
assert!(
matches!(error, EnsureWorkflowError::SuccessorReviewRequired { reason, .. } if reason == expected)
);
assert!(journal.successor_phases.is_empty());
assert!(!paths.journal.exists());
std::fs::remove_dir_all(root).expect("remove fixture");
}
}
#[test]
fn new_review_does_not_revalidate_the_previous_inactive_plans_phases() {
let (desired, original, phase, journal) = fixture();
let mut journal = candidate_journal(&journal, &phase, 50);
let mut successor = original.clone();
successor.operation_id = "88".repeat(32);
successor.continuation = None;
successor.plan_sha256 = expected_plan_sha256(&successor);
let root = crate::test_support::temp_dir("continuation-new-review");
let paths = EnsurePaths::under(&root, "local", "fleet");
let state = read_state(&paths, "fleet").unwrap();
let mut platform = MockPlatform::new(desired, []);
for completion in [
FleetEnsureCompletion::ReplanRequired,
FleetEnsureCompletion::Converged,
] {
journal.completion = completion;
verify_canonical(&successor, &journal, &state, &mut platform)
.expect("a separately reviewed plan does not inherit prior phases");
assert!(matches!(
verify_canonical(&original, &journal, &state, &mut platform),
Err(EnsureWorkflowError::SuccessorReviewRequired {
reason: FleetEnsureSuccessorReviewReason::ProtocolAuthority,
..
})
));
}
journal.completion = FleetEnsureCompletion::InProgress;
assert!(matches!(
verify_canonical(&successor, &journal, &state, &mut platform),
Err(EnsureWorkflowError::JournalIntegrity)
));
}
#[test]
fn affordable_successor_retains_a_prefix_and_its_exact_conservation_bound() {
let (desired, _, mut phase, _) = fixture();
let first = phase.protocol_actions[0].clone();
let one_burn = successor_phase_burn(&desired, &phase).unwrap();
let mut second = first.clone();
if let EnsureAction::FleetProtocol { name, .. } = &mut second {
*name = "second".into();
}
phase.protocol_actions.push(second);
assert!(successor_phase_burn(&desired, &phase).unwrap() > one_burn);
let affordable =
crate::fleet_ensure::policy::recovery::affordable_successor(&desired, phase, one_burn)
.unwrap()
.unwrap();
assert_eq!(affordable.protocol_actions, vec![first]);
assert_eq!(
affordable.conservation.maximum_execution_burn_cycles,
one_burn
);
assert_eq!(
affordable.conservation.expected_post_operation_cycles,
1000 - one_burn
);
assert_eq!(affordable.plan_sha256, expected_plan_sha256(&affordable));
}
#[test]
fn review_difference_exposes_targets_action_kinds_and_bounded_debit() {
let (_, _, mut phase, _) = fixture();
phase.conservation.maximum_operator_debit_cycles = 47;
let review = crate::fleet_ensure::ops::recovery::review_details(&phase);
assert_eq!(review.maximum_additional_debit_cycles, 47);
assert_eq!(review.actions[0].kind, "observe_pool_readiness");
assert_eq!(review.actions[0].principal.as_deref(), Some(ROOT));
assert_eq!(
review.next_review_command,
"canic fleet ensure 'fleet' --environment 'local'"
);
phase.environment = "local's".into();
assert!(
crate::fleet_ensure::ops::recovery::review_details(&phase)
.next_review_command
.contains("'local'\"'\"'s'")
);
}
#[test]
fn affordable_prefix_is_durable_without_authorizing_the_unaffordable_tail() {
let (desired, original, mut phase, mut journal) = fixture();
let first = phase.protocol_actions[0].clone();
let mut second = first.clone();
if let EnsureAction::FleetProtocol {
name,
maximum_execution_burn_cycles,
..
} = &mut second
{
*name = "later-readiness".into();
*maximum_execution_burn_cycles = 60;
}
phase.protocol_actions.push(second);
phase.plan_sha256 = expected_plan_sha256(&phase);
let root = crate::test_support::temp_dir("continuation-affordable-prefix");
std::fs::create_dir_all(&root).unwrap();
let paths = EnsurePaths::under(&root, "local", "fleet");
let mut state = read_state(&paths, "fleet").unwrap();
state.principals.insert("root".into(), ROOT.into());
let mut platform = MockPlatform::new(desired.clone(), []);
platform.set_fresh_protocol_actions(phase.protocol_actions.clone());
append(
&paths,
&desired,
&original,
&mut journal,
&state,
&observation(),
phase,
&mut platform,
)
.unwrap();
let restored = read_journal(&paths).unwrap().unwrap();
assert_eq!(restored, journal);
assert!(restored.effects.is_empty());
assert_eq!(
restored.successor_phases[0]
.plan
.as_ref()
.unwrap()
.protocol_actions,
vec![first]
);
assert_eq!(restored.operation_id, original.operation_id);
verify_records::<MockError>(&original, &restored).unwrap();
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn fixture_retry_allowance_changes_the_reviewed_plan_digest() {
let (_, mut plan, _, _) = fixture();
let original = expected_plan_sha256(&plan);
plan.continuation
.as_mut()
.unwrap()
.fixture_publication_retry_attempts = 1;
assert_ne!(expected_plan_sha256(&plan), original);
}