Skip to main content

canic_host/fleet_ensure/ops/
platform.rs

1//! Module: fleet_ensure::ops::platform
2//!
3//! Responsibility: mechanically observe and mutate the exact current Fleet through ICP CLI.
4//! Does not own: effect ordering, durable intent, retry policy, or plan approval.
5//! Boundary: the workflow calls one method only after persisting its exact action identity.
6
7use crate::icp::cycles_ledger::{
8    CanisterSettings, CmcCreateCanisterArgs, CreateCanisterArgs, CreateCanisterError,
9    CreateCanisterSuccess, SubnetSelection,
10};
11use crate::{
12    canister_protocol::{CanisterProtocolError, call_with_candid, query_with_candid},
13    fleet_ensure::{
14        dto::{FleetEnsureProgress, FleetObservationStage, FleetObservationTiming},
15        model::{
16            CanisterRuntimeStatus, DesiredCanisterKind, DesiredFleet, EffectRecord, EnsureAction,
17            EstateFundingDomainObservation, EstatePoolAssetLifecycle, EstatePoolAssetObservation,
18            EstatePoolAssetOrigin, EstatePoolCreationDiagnostic,
19            EstatePoolCreationReceiptObservation, EstatePoolInventoryObservation,
20            EstatePoolPendingCreationObservation, FleetEnsureStateRecord, FleetObservation,
21            InstallMode, LiveCanister, MAX_FLEET_ENSURE_CANISTERS, RetirementTransferBalances,
22            RetirementTransferInvariantError, RetirementTransferReconciliation,
23            RootManagementCanisterObservation, RootManagementObservation,
24            RootOwnedCanisterLifecycle, create_balance_is_terminal, reconcile_retirement_transfer,
25        },
26        ops::{
27            EffectObservation, EffectOutcome, EffectRetry, EnsurePlatform, EnsureStateError,
28            TerminalFleetInventory, canic_init, current_protocol, protocol, root_owned_lifecycle,
29        },
30    },
31    icp::{
32        IcpCandidCallError, IcpCanisterStatusReport, IcpCli, IcpCommandError, IcpDiagnostic,
33        IcpManagementCallError, LocalReplicaTarget, run_status,
34    },
35    icp_config::resolve_icp_build_network_from_root,
36    subnet_catalog::load_cached_mainnet_subnet_catalog,
37};
38use candid::{CandidType, Nat, Principal};
39use canic_core::{
40    cdk::{types::Cycles, utils::hash::hex_bytes},
41    dto::canister::CanisterInspectionRequest,
42    dto::pool::{
43        CanisterPoolAsset, CanisterPoolAssetOrigin, CanisterPoolAssetStatus,
44        CanisterPoolCreationFailure, CanisterPoolCreationProgress, CanisterPoolHandoff,
45        CanisterPoolResponse, CanisterPoolStatusRequest,
46    },
47    ids::BuildNetwork,
48    protocol as canic_protocol,
49};
50use serde::Deserialize;
51use std::{
52    cell::RefCell,
53    collections::{BTreeMap, BTreeSet},
54    path::{Path, PathBuf},
55    thread,
56    time::{Duration, SystemTime, UNIX_EPOCH},
57};
58use thiserror::Error as ThisError;
59
60#[derive(CandidType)]
61struct ManagementCanisterStatusRequest {
62    canister_id: Principal,
63}
64
65#[derive(CandidType)]
66struct CyclesLedgerAccount {
67    owner: Principal,
68    subaccount: Option<[u8; 32]>,
69}
70
71#[derive(CandidType)]
72struct CyclesLedgerTransferArgs {
73    amount: Nat,
74    created_at_time: Option<u64>,
75    fee: Option<Nat>,
76    from_subaccount: Option<[u8; 32]>,
77    memo: Option<Vec<u8>>,
78    to: CyclesLedgerAccount,
79}
80
81#[derive(CandidType, Deserialize)]
82enum CyclesLedgerTransferError {
83    BadBurn { min_burn_amount: Nat },
84    BadFee { expected_fee: Nat },
85    CreatedInFuture { ledger_time: u64 },
86    Duplicate { duplicate_of: Nat },
87    GenericError { error_code: Nat, message: String },
88    InsufficientFunds { balance: Nat },
89    TemporarilyUnavailable,
90    TooOld,
91}
92
93#[derive(CandidType, Deserialize)]
94struct ManagementCanisterStatusResponse {
95    version: u64,
96    module_hash: Option<Vec<u8>>,
97}
98
99#[derive(CandidType, Clone, Deserialize)]
100struct ManagementCanisterObservationSettings {
101    controllers: Vec<Principal>,
102}
103
104#[derive(CandidType, Deserialize)]
105enum ManagementCanisterRuntimeStatus {
106    #[serde(rename = "running")]
107    Running,
108    #[serde(rename = "stopping")]
109    Stopping,
110    #[serde(rename = "stopped")]
111    Stopped,
112}
113
114#[derive(CandidType, Deserialize)]
115struct ManagementCanisterObservationResponse {
116    status: ManagementCanisterRuntimeStatus,
117    settings: ManagementCanisterObservationSettings,
118    module_hash: Option<Vec<u8>>,
119    cycles: Nat,
120    version: u64,
121}
122
123#[derive(Debug, Eq, PartialEq)]
124struct ExactInstallCanisterStatus {
125    canister_version: u64,
126    module_sha256: Option<String>,
127}
128
129fn is_unallocated_fresh_root(
130    desired: &DesiredFleet,
131    configured: &crate::fleet_ensure::model::DesiredCanister,
132    reviewed_targets: &BTreeSet<String>,
133) -> bool {
134    reviewed_targets.is_empty()
135        && configured.principal.is_none()
136        && desired.bootstrap.as_ref().is_some_and(|bootstrap| {
137            bootstrap.fresh_estate
138                && bootstrap
139                    .roots
140                    .iter()
141                    .any(|root| root.root == configured.name)
142        })
143}
144
145/// Exact input expected by a configured cycle-safe retirement endpoint.
146
147#[derive(CandidType)]
148struct DrainRequest {
149    operation_id: String,
150    destination: Principal,
151    cycles: Nat,
152}
153
154#[derive(CandidType, Deserialize)]
155enum DrainResponse {
156    Accepted { transferred_cycles: Nat },
157    Replayed { transferred_cycles: Nat },
158}
159
160#[derive(CandidType)]
161enum RootPoolStatusRequest {
162    Pool(CanisterPoolStatusRequest),
163}
164
165#[derive(CandidType, Deserialize)]
166enum RootPoolStatusResponse {
167    Pool(Box<CanisterPoolResponse>),
168}
169
170#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
171struct EstatePoolLifecycleCounts {
172    claimed: u32,
173    failed: u32,
174    handing_off: u32,
175    pending_reset: u32,
176    ready: u32,
177    recycling: u32,
178    store: u32,
179    store_deletion_pending: u32,
180    tracked: u32,
181    workload: u32,
182}
183
184impl EstatePoolLifecycleCounts {
185    const fn declared(response: &CanisterPoolResponse) -> Self {
186        Self {
187            claimed: response.claimed,
188            failed: response.failed,
189            handing_off: response.handing_off,
190            pending_reset: response.pending_reset,
191            ready: response.ready,
192            recycling: response.recycling,
193            store: response.store,
194            store_deletion_pending: response.store_deletion_pending,
195            tracked: response.tracked,
196            workload: response.workload,
197        }
198    }
199
200    fn observe(&mut self, status: &CanisterPoolAssetStatus) -> Option<()> {
201        self.tracked = self.tracked.checked_add(1)?;
202        let count = match status {
203            CanisterPoolAssetStatus::Store => &mut self.store,
204            CanisterPoolAssetStatus::StoreDeletionPending { .. } => {
205                &mut self.store_deletion_pending
206            }
207            CanisterPoolAssetStatus::PendingReset => &mut self.pending_reset,
208            CanisterPoolAssetStatus::Ready => &mut self.ready,
209            CanisterPoolAssetStatus::Claimed { .. } => &mut self.claimed,
210            CanisterPoolAssetStatus::Workload { .. } => &mut self.workload,
211            CanisterPoolAssetStatus::Recycling { .. } => &mut self.recycling,
212            CanisterPoolAssetStatus::HandingOff { .. } => &mut self.handing_off,
213            CanisterPoolAssetStatus::Failed { .. } => &mut self.failed,
214        };
215        *count = count.checked_add(1)?;
216        Some(())
217    }
218
219    fn pooled(self) -> Option<u32> {
220        self.pending_reset
221            .checked_add(self.ready)?
222            .checked_add(self.handing_off)?
223            .checked_add(self.failed)
224    }
225}
226
227#[derive(Clone, Debug, Eq, PartialEq)]
228struct EstatePoolPageAuthority {
229    completed_handoffs: u64,
230    counts: EstatePoolLifecycleCounts,
231    pending_handoff: Option<CanisterPoolHandoff>,
232    pooled: u32,
233    surplus: u32,
234}
235
236impl EstatePoolPageAuthority {
237    fn from_response(response: &CanisterPoolResponse) -> Self {
238        Self {
239            completed_handoffs: response.completed_handoffs,
240            counts: EstatePoolLifecycleCounts::declared(response),
241            pending_handoff: response.pending_handoff.clone(),
242            pooled: response.pooled,
243            surplus: response.surplus,
244        }
245    }
246
247    fn matches_complete_inventory(
248        &self,
249        observed: EstatePoolLifecycleCounts,
250        maximum_size: u32,
251    ) -> bool {
252        let Some(pooled) = observed.pooled() else {
253            return false;
254        };
255        self.counts == observed
256            && self.pooled == pooled
257            && self.surplus == pooled.saturating_sub(maximum_size)
258    }
259}
260
261#[derive(Default)]
262struct EstatePoolInventoryAccumulator {
263    assets: Vec<EstatePoolAssetObservation>,
264    expected_config: Option<canic_core::ids::FleetSubnetCanisterPoolConfig>,
265    expected_page_authority: Option<EstatePoolPageAuthority>,
266    expected_pending: Option<canic_core::dto::pool::CanisterPoolCreation>,
267    observed_counts: EstatePoolLifecycleCounts,
268    seen: BTreeSet<Principal>,
269}
270
271impl EstatePoolInventoryAccumulator {
272    fn observe_page(
273        &mut self,
274        root_name: &str,
275        page: CanisterPoolResponse,
276    ) -> Result<Option<Principal>, IcpEnsurePlatformError> {
277        let page_authority = EstatePoolPageAuthority::from_response(&page);
278        let first_page = self.expected_config.is_none();
279        if !first_page
280            && (self.expected_config.as_ref() != Some(&page.config)
281                || self.expected_pending != page.pending_creation
282                || self.expected_page_authority.as_ref() != Some(&page_authority))
283        {
284            return Err(pool_configuration_error(format!(
285                "Root {root_name} pool authority changed during pagination"
286            )));
287        }
288        if first_page {
289            self.expected_config = Some(page.config.clone());
290            self.expected_pending.clone_from(&page.pending_creation);
291            self.expected_page_authority = Some(page_authority);
292        }
293
294        for asset in page.entries {
295            if !self.seen.insert(asset.canister_id) {
296                return Err(pool_configuration_error(format!(
297                    "Root {root_name} pool repeats canister {}",
298                    asset.canister_id
299                )));
300            }
301            if self.seen.len() > MAX_FLEET_ENSURE_CANISTERS
302                || self.observed_counts.observe(&asset.status).is_none()
303            {
304                return Err(pool_configuration_error(format!(
305                    "Root {root_name} pool exceeds the Fleet observation bound"
306                )));
307            }
308            if let Some(lifecycle) = estate_pool_lifecycle(&asset.status) {
309                self.assets.push(EstatePoolAssetObservation {
310                    creation_receipt: asset.creation_receipt.map(|receipt| {
311                        EstatePoolCreationReceiptObservation {
312                            block_index: receipt.block_index,
313                            operation_id: hex_bytes(receipt.operation_id),
314                            cycles_ledger: receipt.cycles_ledger.to_text(),
315                            ledger_amount_cycles: receipt.ledger_amount.to_u128(),
316                            ledger_fee_cycles: receipt.ledger_fee.to_u128(),
317                            readiness_floor_cycles: receipt.readiness_floor.to_u128(),
318                            creation_execution_margin_cycles: receipt
319                                .creation_execution_margin
320                                .to_u128(),
321                            management_creation_fee_cycles: receipt
322                                .management_creation_fee
323                                .to_u128(),
324                            first_observed_cycles: receipt
325                                .first_observed_cycles
326                                .map(|cycles| cycles.to_u128()),
327                        }
328                    }),
329                    cycles: asset.cycles.to_u128(),
330                    lifecycle,
331                    origin: match asset.origin {
332                        CanisterPoolAssetOrigin::InfrastructureStore => {
333                            EstatePoolAssetOrigin::InfrastructureStore
334                        }
335                        CanisterPoolAssetOrigin::Created => EstatePoolAssetOrigin::Created,
336                        CanisterPoolAssetOrigin::Imported => EstatePoolAssetOrigin::Imported,
337                        CanisterPoolAssetOrigin::Recycled => EstatePoolAssetOrigin::Recycled,
338                    },
339                    principal: asset.canister_id.to_text(),
340                });
341            }
342        }
343        Ok(page.next_start_after)
344    }
345
346    fn finish(
347        self,
348        root_name: &str,
349    ) -> Result<EstatePoolInventoryObservation, IcpEnsurePlatformError> {
350        let config = self.expected_config.ok_or_else(|| {
351            pool_configuration_error(format!("Root {root_name} returned no pool authority"))
352        })?;
353        if !self.expected_page_authority.is_some_and(|authority| {
354            authority.matches_complete_inventory(self.observed_counts, config.maximum_size)
355        }) {
356            return Err(pool_configuration_error(format!(
357                "Root {root_name} pool lifecycle totals differ from its complete paged inventory"
358            )));
359        }
360        Ok(EstatePoolInventoryObservation {
361            assets: self.assets,
362            maximum_size: config.maximum_size,
363            minimum_size: config.minimum_size,
364            pending_creation: self.expected_pending.map(estate_pool_pending_creation),
365            readiness_floor_cycles: config.canister_cycles.to_u128(),
366            creation_execution_margin_cycles: config.creation_execution_margin.to_u128(),
367        })
368    }
369}
370
371fn pool_configuration_error(reason: String) -> IcpEnsurePlatformError {
372    current_protocol::CurrentProtocolError::Configuration(reason).into()
373}
374
375// Inventory is not callback-settlement evidence: an expired maintenance lease can be taken over.
376fn inactive_source_pool_has_full_inventory(
377    pool: &CanisterPoolResponse,
378    root: &crate::fleet_ensure::model::RootActivationResetRecord,
379) -> bool {
380    [
381        pool.config.maximum_size > 0,
382        pool.ready.checked_add(pool.workload) == Some(pool.config.maximum_size),
383        pool.config.maximum_size.checked_add(1) == Some(pool.tracked),
384        pool.ready >= pool.config.minimum_size,
385        root.component_count.checked_add(root.managed_descendants) == Some(pool.workload),
386        pool.store == 1,
387        pool.store_deletion_pending == 0,
388        pool.pending_reset == 0,
389        pool.claimed == 0,
390        pool.recycling == 0,
391        pool.handing_off == 0,
392        pool.failed == 0,
393        pool.pending_creation.is_none(),
394        pool.pending_handoff.is_none(),
395    ]
396    .into_iter()
397    .all(|fact| fact)
398}
399
400const fn pool_policy_is_current(
401    observed: &EstatePoolInventoryObservation,
402    desired: &canic_core::ids::FleetSubnetCanisterPoolConfig,
403) -> bool {
404    observed.maximum_size == desired.maximum_size
405        && observed.minimum_size == desired.minimum_size
406        && observed.readiness_floor_cycles == desired.canister_cycles.to_u128()
407        && observed.creation_execution_margin_cycles == desired.creation_execution_margin.to_u128()
408}
409
410fn estate_pool_pending_creation(
411    creation: canic_core::dto::pool::CanisterPoolCreation,
412) -> EstatePoolPendingCreationObservation {
413    let (
414        available_cycles,
415        created_principal,
416        diagnostic,
417        required_cycles,
418        retry_at_ns,
419        shortfall_cycles,
420        uncertain_result,
421    ) = match creation.progress {
422        CanisterPoolCreationProgress::Intent { uncertain_result } => {
423            (None, None, None, None, None, None, uncertain_result)
424        }
425        CanisterPoolCreationProgress::Created { canister_id, .. } => (
426            None,
427            Some(canister_id.to_text()),
428            None,
429            None,
430            None,
431            None,
432            false,
433        ),
434        CanisterPoolCreationProgress::WaitingForFunding {
435            available,
436            required,
437            retry_at_ns,
438            shortfall,
439            ..
440        } => (
441            Some(available.to_u128()),
442            None,
443            Some(EstatePoolCreationDiagnostic::FundingRequired),
444            Some(required.to_u128()),
445            Some(retry_at_ns),
446            Some(shortfall.to_u128()),
447            false,
448        ),
449        CanisterPoolCreationProgress::Blocked { failure } => (
450            None,
451            None,
452            Some(match failure {
453                CanisterPoolCreationFailure::UnresolvedAfterLedgerWindow => {
454                    EstatePoolCreationDiagnostic::UnresolvedAfterLedgerWindow
455                }
456                CanisterPoolCreationFailure::LedgerCreationFailed => {
457                    EstatePoolCreationDiagnostic::LedgerCreationFailed
458                }
459                CanisterPoolCreationFailure::LedgerRejected => {
460                    EstatePoolCreationDiagnostic::LedgerRejected
461                }
462            }),
463            None,
464            None,
465            None,
466            false,
467        ),
468    };
469    EstatePoolPendingCreationObservation {
470        attempt_count: creation.attempt_count,
471        available_cycles,
472        creation_amount_cycles: creation.ledger_amount.to_u128(),
473        created_principal,
474        diagnostic,
475        last_attempt_at_ns: creation.last_attempt_at_ns,
476        operation_id: hex_bytes(creation.operation_id),
477        required_cycles,
478        retry_at_ns,
479        shortfall_cycles,
480        uncertain_result,
481    }
482}
483
484const fn estate_pool_lifecycle(
485    status: &CanisterPoolAssetStatus,
486) -> Option<EstatePoolAssetLifecycle> {
487    match status {
488        CanisterPoolAssetStatus::Store | CanisterPoolAssetStatus::StoreDeletionPending { .. } => {
489            None
490        }
491        CanisterPoolAssetStatus::PendingReset => Some(EstatePoolAssetLifecycle::PendingReset),
492        CanisterPoolAssetStatus::Ready => Some(EstatePoolAssetLifecycle::Ready),
493        CanisterPoolAssetStatus::Claimed { .. } => Some(EstatePoolAssetLifecycle::Claimed),
494        CanisterPoolAssetStatus::Workload { .. } => Some(EstatePoolAssetLifecycle::Workload),
495        CanisterPoolAssetStatus::Recycling { .. } => Some(EstatePoolAssetLifecycle::Recycling),
496        CanisterPoolAssetStatus::HandingOff { .. } => Some(EstatePoolAssetLifecycle::HandingOff),
497        CanisterPoolAssetStatus::Failed { .. } => Some(EstatePoolAssetLifecycle::Failed),
498    }
499}
500
501#[derive(CandidType)]
502enum RootInspectionCommand {
503    InspectCanister(CanisterInspectionRequest),
504}
505
506/// Protected management evidence shared by Root-owned observation and pool funding.
507#[derive(CandidType, Deserialize)]
508enum RootInspectionResponse {
509    InspectCanister(RootInspectionStatus),
510    InspectionReserveRequired(canic_core::dto::canister::CanisterInspectionReserveResponse),
511}
512
513impl RootInspectionResponse {
514    fn into_status(
515        self,
516        root: Principal,
517        target: Principal,
518    ) -> Result<RootInspectionStatus, crate::CanisterProtocolError> {
519        match self {
520            Self::InspectCanister(status) => Ok(status),
521            Self::InspectionReserveRequired(evidence) => Err(
522                crate::CanisterProtocolError::inspection_reserve(root, target, evidence),
523            ),
524        }
525    }
526}
527
528#[derive(CandidType, Clone, Deserialize)]
529struct RootInspectionStatus {
530    status: canic_core::dto::canister::CanisterStatusType,
531    settings: ManagementCanisterObservationSettings,
532    module_hash: Option<Vec<u8>>,
533    cycles: Nat,
534}
535
536/// Fresh management evidence validated for one retained reset asset.
537struct ReinstallAssetStatus {
538    response: RootInspectionStatus,
539    controllers: Vec<String>,
540    cycles: u128,
541}
542
543#[derive(CandidType)]
544enum ManagedCanisterStatusRequest {
545    CycleBalance,
546}
547
548#[derive(CandidType, Deserialize)]
549enum ManagedCanisterStatusResponse {
550    CycleBalance(canic_core::dto::role::CycleBalanceStatusResponse),
551}
552
553struct CreateCanisterAuthority<'a> {
554    controller_canisters: &'a [String],
555    controllers: &'a [String],
556    created_at_time: u64,
557    ledger: &'a str,
558    requested_initial_cycles: u128,
559    subnet: &'a str,
560}
561
562#[derive(CandidType)]
563struct WithdrawArgs {
564    amount: Nat,
565    created_at_time: Option<u64>,
566    from_subaccount: Option<[u8; 32]>,
567    to: Principal,
568}
569
570#[derive(CandidType, Deserialize)]
571enum WithdrawError {
572    BadFee {
573        expected_fee: Nat,
574    },
575    CreatedInFuture {
576        ledger_time: u64,
577    },
578    Duplicate {
579        duplicate_of: Nat,
580    },
581    FailedToWithdraw {
582        fee_block: Option<Nat>,
583        rejection_code: RejectionCode,
584        rejection_reason: String,
585    },
586    GenericError {
587        error_code: Nat,
588        message: String,
589    },
590    InsufficientFunds {
591        balance: Nat,
592    },
593    InvalidReceiver {
594        receiver: Principal,
595    },
596    TemporarilyUnavailable,
597    TooOld,
598}
599
600#[derive(CandidType, Deserialize)]
601enum RejectionCode {
602    CanisterError,
603    CanisterReject,
604    DestinationInvalid,
605    NoError,
606    SysFatal,
607    SysTransient,
608    Unknown,
609}
610
611/// Typed failure at the current IC effect boundary.
612
613#[derive(Debug, ThisError)]
614pub enum IcpEnsurePlatformError {
615    #[error("mainnet Subnet Catalog observation failed: {0}")]
616    SubnetCatalog(#[source] Box<ic_query::subnet_catalog::SubnetCatalogLoadFailure>),
617
618    #[error("cycle arithmetic overflow while executing {0}")]
619    Arithmetic(&'static str),
620
621    #[error(
622        "management history does not prove the issued reinstall of {canister}; refusing another reset"
623    )]
624    ReinstallHistoryConflict { canister: String },
625
626    #[error("configured Candid file is not a regular file: {}", .0.display())]
627    CandidUnavailable(PathBuf),
628
629    #[error("configured effect artifact is unavailable: {}", .0.display())]
630    ArtifactUnavailable(PathBuf),
631
632    #[error("configured {kind} changed after plan review: expected {expected}, observed {actual}")]
633    ArtifactDigestMismatch {
634        actual: String,
635        expected: String,
636        kind: &'static str,
637    },
638
639    #[error("configured Principal is invalid for {field}: {value}")]
640    InvalidPrincipal { field: &'static str, value: String },
641
642    #[error("Cycles Ledger returned an unsupported fee value: {0}")]
643    InvalidLedgerFee(String),
644
645    #[error("active ICP identity is {actual}, but reviewed Fleet operator is {expected}")]
646    OperatorMismatch { actual: String, expected: String },
647
648    #[error("management status for configured canister {expected} returned identity {actual}")]
649    StatusIdentityMismatch { actual: String, expected: String },
650
651    #[error("retained Root management observation failed: {0}")]
652    RootManagement(String),
653
654    #[error("ICP status has invalid cycle balance for {canister}: {value}")]
655    InvalidStatusCycles { canister: String, value: String },
656
657    #[error("ICP status omitted exact {field} required for controlled canister {canister}")]
658    IncompleteCanisterStatus {
659        canister: String,
660        field: &'static str,
661    },
662
663    #[error(
664        "ICP returned only public non-controller status for {canister}; exact runtime and cycle evidence is unavailable"
665    )]
666    PublicCanisterStatusUnavailable { canister: String },
667
668    #[error(
669        "Root-owned observation for {canister} conflicts with reviewed {field}; no paid effect was repeated"
670    )]
671    RootOwnedObservationAuthorityConflict {
672        canister: String,
673        field: &'static str,
674    },
675
676    #[error(
677        "ICP CLI status JSON omitted canister_version for {canister}, and the exact typed \
678         effective-ID-routed management-canister status call failed: {source}; no install was \
679         issued. Restore the selected controller identity and management-status access, then \
680         resume the same reviewed plan"
681    )]
682    InstallVersionProofUnavailable {
683        canister: String,
684        #[source]
685        source: Box<IcpManagementCallError>,
686    },
687
688    #[error("typed local management-status observation failed: {0}")]
689    LocalManagementStatus(#[source] Box<IcpManagementCallError>),
690
691    #[error(
692        "completed reinstall proof for {canister} conflicts with current {field}; no install was authorized"
693    )]
694    CompletedReinstallAuthorityConflict {
695        canister: String,
696        field: &'static str,
697    },
698
699    #[error(transparent)]
700    Candid(#[from] IcpCandidCallError),
701
702    #[error(transparent)]
703    Icp(#[from] IcpCommandError),
704
705    #[error("Cycles Ledger create_canister failed: {0}")]
706    LedgerCreate(String),
707
708    #[error("Cycles Ledger duplicate does not yet identify its created canister")]
709    LedgerCreatePending,
710
711    #[error(
712        "Root {root} pool policy differs from desired input; explicitly review infrastructure reinstall before resetting it"
713    )]
714    PoolPolicyReinstallReviewRequired { root: String },
715
716    #[error("Cycles Ledger withdraw failed: {0}")]
717    LedgerWithdraw(String),
718
719    #[error("completed withdrawal balance conflicts with reviewed funding bounds: {observation:?}")]
720    NativeFundingBalanceDrift {
721        observation: Box<NativeFundingObservation>,
722    },
723
724    #[error("Cycles Ledger estate funding transfer failed: {0}")]
725    LedgerTransfer(String),
726
727    #[error(
728        "Cycles Ledger transfer fee changed from reviewed {reviewed_fee_cycles} to {expected_fee_cycles}; no transfer was accepted"
729    )]
730    LedgerTransferFeeChanged {
731        expected_fee_cycles: u128,
732        reviewed_fee_cycles: u128,
733    },
734
735    #[error(
736        "Root-authorized funding inspection for {canister} conflicts with reviewed {field}; no Ledger withdrawal was repeated"
737    )]
738    FundingInspectionAuthorityConflict {
739        canister: String,
740        field: &'static str,
741    },
742
743    #[error(
744        "canister {canister} retains {actual} cycles above deletion limit {maximum}; left untouched"
745    )]
746    MaterialCycles {
747        actual: u128,
748        canister: String,
749        maximum: u128,
750    },
751
752    #[error("canister {canister} is not stopped; deletion was not attempted")]
753    NotStopped { canister: String },
754
755    #[error("effect references unresolved created canister {0}")]
756    UnresolvedCreated(String),
757
758    #[error("configured retirement endpoint transferred {actual} cycles, expected {expected}")]
759    WrongTransferAmount { actual: Nat, expected: u128 },
760
761    #[error(
762        "retirement transfer for {canister} is not conserved: source debit {source_debit}, treasury credit {destination_credit}, expected credit {expected}, maximum source debit {maximum_source_debit}; source and treasury were left untouched by later retirement steps"
763    )]
764    UnbalancedTransfer {
765        canister: String,
766        destination_credit: u128,
767        expected: u128,
768        maximum_source_debit: u128,
769        source_debit: u128,
770    },
771
772    #[error("retirement transfer for {canister} is missing its durable {side} balance")]
773    MissingTransferBalance {
774        canister: String,
775        side: &'static str,
776    },
777
778    #[error("retirement transfer balance for {canister} moved in an invalid direction")]
779    InvalidTransferBalance { canister: String },
780
781    #[error(transparent)]
782    CurrentProtocol(#[from] current_protocol::CurrentProtocolError),
783
784    #[error(transparent)]
785    CanicInit(#[from] canic_init::CanicInitError),
786
787    #[error(transparent)]
788    Protocol(#[from] protocol::ProtocolEffectError),
789
790    #[error("retained Root status authority is invalid: {0}")]
791    RetainedRootStatusAuthority(#[source] Box<EnsureStateError>),
792}
793
794/// Read evidence shared only by projections of one Fleet observation.
795#[derive(Default)]
796struct FleetObservationSnapshot {
797    pool_inspections: BTreeMap<PoolInspectionAuthority, RootInspectionStatus>,
798    pool_pages: BTreeMap<(Principal, Option<Principal>), CanisterPoolResponse>,
799    statuses: BTreeMap<String, Option<LiveCanister>>,
800}
801
802/// An inspection response belongs to the exact protected Root and requested asset.
803#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
804struct PoolInspectionAuthority {
805    root: Principal,
806    target: Principal,
807}
808
809impl PoolInspectionAuthority {
810    fn parse(root: &str, target: &str) -> Result<Self, IcpEnsurePlatformError> {
811        Ok(Self {
812            root: parse_principal("Fleet Subnet Root", root)?,
813            target: parse_principal("Root-owned inspection target", target)?,
814        })
815    }
816}
817
818/// Consumer requirements kept separate from the transport result shared by one batch.
819enum PoolInspectionRequirement {
820    PendingFresh,
821    RootControlled(InspectedModule),
822}
823
824/// Prepared only after the caller's Root and operator checks succeed.
825struct PreparedPoolInspection<'a> {
826    authority: PoolInspectionAuthority,
827    name: &'a str,
828    root: &'a str,
829    requirement: PoolInspectionRequirement,
830}
831
832/// A configured pending asset awaiting its independent management inspection.
833struct PendingPoolObservation<'a> {
834    configured: &'a crate::fleet_ensure::model::DesiredCanister,
835    principal: &'a str,
836    asset: CanisterPoolAsset,
837    inspection: PreparedPoolInspection<'a>,
838}
839
840/// Pool-page and Root checks stay ordered before independent inspections overlap.
841enum PreparedRootOwnedObservation<'a> {
842    Observed(Option<LiveCanister>),
843    Pending(Box<PendingPoolObservation<'a>>),
844}
845
846/// Completed transport outcomes local to one drained inspection batch.
847#[derive(Default)]
848struct PoolInspectionBatch {
849    responses:
850        BTreeMap<PoolInspectionAuthority, Result<RootInspectionStatus, IcpEnsurePlatformError>>,
851}
852
853/// Distinguish an unobserved identity from an observed missing canister.
854enum StatusCacheEntry {
855    Miss,
856    Hit(Option<LiveCanister>),
857}
858
859/// Production ICP adapter for the current desired Fleet.
860pub struct IcpEnsurePlatform {
861    desired: DesiredFleet,
862    icp: IcpCli,
863    initial_observation_delay: Duration,
864    maximum_observation_delay: Duration,
865    observation_snapshot: RefCell<Option<FleetObservationSnapshot>>,
866    progress_handler: Option<Box<dyn FnMut(FleetEnsureProgress)>>,
867    observation_handler: Option<Box<dyn FnMut(FleetObservationTiming)>>,
868    estate_observations: BTreeMap<String, EstateFundingDomainObservation>,
869    root: PathBuf,
870}
871
872const INITIAL_PROTOCOL_OBSERVATION_DELAY: Duration = Duration::from_millis(250);
873const MAXIMUM_PROTOCOL_OBSERVATION_DELAY: Duration = Duration::from_secs(5);
874
875fn protocol_observation_delay(
876    consecutive_unchanged_observations: u32,
877    initial_observation_delay: Duration,
878    maximum_observation_delay: Duration,
879) -> Duration {
880    let exponent = consecutive_unchanged_observations.saturating_sub(1).min(5);
881    let multiplier = 1_u32.checked_shl(exponent).unwrap_or(u32::MAX);
882    initial_observation_delay
883        .checked_mul(multiplier)
884        .unwrap_or(MAXIMUM_PROTOCOL_OBSERVATION_DELAY)
885        .min(MAXIMUM_PROTOCOL_OBSERVATION_DELAY)
886        .min(maximum_observation_delay)
887}
888
889impl IcpEnsurePlatform {
890    #[must_use]
891    pub fn new(desired: DesiredFleet, icp_executable: &str, root: &Path) -> Self {
892        let icp = IcpCli::new(icp_executable, Some(desired.environment.clone()))
893            .with_cwd(root.to_path_buf());
894        Self {
895            desired,
896            icp,
897            initial_observation_delay: INITIAL_PROTOCOL_OBSERVATION_DELAY,
898            maximum_observation_delay: MAXIMUM_PROTOCOL_OBSERVATION_DELAY,
899            observation_snapshot: RefCell::new(None),
900            progress_handler: None,
901            observation_handler: None,
902            estate_observations: BTreeMap::new(),
903            root: root.to_path_buf(),
904        }
905    }
906
907    /// Attach an informational progress sink to this operation.
908    #[must_use]
909    pub fn with_progress_handler(
910        mut self,
911        handler: impl FnMut(FleetEnsureProgress) + 'static,
912    ) -> Self {
913        self.progress_handler = Some(Box::new(handler));
914        self
915    }
916
917    /// Attach informational stage timings without retaining observations across boundaries.
918    #[must_use]
919    pub fn with_observation_handler(
920        mut self,
921        handler: impl FnMut(FleetObservationTiming) + 'static,
922    ) -> Self {
923        self.observation_handler = Some(Box::new(handler));
924        self
925    }
926
927    fn finish_observation_timing(
928        &mut self,
929        stage: FleetObservationStage,
930        started: std::time::Instant,
931        calls_before: u64,
932        succeeded: bool,
933    ) {
934        let timing = FleetObservationTiming {
935            stage,
936            elapsed_millis: started.elapsed().as_millis(),
937            remote_call_attempts: self.icp.remote_call_count().saturating_sub(calls_before),
938            succeeded,
939        };
940        if let Some(handler) = &mut self.observation_handler {
941            handler(timing);
942        }
943    }
944
945    fn timed_observation<T>(
946        &mut self,
947        stage: FleetObservationStage,
948        observe: impl FnOnce(&mut Self) -> Result<T, IcpEnsurePlatformError>,
949    ) -> Result<T, IcpEnsurePlatformError> {
950        let started = std::time::Instant::now();
951        let calls = self.icp.remote_call_count();
952        let result = observe(self);
953        self.finish_observation_timing(stage, started, calls, result.is_ok());
954        result
955    }
956
957    /// Return this adapter bound to one explicit local replica endpoint.
958    ///
959    /// This keeps every ordinary ICP CLI call and effective-ID management
960    /// observation on the same replica when no named ICP project network owns
961    /// the test or operator session.
962    #[must_use]
963    pub fn with_local_replica(mut self, target: LocalReplicaTarget) -> Self {
964        self.icp = self.icp.with_local_replica(Some(target));
965        self
966    }
967
968    /// Select passive observation pacing without changing retry or stall bounds.
969    ///
970    /// Production callers retain the default five-second cap. Deterministic
971    /// test replicas may select shorter fixed delays while retaining every
972    /// production observation and terminal predicate.
973    #[doc(hidden)]
974    #[must_use]
975    pub fn with_observation_delay_bounds(mut self, initial: Duration, maximum: Duration) -> Self {
976        self.initial_observation_delay = initial.min(maximum);
977        self.maximum_observation_delay = maximum;
978        self
979    }
980
981    // The scope ends on every Result path before workflow can issue an effect.
982    // Action observations and subsequent retries therefore always read live state.
983    fn with_observation_snapshot<T>(
984        &mut self,
985        observe: impl FnOnce(&mut Self) -> Result<T, IcpEnsurePlatformError>,
986    ) -> Result<T, IcpEnsurePlatformError> {
987        self.observation_snapshot
988            .replace(Some(FleetObservationSnapshot::default()));
989        let result = observe(self);
990        self.observation_snapshot.take();
991        result
992    }
993
994    fn require_operator(&self) -> Result<(), IcpEnsurePlatformError> {
995        self.icp.bind_selected_identity()?;
996        let actual = self.icp.identity_principal_text()?;
997        if actual != self.desired.operator {
998            return Err(IcpEnsurePlatformError::OperatorMismatch {
999                actual,
1000                expected: self.desired.operator.clone(),
1001            });
1002        }
1003        Ok(())
1004    }
1005
1006    fn current_principal<'a>(
1007        &'a self,
1008        state: &'a FleetEnsureStateRecord,
1009        name: &str,
1010    ) -> Option<&'a str> {
1011        state
1012            .pending_principals
1013            .get(name)
1014            .or_else(|| state.principals.get(name))
1015            .map(String::as_str)
1016            .or_else(|| {
1017                self.desired
1018                    .canisters
1019                    .iter()
1020                    .find(|configured| configured.name == name)
1021                    .and_then(|configured| configured.principal.as_deref())
1022            })
1023    }
1024
1025    fn protocol_principals(&self, state: &FleetEnsureStateRecord) -> BTreeMap<String, String> {
1026        self.desired
1027            .canisters
1028            .iter()
1029            .filter_map(|configured| {
1030                state
1031                    .pending_principals
1032                    .get(&configured.name)
1033                    .or_else(|| state.principals.get(&configured.name))
1034                    .or(configured.principal.as_ref())
1035                    .map(|principal| (configured.name.clone(), principal.clone()))
1036            })
1037            .collect()
1038    }
1039
1040    fn current_protocol_owners_are_ready(
1041        &self,
1042        state: &FleetEnsureStateRecord,
1043    ) -> Result<bool, IcpEnsurePlatformError> {
1044        let mut owners = Vec::new();
1045        for configured in self.desired.canisters.iter().filter(|configured| {
1046            configured.presence == crate::fleet_ensure::model::DesiredPresence::Present
1047                && matches!(
1048                    configured.kind,
1049                    DesiredCanisterKind::Coordinator
1050                        | DesiredCanisterKind::Root
1051                        | DesiredCanisterKind::Store
1052                )
1053        }) {
1054            let live = self
1055                .current_principal(state, &configured.name)
1056                .map(|principal| self.status_optional(principal))
1057                .transpose()?
1058                .flatten();
1059            if live
1060                .as_ref()
1061                .is_some_and(|live| live.status != CanisterRuntimeStatus::Running)
1062            {
1063                return Ok(false);
1064            }
1065            owners.push((configured, live));
1066        }
1067        // Check running state and module identity from the same fresh status read.
1068        // Keep missing-owner/module decisions after the status scan so transport
1069        // failures retain their existing precedence.
1070        for (configured, live) in owners {
1071            let Some(live) = live else {
1072                return Ok(false);
1073            };
1074            let Some(wasm) = configured.wasm.as_deref() else {
1075                return Ok(false);
1076            };
1077            let expected = artifact_hash(&resolve_path(&self.root, wasm))?;
1078            if live.module_sha256.as_deref() != Some(expected.as_str()) {
1079                return Ok(false);
1080            }
1081        }
1082        Ok(true)
1083    }
1084
1085    fn observe_estate_funding_domains(
1086        &self,
1087        state: &FleetEnsureStateRecord,
1088    ) -> Result<BTreeMap<String, EstateFundingDomainObservation>, IcpEnsurePlatformError> {
1089        let Some(bootstrap) = self.desired.bootstrap.as_ref() else {
1090            return Ok(BTreeMap::new());
1091        };
1092        bootstrap
1093            .roots
1094            .iter()
1095            .map(|root| {
1096                let root_principal = self.current_principal(state, &root.root);
1097                let balance_cycles = root_principal
1098                    .map(|principal| self.cycles_ledger_balance(principal))
1099                    .transpose()?;
1100                let pool = root_principal
1101                    .map(|principal| {
1102                        self.observe_estate_pool_inventory(&root.root, principal, state)
1103                    })
1104                    .transpose()?
1105                    .flatten();
1106                Ok((
1107                    root.root.clone(),
1108                    EstateFundingDomainObservation {
1109                        balance_cycles,
1110                        cycles_ledger: self.desired.cycles_ledger.clone(),
1111                        pool,
1112                        root_principal: root_principal.map(str::to_string),
1113                    },
1114                ))
1115            })
1116            .collect()
1117    }
1118
1119    fn cycles_ledger_balance(&self, owner: &str) -> Result<u128, IcpEnsurePlatformError> {
1120        let balance: Nat = self.icp.canister_query_candid(
1121            &self.desired.cycles_ledger,
1122            "icrc1_balance_of",
1123            &CyclesLedgerAccount {
1124                owner: parse_principal("Cycles Ledger account owner", owner)?,
1125                subaccount: None,
1126            },
1127            None,
1128        )?;
1129        ledger_fee_cycles(balance)
1130    }
1131
1132    fn refresh_estate_funding_observation(
1133        &self,
1134        observation: &mut EffectObservation,
1135        state: &FleetEnsureStateRecord,
1136    ) -> Result<(), IcpEnsurePlatformError> {
1137        let Some(required) = observation.estate_funding_required.as_mut() else {
1138            return Ok(());
1139        };
1140        let root = required.root.to_text();
1141        let known_root = self.desired.canisters.iter().any(|canister| {
1142            canister.kind == DesiredCanisterKind::Root
1143                && self.current_principal(state, &canister.name) == Some(root.as_str())
1144        });
1145        if !known_root || required.cycles_ledger.to_text() != self.desired.cycles_ledger {
1146            return Err(current_protocol::CurrentProtocolError::ResponseMismatch.into());
1147        }
1148        // Root retains the balance from its last creation attempt until its next
1149        // retry. A completed host transfer can therefore precede this status.
1150        let available = self.cycles_ledger_balance(&root)?;
1151        let debit = required.required.to_u128();
1152        if available >= debit {
1153            observation.estate_funding_required = None;
1154        } else {
1155            required.available = Cycles::new(available);
1156            required.shortfall = Cycles::new(debit - available);
1157        }
1158        Ok(())
1159    }
1160
1161    fn require_current_pool_policy(
1162        &self,
1163        domains: &BTreeMap<String, EstateFundingDomainObservation>,
1164    ) -> Result<(), IcpEnsurePlatformError> {
1165        let Some(bootstrap) = self.desired.bootstrap.as_ref() else {
1166            return Ok(());
1167        };
1168        for root in &bootstrap.roots {
1169            let Some(pool) = domains
1170                .get(&root.root)
1171                .and_then(|domain| domain.pool.as_ref())
1172            else {
1173                continue;
1174            };
1175            if !pool_policy_is_current(pool, &root.limits.canister_pool) {
1176                return Err(IcpEnsurePlatformError::PoolPolicyReinstallReviewRequired {
1177                    root: root.root.clone(),
1178                });
1179            }
1180        }
1181        Ok(())
1182    }
1183
1184    fn observe_estate_pool_inventory(
1185        &self,
1186        root_name: &str,
1187        root: &str,
1188        _state: &FleetEnsureStateRecord,
1189    ) -> Result<Option<EstatePoolInventoryObservation>, IcpEnsurePlatformError> {
1190        if self.required_root_status(root_name, root)? != CanisterRuntimeStatus::Running {
1191            return Ok(None);
1192        }
1193        let candid = self.root_protocol_candid()?;
1194        let root_principal = parse_principal("Fleet Subnet Root", root)?;
1195        let mut start_after = None;
1196        let mut inventory = EstatePoolInventoryAccumulator::default();
1197        loop {
1198            let page = self.query_estate_pool_page(&candid, root_principal, start_after)?;
1199            let next = inventory.observe_page(root_name, page)?;
1200            if next.is_none() {
1201                break;
1202            }
1203            if next == start_after {
1204                return Err(pool_configuration_error(format!(
1205                    "Root {root_name} pool cursor did not advance"
1206                )));
1207            }
1208            start_after = next;
1209        }
1210        inventory.finish(root_name).map(Some)
1211    }
1212
1213    fn query_estate_pool_page(
1214        &self,
1215        candid: &Path,
1216        root: Principal,
1217        start_after: Option<Principal>,
1218    ) -> Result<CanisterPoolResponse, IcpEnsurePlatformError> {
1219        let key = (root, start_after);
1220        if let Some(page) = self
1221            .observation_snapshot
1222            .borrow()
1223            .as_ref()
1224            .and_then(|snapshot| snapshot.pool_pages.get(&key).cloned())
1225        {
1226            return Ok(page);
1227        }
1228        let response: RootPoolStatusResponse = query_with_candid(
1229            &self.icp,
1230            candid,
1231            root,
1232            canic_protocol::CANIC_ROOT_STATUS,
1233            &RootPoolStatusRequest::Pool(CanisterPoolStatusRequest {
1234                start_after,
1235                limit: 256,
1236            }),
1237        )
1238        .map_err(current_protocol::CurrentProtocolError::from)?;
1239        let RootPoolStatusResponse::Pool(page) = response;
1240        if let Some(snapshot) = self.observation_snapshot.borrow_mut().as_mut() {
1241            snapshot.pool_pages.insert(key, (*page).clone());
1242        }
1243        Ok(*page)
1244    }
1245
1246    #[expect(
1247        clippy::too_many_lines,
1248        reason = "one inspection binds each source asset to exact management authority and cycles"
1249    )]
1250    fn inspect_reinstall_assets(
1251        &self,
1252        state: &FleetEnsureStateRecord,
1253        authorities: &BTreeMap<String, RootManagementCanisterObservation>,
1254        observation: &mut FleetObservation,
1255        candid_by_root: &BTreeMap<String, PathBuf>,
1256        allow_pending_reset: bool,
1257    ) -> Result<Vec<crate::fleet_ensure::model::FleetReinstallAssetRecord>, IcpEnsurePlatformError>
1258    {
1259        let mut assets = Vec::new();
1260        let mut seen = BTreeSet::new();
1261        for (root_name, domain) in &observation.estate_funding_domains {
1262            let root = domain.root_principal.as_deref().ok_or_else(|| {
1263                IcpEnsurePlatformError::RootManagement("missing Root principal".to_string())
1264            })?;
1265            let pool = domain.pool.as_ref().ok_or_else(|| {
1266                IcpEnsurePlatformError::RootManagement("missing complete pool".to_string())
1267            })?;
1268            if pool.pending_creation.is_some() {
1269                return Err(IcpEnsurePlatformError::RootManagement(
1270                    "pool creation is still in flight".to_string(),
1271                ));
1272            }
1273            let root_authority = authorities.get(root_name).ok_or_else(|| {
1274                IcpEnsurePlatformError::RootManagement("missing Root binding".to_string())
1275            })?;
1276            for batch in pool
1277                .assets
1278                .chunks(super::bounded_observations::MAX_IN_FLIGHT)
1279            {
1280                // Record uniqueness in inventory order, then drain each issued batch
1281                // before mutating the observation or scheduling another inspection.
1282                let requests = batch
1283                    .iter()
1284                    .map(|asset| (asset, seen.insert(asset.principal.clone())))
1285                    .collect::<Vec<_>>();
1286                let icp = &self.icp;
1287                let responses =
1288                    super::bounded_observations::collect(&requests, |(asset, unique)| {
1289                        Self::inspect_reinstall_asset(
1290                            icp,
1291                            candid_by_root.get(root_name).map(PathBuf::as_path),
1292                            root,
1293                            asset,
1294                            *unique,
1295                            allow_pending_reset,
1296                        )
1297                    })?;
1298                for (asset, inspected) in batch.iter().zip(responses) {
1299                    let ReinstallAssetStatus {
1300                        response,
1301                        controllers,
1302                        cycles,
1303                    } = inspected;
1304
1305                    let module_sha256 = response
1306                        .module_hash
1307                        .as_ref()
1308                        .map(canic_core::cdk::utils::hash::hex_bytes);
1309                    let configured_name = self
1310                        .desired
1311                        .canisters
1312                        .iter()
1313                        .find(|c| {
1314                            c.principal.as_deref() == Some(&asset.principal)
1315                                || self.current_principal(state, &c.name) == Some(&asset.principal)
1316                        })
1317                        .map(|c| c.name.clone());
1318                    let name = configured_name
1319                        .unwrap_or_else(|| format!("reinstall-pool-{}", asset.principal));
1320                    observation.canisters.insert(
1321                        name,
1322                        Some(LiveCanister {
1323                            canister_version: None,
1324                            controllers: controllers.clone(),
1325                            cycles,
1326                            module_sha256: module_sha256.clone(),
1327                            principal: asset.principal.clone(),
1328                            reinstall_required: false,
1329                            root_owned_lifecycle: Some(match asset.lifecycle {
1330                                EstatePoolAssetLifecycle::Ready => RootOwnedCanisterLifecycle::Idle,
1331                                EstatePoolAssetLifecycle::Workload => {
1332                                    RootOwnedCanisterLifecycle::Workload
1333                                }
1334                                _ => RootOwnedCanisterLifecycle::Retained,
1335                            }),
1336                            status: match response.status {
1337                                canic_core::dto::canister::CanisterStatusType::Running => {
1338                                    CanisterRuntimeStatus::Running
1339                                }
1340                                canic_core::dto::canister::CanisterStatusType::Stopped => {
1341                                    CanisterRuntimeStatus::Stopped
1342                                }
1343                                canic_core::dto::canister::CanisterStatusType::Stopping => {
1344                                    CanisterRuntimeStatus::Stopping
1345                                }
1346                            },
1347                        }),
1348                    );
1349                    observation
1350                        .additional_controlled_cycles
1351                        .remove(&asset.principal);
1352                    assets.push(crate::fleet_ensure::model::FleetReinstallAssetRecord {
1353                        controllers,
1354                        module_sha256,
1355                        principal: asset.principal.clone(),
1356                        root: root_name.clone(),
1357                        subnet: root_authority.subnet.clone(),
1358                    });
1359                }
1360            }
1361        }
1362        assets.sort_by(|a, b| a.principal.cmp(&b.principal));
1363        Ok(assets)
1364    }
1365
1366    fn inspect_reinstall_asset(
1367        icp: &IcpCli,
1368        candid: Option<&Path>,
1369        root: &str,
1370        asset: &EstatePoolAssetObservation,
1371        unique: bool,
1372        allow_pending_reset: bool,
1373    ) -> Result<ReinstallAssetStatus, IcpEnsurePlatformError> {
1374        let terminal = matches!(
1375            asset.lifecycle,
1376            EstatePoolAssetLifecycle::Ready | EstatePoolAssetLifecycle::Workload
1377        );
1378        let recoverable = allow_pending_reset
1379            && matches!(
1380                asset.lifecycle,
1381                EstatePoolAssetLifecycle::PendingReset | EstatePoolAssetLifecycle::Failed
1382            );
1383        if !unique || !(terminal || recoverable) {
1384            return Err(IcpEnsurePlatformError::RootManagement(
1385                "pool asset is duplicated or not terminal".to_string(),
1386            ));
1387        }
1388        let authority = PoolInspectionAuthority {
1389            target: parse_principal("reset pool asset", &asset.principal)?,
1390            root: parse_principal("reset Root", root)?,
1391        };
1392        let candid = candid
1393            .ok_or_else(|| pool_configuration_error("missing reset read contract".to_string()))?;
1394        // Reset review always fetches fresh evidence; it does not consume the
1395        // earlier observation's balance or inspection cache.
1396        let response = Self::fetch_pool_inspection(icp, candid, authority)?;
1397        let mut controllers = response
1398            .settings
1399            .controllers
1400            .iter()
1401            .map(Principal::to_text)
1402            .collect::<Vec<_>>();
1403        if !controllers.iter().any(|controller| controller == root) {
1404            return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1405                canister: asset.principal.clone(),
1406                field: "Root controller",
1407            });
1408        }
1409        let cycles = validate_inspected_cycles(
1410            &asset.principal,
1411            InspectedModule::Any,
1412            response.module_hash.as_deref(),
1413            &response.cycles,
1414        )?;
1415        controllers.sort();
1416        if controllers != [root.to_string()] {
1417            return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1418                canister: asset.principal.clone(),
1419                field: "exact Root controllers",
1420            });
1421        }
1422        Ok(ReinstallAssetStatus {
1423            response,
1424            controllers,
1425            cycles,
1426        })
1427    }
1428
1429    fn resolved_controllers(
1430        &self,
1431        state: &FleetEnsureStateRecord,
1432        controllers: &[String],
1433        controller_canisters: &[String],
1434    ) -> Result<Vec<String>, IcpEnsurePlatformError> {
1435        let mut resolved = controllers.to_vec();
1436        for name in controller_canisters {
1437            resolved.push(
1438                self.current_principal(state, name)
1439                    .ok_or_else(|| IcpEnsurePlatformError::UnresolvedCreated(name.clone()))?
1440                    .to_string(),
1441            );
1442        }
1443        resolved.sort();
1444        resolved.dedup();
1445        Ok(resolved)
1446    }
1447
1448    fn observed_protocol_action(
1449        &self,
1450        step: &crate::fleet_ensure::model::DesiredProtocolStep,
1451        principal: String,
1452    ) -> Result<EnsureAction, IcpEnsurePlatformError> {
1453        Ok(EnsureAction::Protocol {
1454            candid: step.candid.clone(),
1455            candid_sha256: artifact_hash(&resolve_path(&self.root, &step.candid))?,
1456            command_args: step.command_args.clone(),
1457            command_args_sha256: artifact_hash(&resolve_path(&self.root, &step.command_args))?,
1458            command_method: step.command_method.clone(),
1459            expected_status: step.expected_status.clone(),
1460            expected_status_sha256: artifact_hash(&resolve_path(
1461                &self.root,
1462                &step.expected_status,
1463            ))?,
1464            maximum_execution_burn_cycles: step
1465                .maximum_execution_burn_cycles
1466                .parse()
1467                .map_err(|_| IcpEnsurePlatformError::Arithmetic("protocol burn"))?,
1468            name: step.name.clone(),
1469            principal,
1470            status_args: step.status_args.clone(),
1471            status_args_sha256: artifact_hash(&resolve_path(&self.root, &step.status_args))?,
1472            status_method: step.status_method.clone(),
1473        })
1474    }
1475
1476    fn action_principal<'a>(
1477        state: &'a FleetEnsureStateRecord,
1478        principal: &'a str,
1479    ) -> Result<&'a str, IcpEnsurePlatformError> {
1480        if let Some(name) = principal.strip_prefix("created:") {
1481            return state
1482                .pending_principals
1483                .get(name)
1484                .map(String::as_str)
1485                .ok_or_else(|| IcpEnsurePlatformError::UnresolvedCreated(name.to_string()));
1486        }
1487        Ok(principal)
1488    }
1489
1490    fn status_optional(
1491        &self,
1492        principal: &str,
1493    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1494        if let Some(cached) = self
1495            .observation_snapshot
1496            .borrow()
1497            .as_ref()
1498            .and_then(|snapshot| snapshot.statuses.get(principal).cloned())
1499        {
1500            return Ok(cached);
1501        }
1502        let observed = self.read_status_optional(principal)?;
1503        if let Some(snapshot) = self.observation_snapshot.borrow_mut().as_mut() {
1504            snapshot
1505                .statuses
1506                .insert(principal.to_string(), observed.clone());
1507        }
1508        Ok(observed)
1509    }
1510
1511    fn read_status_optional(
1512        &self,
1513        principal: &str,
1514    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1515        Self::read_status_with(&self.icp, principal)
1516    }
1517
1518    fn read_status_with(
1519        icp: &IcpCli,
1520        principal: &str,
1521    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1522        if icp.uses_direct_local_replica() {
1523            return Self::direct_local_status_optional(icp, principal);
1524        }
1525        let report = match icp.canister_status_report(principal) {
1526            Ok(report) => report,
1527            Err(error)
1528                if matches!(
1529                    error.diagnostic(),
1530                    Some(IcpDiagnostic::CanisterNotFound { .. })
1531                ) =>
1532            {
1533                return Ok(None);
1534            }
1535            Err(error) => return Err(error.into()),
1536        };
1537        if report.id != principal {
1538            return Err(IcpEnsurePlatformError::StatusIdentityMismatch {
1539                actual: report.id,
1540                expected: principal.to_string(),
1541            });
1542        }
1543        reject_public_status_projection(&report, principal)?;
1544        let status_text =
1545            report
1546                .status
1547                .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
1548                    canister: principal.to_string(),
1549                    field: "status",
1550                })?;
1551        let cycles_text =
1552            report
1553                .cycles
1554                .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
1555                    canister: principal.to_string(),
1556                    field: "cycles",
1557                })?;
1558        let cycles = parse_status_cycles(&cycles_text).ok_or_else(|| {
1559            IcpEnsurePlatformError::InvalidStatusCycles {
1560                canister: principal.to_string(),
1561                value: cycles_text,
1562            }
1563        })?;
1564        let mut controllers = report
1565            .settings
1566            .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
1567                canister: principal.to_string(),
1568                field: "controllers",
1569            })?
1570            .controllers;
1571        controllers.sort();
1572        let status = match status_text.to_ascii_lowercase().as_str() {
1573            "stopped" => CanisterRuntimeStatus::Stopped,
1574            "stopping" => CanisterRuntimeStatus::Stopping,
1575            _ => CanisterRuntimeStatus::Running,
1576        };
1577        Ok(Some(LiveCanister {
1578            canister_version: report.canister_version,
1579            controllers,
1580            cycles,
1581            module_sha256: report.module_hash.map(|hash| normalize_hash(&hash)),
1582            principal: principal.to_string(),
1583            reinstall_required: false,
1584            root_owned_lifecycle: None,
1585            status,
1586        }))
1587    }
1588
1589    fn direct_local_status_optional(
1590        icp: &IcpCli,
1591        principal: &str,
1592    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1593        let canister_id = parse_principal("local canister status target", principal)?;
1594        let response = icp
1595            .management_canister_status_candid::<_, ManagementCanisterObservationResponse>(
1596                canister_id,
1597                &ManagementCanisterStatusRequest { canister_id },
1598            );
1599        let response = match response {
1600            Ok(response) => response,
1601            Err(error)
1602                if matches!(
1603                    crate::icp::classify_icp_diagnostic(&error.to_string()),
1604                    Some(IcpDiagnostic::CanisterNotFound { .. })
1605                ) =>
1606            {
1607                return Ok(None);
1608            }
1609            Err(error) => {
1610                return Err(IcpEnsurePlatformError::LocalManagementStatus(Box::new(
1611                    error,
1612                )));
1613            }
1614        };
1615        let cycles_text = response.cycles.to_string();
1616        let cycles = u128::try_from(response.cycles.0).map_err(|_| {
1617            IcpEnsurePlatformError::InvalidStatusCycles {
1618                canister: principal.to_string(),
1619                value: cycles_text,
1620            }
1621        })?;
1622        let mut controllers = response
1623            .settings
1624            .controllers
1625            .into_iter()
1626            .map(|controller| controller.to_text())
1627            .collect::<Vec<_>>();
1628        controllers.sort();
1629        controllers.dedup();
1630        let status = match response.status {
1631            ManagementCanisterRuntimeStatus::Running => CanisterRuntimeStatus::Running,
1632            ManagementCanisterRuntimeStatus::Stopping => CanisterRuntimeStatus::Stopping,
1633            ManagementCanisterRuntimeStatus::Stopped => CanisterRuntimeStatus::Stopped,
1634        };
1635        Ok(Some(LiveCanister {
1636            canister_version: Some(response.version),
1637            controllers,
1638            cycles,
1639            module_sha256: response
1640                .module_hash
1641                .map(|hash| canic_core::cdk::utils::hash::hex_bytes(&hash)),
1642            principal: principal.to_string(),
1643            reinstall_required: false,
1644            root_owned_lifecycle: None,
1645            status,
1646        }))
1647    }
1648
1649    fn inspect_root_owned_canister(
1650        &self,
1651        configured: &crate::fleet_ensure::model::DesiredCanister,
1652        principal: &str,
1653        state: &FleetEnsureStateRecord,
1654    ) -> Result<Option<RootInspectionStatus>, IcpEnsurePlatformError> {
1655        self.root_owned_inspection_authority(configured, principal, state)?
1656            .map(|authority| self.read_pool_inspection(authority))
1657            .transpose()
1658    }
1659
1660    fn root_owned_inspection_authority(
1661        &self,
1662        configured: &crate::fleet_ensure::model::DesiredCanister,
1663        principal: &str,
1664        state: &FleetEnsureStateRecord,
1665    ) -> Result<Option<PoolInspectionAuthority>, IcpEnsurePlatformError> {
1666        let parent = configured.parent.as_deref().ok_or_else(|| {
1667            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1668                canister: configured.name.clone(),
1669                field: "Root parent",
1670            }
1671        })?;
1672        let root = self.current_principal(state, parent).ok_or_else(|| {
1673            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1674                canister: configured.name.clone(),
1675                field: "Root Principal",
1676            }
1677        })?;
1678        let root_configured = self
1679            .desired
1680            .canisters
1681            .iter()
1682            .find(|candidate| {
1683                candidate.name == parent && candidate.kind == DesiredCanisterKind::Root
1684            })
1685            .ok_or_else(
1686                || IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1687                    canister: configured.name.clone(),
1688                    field: "Root desired authority",
1689                },
1690            )?;
1691        let root_wasm = root_configured.wasm.as_deref().ok_or_else(|| {
1692            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1693                canister: configured.name.clone(),
1694                field: "Root artifact",
1695            }
1696        })?;
1697        let expected_root_module = artifact_hash(&resolve_path(&self.root, root_wasm))?;
1698        let root_live = self.status_optional(root)?.ok_or_else(|| {
1699            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1700                canister: configured.name.clone(),
1701                field: "live Root",
1702            }
1703        })?;
1704        let expected_root_controllers = self.resolved_controllers(
1705            state,
1706            &root_configured.controllers,
1707            &root_configured.controller_canisters,
1708        )?;
1709        if root_live.controllers != expected_root_controllers {
1710            return Err(
1711                IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1712                    canister: configured.name.clone(),
1713                    field: "Root controllers",
1714                },
1715            );
1716        }
1717        let Some(root_module) = root_live.module_sha256.as_deref() else {
1718            return Ok(None);
1719        };
1720        if root_module != expected_root_module {
1721            return Err(
1722                IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1723                    canister: configured.name.clone(),
1724                    field: "Root module SHA-256",
1725                },
1726            );
1727        }
1728        if root_live.status != CanisterRuntimeStatus::Running {
1729            return Ok(None);
1730        }
1731        self.require_operator()?;
1732        PoolInspectionAuthority::parse(root, principal).map(Some)
1733    }
1734
1735    fn created_canister_cycles(
1736        &self,
1737        name: &str,
1738        principal: &str,
1739        state: &FleetEnsureStateRecord,
1740    ) -> Result<(Option<u128>, bool), IcpEnsurePlatformError> {
1741        match self.status_optional(principal) {
1742            Ok(live) => return Ok((live.map(|live| live.cycles), false)),
1743            Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable { .. }) => {}
1744            Err(error) => return Err(error),
1745        }
1746        let configured = self
1747            .desired
1748            .canisters
1749            .iter()
1750            .find(|configured| configured.name == name)
1751            .ok_or_else(
1752                || IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1753                    canister: name.to_string(),
1754                    field: "desired canister",
1755                },
1756            )?;
1757        if configured.kind != DesiredCanisterKind::Pool {
1758            return Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable {
1759                canister: principal.to_string(),
1760            });
1761        }
1762        let Some(response) = self.inspect_root_owned_canister(configured, principal, state)? else {
1763            return Ok((None, true));
1764        };
1765        let parent = configured.parent.as_deref().ok_or_else(|| {
1766            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1767                canister: name.to_string(),
1768                field: "Root parent",
1769            }
1770        })?;
1771        let root = self.current_principal(state, parent).ok_or_else(|| {
1772            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1773                canister: name.to_string(),
1774                field: "Root Principal",
1775            }
1776        })?;
1777        let cycles = validate_root_controlled_inspection(
1778            name,
1779            InspectedModule::Empty,
1780            root,
1781            &response.settings.controllers,
1782            response.module_hash.as_deref(),
1783            &response.cycles,
1784        )?;
1785        Ok((Some(cycles), false))
1786    }
1787
1788    fn install_status_optional(
1789        &self,
1790        principal: &str,
1791    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1792        let Some(live) = self.status_optional(principal)? else {
1793            return Ok(None);
1794        };
1795        self.complete_install_status(principal, live).map(Some)
1796    }
1797
1798    fn complete_install_status(
1799        &self,
1800        principal: &str,
1801        mut live: LiveCanister,
1802    ) -> Result<LiveCanister, IcpEnsurePlatformError> {
1803        let exact = exact_install_canister_status(
1804            &self.icp,
1805            principal,
1806            live.canister_version,
1807            live.module_sha256.clone(),
1808        )?;
1809        live.canister_version = Some(exact.canister_version);
1810        live.module_sha256 = exact.module_sha256;
1811        Ok(live)
1812    }
1813
1814    fn observe_configured_canister(
1815        &self,
1816        configured: &crate::fleet_ensure::model::DesiredCanister,
1817        principal: &str,
1818        state: &FleetEnsureStateRecord,
1819    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1820        if configured.kind == DesiredCanisterKind::Pool {
1821            return self.observe_root_owned_canister(configured, principal, state);
1822        }
1823        let status = self.status_optional(principal);
1824        self.configured_status_or_root(configured, principal, state, status)
1825    }
1826
1827    fn configured_status_or_root(
1828        &self,
1829        configured: &crate::fleet_ensure::model::DesiredCanister,
1830        principal: &str,
1831        state: &FleetEnsureStateRecord,
1832        status: Result<Option<LiveCanister>, IcpEnsurePlatformError>,
1833    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1834        match status {
1835            Ok(observed) => Ok(observed),
1836            Err(error)
1837                if matches!(
1838                    configured.kind,
1839                    DesiredCanisterKind::Store | DesiredCanisterKind::Pool
1840                ) && !matches!(
1841                    error,
1842                    IcpEnsurePlatformError::Icp(ref source)
1843                        if matches!(
1844                            source.diagnostic(),
1845                            Some(IcpDiagnostic::CanisterNotFound { .. })
1846                        )
1847                ) =>
1848            {
1849                self.observe_root_owned_canister(configured, principal, state)
1850            }
1851            Err(error) => Err(error),
1852        }
1853    }
1854
1855    fn observe_root_owned_canister(
1856        &self,
1857        configured: &crate::fleet_ensure::model::DesiredCanister,
1858        principal: &str,
1859        state: &FleetEnsureStateRecord,
1860    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1861        match self.prepare_root_owned_observation(configured, principal, state)? {
1862            PreparedRootOwnedObservation::Observed(live) => Ok(live),
1863            PreparedRootOwnedObservation::Pending(pending) => {
1864                let response = self.read_pool_inspection(pending.inspection.authority)?;
1865                self.complete_pending_pool_observation(*pending, response)
1866            }
1867        }
1868    }
1869
1870    fn prepare_root_owned_observation<'a>(
1871        &'a self,
1872        configured: &'a crate::fleet_ensure::model::DesiredCanister,
1873        principal: &'a str,
1874        state: &'a FleetEnsureStateRecord,
1875    ) -> Result<PreparedRootOwnedObservation<'a>, IcpEnsurePlatformError> {
1876        let candid = self.root_protocol_candid()?;
1877        let parent = configured.parent.as_deref().ok_or_else(|| {
1878            current_protocol::CurrentProtocolError::Configuration(format!(
1879                "Root-owned canister {} has no Root parent",
1880                configured.name
1881            ))
1882        })?;
1883        let root = self.current_principal(state, parent).ok_or_else(|| {
1884            current_protocol::CurrentProtocolError::Configuration(format!(
1885                "Root-owned canister {} has no resolved Root",
1886                configured.name
1887            ))
1888        })?;
1889        if self.required_root_status(&configured.name, root)? == CanisterRuntimeStatus::Stopped {
1890            return self
1891                .retained_root_owned_observation(configured, principal, parent, root, state)
1892                .map(PreparedRootOwnedObservation::Observed);
1893        }
1894        let target = parse_principal("Root-owned canister", principal)?;
1895        let mut start_after = None;
1896        loop {
1897            let page = self.query_estate_pool_page(
1898                &candid,
1899                parse_principal("Fleet Subnet Root", root)?,
1900                start_after,
1901            )?;
1902            if let Some(asset) = page
1903                .entries
1904                .into_iter()
1905                .find(|asset| asset.canister_id == target)
1906            {
1907                if matches!(
1908                    asset.status,
1909                    canic_core::dto::pool::CanisterPoolAssetStatus::PendingReset
1910                ) && asset.cycles.to_u128() == 0
1911                {
1912                    let inspection =
1913                        self.prepare_pending_pool_inspection(configured, principal, state)?;
1914                    return Ok(PreparedRootOwnedObservation::Pending(Box::new(
1915                        PendingPoolObservation {
1916                            configured,
1917                            principal,
1918                            asset,
1919                            inspection,
1920                        },
1921                    )));
1922                }
1923                return Self::observed_root_owned_asset(configured, principal, root, asset)
1924                    .map(PreparedRootOwnedObservation::Observed);
1925            }
1926            let next = page.next_start_after;
1927            if next.is_none() {
1928                return Ok(PreparedRootOwnedObservation::Observed(None));
1929            }
1930            if next == start_after {
1931                return Err(
1932                    current_protocol::CurrentProtocolError::Configuration(format!(
1933                        "Root {parent} pool cursor did not advance"
1934                    ))
1935                    .into(),
1936                );
1937            }
1938            start_after = next;
1939        }
1940    }
1941
1942    fn complete_pending_pool_observation(
1943        &self,
1944        mut pending: PendingPoolObservation<'_>,
1945        response: RootInspectionStatus,
1946    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1947        let root = pending.inspection.root;
1948        let cycles = self.complete_pool_inspection(pending.inspection, response)?;
1949        pending.asset.cycles = Cycles::new(cycles);
1950        Self::observed_root_owned_asset(pending.configured, pending.principal, root, pending.asset)
1951    }
1952
1953    fn prepare_pending_pool_inspection<'a>(
1954        &'a self,
1955        configured: &'a crate::fleet_ensure::model::DesiredCanister,
1956        principal: &str,
1957        state: &'a FleetEnsureStateRecord,
1958    ) -> Result<PreparedPoolInspection<'a>, IcpEnsurePlatformError> {
1959        let parent = configured.parent.as_deref().ok_or_else(|| {
1960            IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1961                canister: configured.name.clone(),
1962                field: "Root parent",
1963            }
1964        })?;
1965        let root = self.current_principal(state, parent).ok_or_else(|| {
1966            IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1967                canister: configured.name.clone(),
1968                field: "Root Principal",
1969            }
1970        })?;
1971        let pending_fresh_pool = self
1972            .desired
1973            .bootstrap
1974            .as_ref()
1975            .is_some_and(|bootstrap| bootstrap.fresh_estate)
1976            && configured.principal.is_none()
1977            && state
1978                .pending_principals
1979                .get(&configured.name)
1980                .map(String::as_str)
1981                == Some(principal);
1982        if pending_fresh_pool {
1983            let authority = self
1984                .root_owned_inspection_authority(configured, principal, state)?
1985                .ok_or_else(
1986                    || IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1987                        canister: configured.name.clone(),
1988                        field: "installed Root",
1989                    },
1990                )?;
1991            return Ok(PreparedPoolInspection {
1992                authority,
1993                name: &configured.name,
1994                root,
1995                requirement: PoolInspectionRequirement::PendingFresh,
1996            });
1997        }
1998        self.prepare_pool_balance_inspection(
1999            &configured.name,
2000            root,
2001            principal,
2002            InspectedModule::Any,
2003        )
2004    }
2005
2006    fn inspect_pool_balance(
2007        &self,
2008        name: &str,
2009        root: &str,
2010        principal: &str,
2011        module: InspectedModule,
2012    ) -> Result<u128, IcpEnsurePlatformError> {
2013        let inspection = self.prepare_pool_balance_inspection(name, root, principal, module)?;
2014        let response = self.read_pool_inspection(inspection.authority)?;
2015        self.complete_pool_inspection(inspection, response)
2016    }
2017
2018    fn refresh_pool_balances(
2019        &self,
2020        root: &str,
2021        assets: &mut [EstatePoolAssetObservation],
2022    ) -> Result<(), IcpEnsurePlatformError> {
2023        let indices = assets
2024            .iter()
2025            .enumerate()
2026            .filter_map(|(index, asset)| {
2027                matches!(
2028                    asset.lifecycle,
2029                    EstatePoolAssetLifecycle::PendingReset | EstatePoolAssetLifecycle::Failed
2030                )
2031                .then_some(index)
2032            })
2033            .collect::<Vec<_>>();
2034        for batch in indices.chunks(super::bounded_observations::MAX_IN_FLIGHT) {
2035            let prepared = batch
2036                .iter()
2037                .map(|index| {
2038                    let target = &assets[*index].principal;
2039                    self.prepare_pool_balance_inspection(target, root, target, InspectedModule::Any)
2040                })
2041                .collect::<Vec<_>>();
2042            let authorities = prepared
2043                .iter()
2044                .filter_map(|result| result.as_ref().ok().map(|inspection| inspection.authority))
2045                .collect::<Vec<_>>();
2046            let mut responses = self.read_pool_inspection_batch(&authorities)?;
2047            // Drain issued reads, then validate in inventory order before publishing
2048            // this batch's balances. A failed batch never starts the next group.
2049            let balances = prepared
2050                .into_iter()
2051                .map(|inspection| {
2052                    let inspection = inspection?;
2053                    let response =
2054                        self.pool_inspection_batch_response(&mut responses, inspection.authority)?;
2055                    self.complete_pool_inspection(inspection, response)
2056                })
2057                .collect::<Result<Vec<_>, IcpEnsurePlatformError>>()?;
2058            for (index, cycles) in batch.iter().zip(balances) {
2059                assets[*index].cycles = cycles;
2060            }
2061        }
2062        Ok(())
2063    }
2064
2065    fn prepare_pool_balance_inspection<'a>(
2066        &self,
2067        name: &'a str,
2068        root: &'a str,
2069        principal: &str,
2070        module: InspectedModule,
2071    ) -> Result<PreparedPoolInspection<'a>, IcpEnsurePlatformError> {
2072        if self.required_root_status(name, root)? != CanisterRuntimeStatus::Running {
2073            return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
2074                canister: name.to_string(),
2075                field: "running Root",
2076            });
2077        }
2078        self.require_operator()?;
2079        Ok(PreparedPoolInspection {
2080            authority: PoolInspectionAuthority::parse(root, principal)?,
2081            name,
2082            root,
2083            requirement: PoolInspectionRequirement::RootControlled(module),
2084        })
2085    }
2086
2087    fn complete_pool_inspection(
2088        &self,
2089        inspection: PreparedPoolInspection<'_>,
2090        response: RootInspectionStatus,
2091    ) -> Result<u128, IcpEnsurePlatformError> {
2092        let cycles = match inspection.requirement {
2093            PoolInspectionRequirement::PendingFresh => validate_pending_fresh_pool_inspection(
2094                inspection.name,
2095                inspection.root,
2096                &self.desired.operator,
2097                &response.settings.controllers,
2098                response.module_hash.as_deref(),
2099                &response.cycles,
2100            ),
2101            PoolInspectionRequirement::RootControlled(module) => {
2102                validate_root_controlled_inspection(
2103                    inspection.name,
2104                    module,
2105                    inspection.root,
2106                    &response.settings.controllers,
2107                    response.module_hash.as_deref(),
2108                    &response.cycles,
2109                )
2110            }
2111        }?;
2112        self.retain_pool_inspection(inspection.authority, response);
2113        Ok(cycles)
2114    }
2115
2116    // Read raw evidence only after the caller has established its Root/operator authority.
2117    fn read_pool_inspection(
2118        &self,
2119        authority: PoolInspectionAuthority,
2120    ) -> Result<RootInspectionStatus, IcpEnsurePlatformError> {
2121        if let Some(response) = self
2122            .observation_snapshot
2123            .borrow()
2124            .as_ref()
2125            .and_then(|snapshot| snapshot.pool_inspections.get(&authority).cloned())
2126        {
2127            return Ok(response);
2128        }
2129        Self::fetch_pool_inspection(&self.icp, &self.root_protocol_candid()?, authority)
2130    }
2131
2132    fn fetch_pool_inspection(
2133        icp: &IcpCli,
2134        candid: &Path,
2135        authority: PoolInspectionAuthority,
2136    ) -> Result<RootInspectionStatus, IcpEnsurePlatformError> {
2137        crate::canister_protocol::inspection::preflight_inspection(
2138            icp,
2139            candid,
2140            authority.root,
2141            authority.target,
2142        )
2143        .map_err(current_protocol::CurrentProtocolError::from)?;
2144        let response: RootInspectionResponse = call_with_candid(
2145            icp,
2146            candid,
2147            authority.root,
2148            canic_protocol::CANIC_ROOT_COMMAND,
2149            &RootInspectionCommand::InspectCanister(CanisterInspectionRequest {
2150                canister_id: authority.target,
2151            }),
2152        )
2153        .map_err(current_protocol::CurrentProtocolError::from)?;
2154        response
2155            .into_status(authority.root, authority.target)
2156            .map_err(current_protocol::CurrentProtocolError::from)
2157            .map_err(Into::into)
2158    }
2159
2160    // Only a successfully validated consumer may retain a response in this observation.
2161    fn retain_pool_inspection(
2162        &self,
2163        authority: PoolInspectionAuthority,
2164        response: RootInspectionStatus,
2165    ) {
2166        if let Some(snapshot) = self.observation_snapshot.borrow_mut().as_mut() {
2167            snapshot.pool_inspections.insert(authority, response);
2168        }
2169    }
2170
2171    fn pool_funding_module(
2172        &self,
2173        name: &str,
2174        authority: &crate::fleet_ensure::model::PoolFundingAuthority,
2175        principal: &str,
2176    ) -> Result<InspectedModule, IcpEnsurePlatformError> {
2177        let candid = self.root_protocol_candid()?;
2178        let root = parse_principal("Fleet Subnet Root", &authority.root)?;
2179        let target = parse_principal("pool funding target", principal)?;
2180        let mut start_after = None;
2181        loop {
2182            let page = self.query_estate_pool_page(&candid, root, start_after)?;
2183            if let Some(asset) = page
2184                .entries
2185                .iter()
2186                .find(|asset| asset.canister_id == target)
2187            {
2188                return pool_funding_inspected_module(name, authority.lifecycle, &asset.status);
2189            }
2190            if page.next_start_after.is_none() || page.next_start_after == start_after {
2191                return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
2192                    canister: name.to_string(),
2193                    field: "reviewed pool membership",
2194                });
2195            }
2196            start_after = page.next_start_after;
2197        }
2198    }
2199
2200    fn observed_root_owned_asset(
2201        configured: &crate::fleet_ensure::model::DesiredCanister,
2202        principal: &str,
2203        root: &str,
2204        asset: CanisterPoolAsset,
2205    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
2206        let Some(root_owned_lifecycle) = root_owned_lifecycle(configured.kind, &asset.status)
2207        else {
2208            return Err(
2209                current_protocol::CurrentProtocolError::Configuration(format!(
2210                    "Root-owned canister {} has a live pool role incompatible with desired {:?}",
2211                    configured.name, configured.kind
2212                ))
2213                .into(),
2214            );
2215        };
2216        let status = match root_owned_lifecycle {
2217            RootOwnedCanisterLifecycle::Store | RootOwnedCanisterLifecycle::Workload => {
2218                CanisterRuntimeStatus::Running
2219            }
2220            RootOwnedCanisterLifecycle::Claimed
2221            | RootOwnedCanisterLifecycle::Idle
2222            | RootOwnedCanisterLifecycle::Reconciling
2223            | RootOwnedCanisterLifecycle::Retained => CanisterRuntimeStatus::Stopped,
2224        };
2225        Ok(Some(LiveCanister {
2226            canister_version: None,
2227            controllers: vec![root.to_string()],
2228            cycles: asset.cycles.to_u128(),
2229            module_sha256: None,
2230            principal: principal.to_string(),
2231            reinstall_required: false,
2232            root_owned_lifecycle: Some(root_owned_lifecycle),
2233            status,
2234        }))
2235    }
2236
2237    fn root_protocol_candid(&self) -> Result<PathBuf, IcpEnsurePlatformError> {
2238        let protocol = self.desired.protocol.as_ref().ok_or_else(|| {
2239            current_protocol::CurrentProtocolError::Configuration(
2240                "Root-owned observation requires typed Fleet protocol".to_string(),
2241            )
2242        })?;
2243        Ok(resolve_path(&self.root, &protocol.root_candid))
2244    }
2245
2246    fn required_root_status(
2247        &self,
2248        configured_name: &str,
2249        root: &str,
2250    ) -> Result<CanisterRuntimeStatus, IcpEnsurePlatformError> {
2251        self.status_optional(root)?
2252            .map(|live| live.status)
2253            .ok_or_else(|| {
2254                current_protocol::CurrentProtocolError::Configuration(format!(
2255                    "Root-owned canister {configured_name} has no live Root"
2256                ))
2257                .into()
2258            })
2259    }
2260
2261    fn retained_root_owned_observation(
2262        &self,
2263        configured: &crate::fleet_ensure::model::DesiredCanister,
2264        principal: &str,
2265        parent: &str,
2266        root: &str,
2267        state: &FleetEnsureStateRecord,
2268    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
2269        let Some(retained_topology) = exact_retained_root_owned_topology(
2270            state,
2271            &configured.name,
2272            configured.kind,
2273            parent,
2274            principal,
2275            root,
2276        ) else {
2277            return Err(
2278                current_protocol::CurrentProtocolError::Configuration(format!(
2279                    "Root-owned canister {} has no exact retained topology authority",
2280                    configured.name
2281                ))
2282                .into(),
2283            );
2284        };
2285        let cycles = self
2286            .controller_cycle_balance(principal)
2287            .or_else(|| state.retained_cycles_by_principal.get(principal).copied())
2288            .ok_or_else(|| {
2289                current_protocol::CurrentProtocolError::Configuration(format!(
2290                    "Root-owned canister {} has no current or retained exact native balance",
2291                    configured.name
2292                ))
2293            })?;
2294        if !matches!(
2295            configured.kind,
2296            DesiredCanisterKind::Pool | DesiredCanisterKind::Store
2297        ) || configured.presence != crate::fleet_ensure::model::DesiredPresence::Present
2298            || configured.replace
2299            || configured.drain.is_some()
2300            || (configured.kind == DesiredCanisterKind::Pool && configured.wasm.is_some())
2301            || (configured.kind == DesiredCanisterKind::Store && configured.wasm.is_none())
2302        {
2303            return Err(
2304                current_protocol::CurrentProtocolError::Configuration(format!(
2305                    "Root-owned recovery evidence for {} cannot authorize a mutation",
2306                    configured.name
2307                ))
2308                .into(),
2309            );
2310        }
2311        let root_config = self
2312            .desired
2313            .canisters
2314            .iter()
2315            .find(|canister| canister.name == parent && canister.kind == DesiredCanisterKind::Root)
2316            .ok_or_else(|| {
2317                current_protocol::CurrentProtocolError::Configuration(format!(
2318                    "Root-owned canister {} has no exact configured Root",
2319                    configured.name
2320                ))
2321            })?;
2322        let live_root = self.status_optional(root)?.ok_or_else(|| {
2323            current_protocol::CurrentProtocolError::Configuration(format!(
2324                "Root-owned canister {} has no live Root",
2325                configured.name
2326            ))
2327        })?;
2328        let mut expected_controllers = root_config.controllers.clone();
2329        expected_controllers.sort();
2330        if live_root.controllers != expected_controllers {
2331            return Err(
2332                current_protocol::CurrentProtocolError::Configuration(format!(
2333                    "Root-owned canister {} Root controllers drifted",
2334                    configured.name
2335                ))
2336                .into(),
2337            );
2338        }
2339        Ok(Some(LiveCanister {
2340            canister_version: None,
2341            controllers: vec![root.to_string()],
2342            cycles,
2343            module_sha256: retained_topology.module_hash.clone(),
2344            principal: principal.to_string(),
2345            reinstall_required: false,
2346            root_owned_lifecycle: Some(RootOwnedCanisterLifecycle::Retained),
2347            status: if configured.kind == DesiredCanisterKind::Store {
2348                CanisterRuntimeStatus::Running
2349            } else {
2350                CanisterRuntimeStatus::Stopped
2351            },
2352        }))
2353    }
2354
2355    fn completed_reinstall_is_current(
2356        &self,
2357        state: &FleetEnsureStateRecord,
2358        name: &str,
2359        live: &LiveCanister,
2360    ) -> Result<bool, IcpEnsurePlatformError> {
2361        if state.completed_reinstall_operation_id.is_none()
2362            || !state.completed_reinstall_action_sha256.contains_key(name)
2363        {
2364            return Ok(false);
2365        }
2366        let Some(pre_canister_version) = state.completed_reinstalls.get(name) else {
2367            return Ok(false);
2368        };
2369        let Some(configured) = self
2370            .desired
2371            .canisters
2372            .iter()
2373            .find(|configured| configured.name == name)
2374        else {
2375            return Ok(false);
2376        };
2377        let Some(wasm) = configured.wasm.as_deref() else {
2378            return Ok(false);
2379        };
2380        let principal_matches = state
2381            .principals
2382            .get(name)
2383            .is_some_and(|principal| principal == &live.principal);
2384        let retained_topology = state.topology.get(name);
2385        let topology_matches = retained_topology.is_some_and(|topology| {
2386            topology.kind == configured.kind && topology.parent == configured.parent
2387        });
2388        let desired_module_sha256 = artifact_hash(&resolve_path(&self.root, wasm))?;
2389        let root_owned_store_module = (live.module_sha256.is_none()
2390            && live.root_owned_lifecycle == Some(RootOwnedCanisterLifecycle::Store))
2391        .then(|| {
2392            configured
2393                .parent
2394                .as_deref()
2395                .and_then(|parent| state.principals.get(parent))
2396                .filter(|root| live.controllers.as_slice() == [root.as_str()])
2397                .and(retained_topology)
2398                .and_then(|topology| topology.module_hash.as_deref())
2399        })
2400        .flatten();
2401        completed_reinstall_continuity(
2402            *pre_canister_version,
2403            principal_matches,
2404            topology_matches,
2405            live.module_sha256.as_deref().or(root_owned_store_module),
2406            &desired_module_sha256,
2407            live.canister_version,
2408        )
2409        .map_err(
2410            |field| IcpEnsurePlatformError::CompletedReinstallAuthorityConflict {
2411                canister: name.to_string(),
2412                field,
2413            },
2414        )?;
2415        Ok(true)
2416    }
2417
2418    fn controller_cycle_balance(&self, principal: &str) -> Option<u128> {
2419        let response: Result<ManagedCanisterStatusResponse, canic_core::dto::error::Error> = self
2420            .icp
2421            .canister_query_candid(
2422                principal,
2423                canic_protocol::CANIC_OBSERVABILITY,
2424                &ManagedCanisterStatusRequest::CycleBalance,
2425                None,
2426            )
2427            .ok()?;
2428        let ManagedCanisterStatusResponse::CycleBalance(balance) = response.ok()?;
2429        Some(balance.cycles)
2430    }
2431
2432    fn apply_create(
2433        &self,
2434        authority: CreateCanisterAuthority<'_>,
2435        state: &FleetEnsureStateRecord,
2436    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2437        let creation_fee = self
2438            .desired
2439            .management_creation_fee_cycles
2440            .parse::<Cycles>()
2441            .map(|cycles| cycles.to_u128())
2442            .map_err(|_| IcpEnsurePlatformError::Arithmetic("management creation fee"))?;
2443        let amount = authority
2444            .requested_initial_cycles
2445            .checked_add(creation_fee)
2446            .ok_or(IcpEnsurePlatformError::Arithmetic("creation amount"))?;
2447        let controllers = self
2448            .resolved_controllers(state, authority.controllers, authority.controller_canisters)?
2449            .iter()
2450            .map(|value| parse_principal("controller", value))
2451            .collect::<Result<Vec<_>, _>>()?;
2452        let request = CreateCanisterArgs {
2453            amount: Nat::from(amount),
2454            created_at_time: Some(authority.created_at_time),
2455            creation_args: Some(CmcCreateCanisterArgs {
2456                settings: Some(CanisterSettings {
2457                    compute_allocation: None,
2458                    controllers: Some(controllers),
2459                    freezing_threshold: None,
2460                    memory_allocation: None,
2461                    reserved_cycles_limit: None,
2462                }),
2463                subnet_selection: Some(SubnetSelection::Subnet {
2464                    subnet: parse_principal("subnet", authority.subnet)?,
2465                }),
2466            }),
2467            from_subaccount: None,
2468        };
2469        let response: Result<CreateCanisterSuccess, CreateCanisterError> = self
2470            .icp
2471            .canister_call_candid(authority.ledger, "create_canister", &request, None)?;
2472        create_canister_response_outcome(response, authority.requested_initial_cycles)
2473    }
2474
2475    fn apply_fund(
2476        &self,
2477        amount: u128,
2478        created_at_time: u64,
2479        ledger: &str,
2480        principal: &str,
2481    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2482        let request = WithdrawArgs {
2483            amount: Nat::from(amount),
2484            created_at_time: Some(created_at_time),
2485            from_subaccount: None,
2486            to: parse_principal("funding target", principal)?,
2487        };
2488        let response: Result<Nat, WithdrawError> = self
2489            .icp
2490            .canister_call_candid(ledger, "withdraw", &request, None)?;
2491        match response {
2492            Ok(block) => Ok(EffectOutcome {
2493                created_principal: None,
2494                post_cycles: None,
2495                receipt: Some(block.to_string()),
2496            }),
2497            Err(WithdrawError::Duplicate { duplicate_of }) => Ok(EffectOutcome {
2498                created_principal: None,
2499                post_cycles: None,
2500                receipt: Some(duplicate_of.to_string()),
2501            }),
2502            Err(error) => Err(IcpEnsurePlatformError::LedgerWithdraw(
2503                render_withdraw_error(error),
2504            )),
2505        }
2506    }
2507
2508    fn apply_estate_fund(
2509        &self,
2510        amount: u128,
2511        created_at_time: u64,
2512        ledger: &str,
2513        principal: &str,
2514        reviewed_fee_cycles: u128,
2515    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2516        let request = CyclesLedgerTransferArgs {
2517            amount: Nat::from(amount),
2518            created_at_time: Some(created_at_time),
2519            fee: Some(Nat::from(reviewed_fee_cycles)),
2520            from_subaccount: None,
2521            memo: None,
2522            to: CyclesLedgerAccount {
2523                owner: parse_principal("estate funding target", principal)?,
2524                subaccount: None,
2525            },
2526        };
2527        let response: Result<Nat, CyclesLedgerTransferError> =
2528            self.icp
2529                .canister_call_candid(ledger, "icrc1_transfer", &request, None)?;
2530        match response {
2531            Ok(block) => Ok(EffectOutcome {
2532                created_principal: None,
2533                post_cycles: None,
2534                receipt: Some(block.to_string()),
2535            }),
2536            Err(CyclesLedgerTransferError::Duplicate { duplicate_of }) => Ok(EffectOutcome {
2537                created_principal: None,
2538                post_cycles: None,
2539                receipt: Some(duplicate_of.to_string()),
2540            }),
2541            Err(CyclesLedgerTransferError::BadFee { expected_fee }) => {
2542                Err(IcpEnsurePlatformError::LedgerTransferFeeChanged {
2543                    expected_fee_cycles: ledger_fee_cycles(expected_fee)?,
2544                    reviewed_fee_cycles,
2545                })
2546            }
2547            Err(error) => Err(IcpEnsurePlatformError::LedgerTransfer(
2548                render_ledger_transfer_error(error),
2549            )),
2550        }
2551    }
2552
2553    #[expect(
2554        clippy::too_many_arguments,
2555        reason = "the install boundary verifies one complete immutable action tuple"
2556    )]
2557    fn apply_install(
2558        &self,
2559        operation_id: &str,
2560        principals: &BTreeMap<String, String>,
2561        canic_init: Option<&crate::fleet_ensure::model::DesiredCanisterInit>,
2562        init_arg: Option<&str>,
2563        init_arg_sha256: Option<&str>,
2564        init_candid: Option<&str>,
2565        init_candid_sha256: Option<&str>,
2566        mode: InstallMode,
2567        principal: &str,
2568        wasm: &str,
2569        wasm_sha256: &str,
2570    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2571        verify_artifact(&resolve_path(&self.root, wasm), wasm_sha256, "Wasm")?;
2572        let mut command = self.icp.canister_command();
2573        command.args(["install", principal, "--mode"]);
2574        command.arg(match mode {
2575            InstallMode::Install => "install",
2576            InstallMode::Reinstall => "reinstall",
2577        });
2578        command.args(["--yes", "--wasm"]);
2579        command.arg(resolve_path(&self.root, wasm));
2580        let generated_init = if let Some(canic_init) = canic_init {
2581            let path = canic_init::write_arguments(canic_init::CanicInitRequest {
2582                desired: &self.desired,
2583                init: canic_init,
2584                operation_id,
2585                principals,
2586                root: &self.root,
2587                wasm,
2588                wasm_sha256,
2589            })?;
2590            command.arg("--args-file");
2591            command.arg(&path);
2592            command.args(["--args-format", "bin"]);
2593            Some(path)
2594        } else if let Some(init_arg) = init_arg {
2595            let expected =
2596                init_arg_sha256.ok_or_else(|| IcpEnsurePlatformError::ArtifactDigestMismatch {
2597                    actual: "missing".to_string(),
2598                    expected: "reviewed init argument identity".to_string(),
2599                    kind: "init argument",
2600                })?;
2601            let init_candid =
2602                init_candid.ok_or_else(|| IcpEnsurePlatformError::ArtifactDigestMismatch {
2603                    actual: "missing".to_string(),
2604                    expected: "reviewed init Candid".to_string(),
2605                    kind: "init Candid",
2606                })?;
2607            let init_candid_sha256 = init_candid_sha256.ok_or_else(|| {
2608                IcpEnsurePlatformError::ArtifactDigestMismatch {
2609                    actual: "missing".to_string(),
2610                    expected: "reviewed init Candid identity".to_string(),
2611                    kind: "init Candid",
2612                }
2613            })?;
2614            let path = protocol::write_init_arguments(
2615                &self.root,
2616                operation_id,
2617                principals,
2618                init_candid,
2619                init_candid_sha256,
2620                init_arg,
2621                expected,
2622            )?;
2623            command.arg("--args-file");
2624            command.arg(&path);
2625            command.args(["--args-format", "bin"]);
2626            Some(path)
2627        } else {
2628            command.args(["--args", "()"]);
2629            None
2630        };
2631        self.icp.add_target_args(&mut command);
2632        let result = run_status(&mut command).map_err(IcpEnsurePlatformError::from);
2633        if let Some(path) = generated_init {
2634            std::fs::remove_file(&path)
2635                .map_err(|_| IcpEnsurePlatformError::ArtifactUnavailable(path))?;
2636        }
2637        result?;
2638        Ok(empty_outcome())
2639    }
2640
2641    fn apply_controllers(
2642        &self,
2643        principal: &str,
2644        controllers: &[String],
2645    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2646        let mut command = self.icp.canister_command();
2647        command.args([
2648            "settings",
2649            "update",
2650            principal,
2651            "--force",
2652            "--remove-all-controllers",
2653        ]);
2654        for controller in controllers {
2655            parse_principal("controller", controller)?;
2656            command.args(["--add-controller", controller]);
2657        }
2658        self.icp.add_target_args(&mut command);
2659        run_status(&mut command)?;
2660        Ok(empty_outcome())
2661    }
2662
2663    fn observe_fleet_snapshot(
2664        &mut self,
2665        operation_id: &str,
2666        state: &FleetEnsureStateRecord,
2667    ) -> Result<FleetObservation, IcpEnsurePlatformError> {
2668        self.require_operator()?;
2669        let mut canisters = self
2670            .timed_observation(FleetObservationStage::ConfiguredCanisters, |platform| {
2671                platform.observe_configured_canisters(state)
2672            })?;
2673        self.reconcile_completed_reinstalls(state, &mut canisters)?;
2674        let mut estate_funding_domains = self
2675            .timed_observation(FleetObservationStage::EstateFunding, |platform| {
2676                platform.observe_estate_funding_domains(state)
2677            })?;
2678        self.timed_observation(FleetObservationStage::PoolBalances, |platform| {
2679            for domain in estate_funding_domains.values_mut() {
2680                if let (Some(root), Some(pool)) = (&domain.root_principal, &mut domain.pool) {
2681                    platform.refresh_pool_balances(root, &mut pool.assets)?;
2682                    for asset in &mut pool.assets {
2683                        if matches!(
2684                            asset.lifecycle,
2685                            EstatePoolAssetLifecycle::PendingReset
2686                                | EstatePoolAssetLifecycle::Failed
2687                        ) {
2688                            for live in canisters.values_mut().filter_map(Option::as_mut) {
2689                                if live.principal == asset.principal {
2690                                    live.cycles = asset.cycles;
2691                                }
2692                            }
2693                        }
2694                    }
2695                }
2696            }
2697            Ok(())
2698        })?;
2699        self.estate_observations.clone_from(&estate_funding_domains);
2700        self.require_current_pool_policy(&estate_funding_domains)?;
2701        self.reconcile_completed_reinstalls(state, &mut canisters)?;
2702        let additional_controlled_cycles =
2703            Self::additional_pool_cycles(&canisters, &estate_funding_domains)?;
2704        let protocol_ready = self
2705            .timed_observation(FleetObservationStage::ProtocolReadiness, |platform| {
2706                platform.observe_protocol_readiness(operation_id, state, &canisters)
2707            })?;
2708        let ledger_fee_cycles =
2709            self.timed_observation(FleetObservationStage::LedgerFee, |platform| {
2710                ledger_fee_cycles(platform.icp.canister_query_candid(
2711                    &platform.desired.cycles_ledger,
2712                    "icrc1_fee",
2713                    &(),
2714                    None,
2715                )?)
2716            })?;
2717        let operator_cycles =
2718            self.timed_observation(FleetObservationStage::OperatorBalance, |platform| {
2719                platform
2720                    .icp
2721                    .identity_cycles_balance()
2722                    .map_err(|error| IcpEnsurePlatformError::LedgerWithdraw(error.to_string()))
2723            })?;
2724        Ok(FleetObservation {
2725            additional_controlled_cycles,
2726            canisters,
2727            estate_funding_domains,
2728            ledger_fee_cycles,
2729            operator_cycles,
2730            protocol_ready,
2731        })
2732    }
2733
2734    fn observe_configured_canisters(
2735        &self,
2736        state: &FleetEnsureStateRecord,
2737    ) -> Result<BTreeMap<String, Option<LiveCanister>>, IcpEnsurePlatformError> {
2738        let configured = &self.desired.canisters;
2739        let mut observed = BTreeMap::new();
2740        let mut cursor = 0;
2741        while cursor < configured.len() {
2742            let pool_batch = configured[cursor].kind == DesiredCanisterKind::Pool;
2743            let end = configured[cursor..]
2744                .iter()
2745                .take(super::bounded_observations::MAX_IN_FLIGHT)
2746                .take_while(|canister| (canister.kind == DesiredCanisterKind::Pool) == pool_batch)
2747                .count()
2748                + cursor;
2749            if pool_batch {
2750                self.observe_configured_pool_batch(&configured[cursor..end], state, &mut observed)?;
2751                cursor = end;
2752                continue;
2753            }
2754            self.observe_configured_status_batch(&configured[cursor..end], state, &mut observed)?;
2755            cursor = end;
2756        }
2757        Ok(observed)
2758    }
2759
2760    fn observe_configured_pool_batch(
2761        &self,
2762        configured: &[crate::fleet_ensure::model::DesiredCanister],
2763        state: &FleetEnsureStateRecord,
2764        observed: &mut BTreeMap<String, Option<LiveCanister>>,
2765    ) -> Result<(), IcpEnsurePlatformError> {
2766        // Preparation and result consumption follow desired order. An earlier transport
2767        // failure must still precede a later preparation failure, regardless of arrival.
2768        let prepared = configured
2769            .iter()
2770            .map(|canister| {
2771                self.current_principal(state, &canister.name).map_or(
2772                    Ok(PreparedRootOwnedObservation::Observed(None)),
2773                    |principal| self.prepare_root_owned_observation(canister, principal, state),
2774                )
2775            })
2776            .collect::<Vec<_>>();
2777        let authorities = prepared
2778            .iter()
2779            .filter_map(|result| match result {
2780                Ok(PreparedRootOwnedObservation::Pending(pending)) => {
2781                    Some(pending.inspection.authority)
2782                }
2783                _ => None,
2784            })
2785            .collect::<Vec<_>>();
2786        let mut responses = self.read_pool_inspection_batch(&authorities)?;
2787        for (canister, prepared) in configured.iter().zip(prepared) {
2788            let live = match prepared? {
2789                PreparedRootOwnedObservation::Observed(live) => live,
2790                PreparedRootOwnedObservation::Pending(pending) => {
2791                    let response = self.pool_inspection_batch_response(
2792                        &mut responses,
2793                        pending.inspection.authority,
2794                    )?;
2795                    self.complete_pending_pool_observation(*pending, response)?
2796                }
2797            };
2798            observed.insert(canister.name.clone(), live);
2799        }
2800        Ok(())
2801    }
2802
2803    fn read_pool_inspection_batch(
2804        &self,
2805        authorities: &[PoolInspectionAuthority],
2806    ) -> Result<PoolInspectionBatch, IcpEnsurePlatformError> {
2807        let mut seen = BTreeSet::new();
2808        let requests = authorities
2809            .iter()
2810            .copied()
2811            .filter(|authority| {
2812                seen.insert(*authority)
2813                    && !self
2814                        .observation_snapshot
2815                        .borrow()
2816                        .as_ref()
2817                        .is_some_and(|snapshot| snapshot.pool_inspections.contains_key(authority))
2818            })
2819            .collect::<Vec<_>>();
2820        if requests.is_empty() {
2821            return Ok(PoolInspectionBatch::default());
2822        }
2823        let candid = self.root_protocol_candid()?;
2824        let icp = &self.icp;
2825        Ok(PoolInspectionBatch {
2826            responses: super::bounded_observations::collect(&requests, |authority| {
2827                Ok::<_, std::convert::Infallible>((
2828                    *authority,
2829                    Self::fetch_pool_inspection(icp, &candid, *authority),
2830                ))
2831            })
2832            .expect("inspection results are retained for ordered authority decisions")
2833            .into_iter()
2834            .collect(),
2835        })
2836    }
2837
2838    fn pool_inspection_batch_response(
2839        &self,
2840        batch: &mut PoolInspectionBatch,
2841        authority: PoolInspectionAuthority,
2842    ) -> Result<RootInspectionStatus, IcpEnsurePlatformError> {
2843        match batch.responses.get(&authority) {
2844            Some(Ok(response)) => Ok(response.clone()),
2845            Some(Err(_)) => batch
2846                .responses
2847                .remove(&authority)
2848                .expect("observed batch failure"),
2849            None => self.read_pool_inspection(authority),
2850        }
2851    }
2852
2853    fn observe_configured_status_batch(
2854        &self,
2855        configured: &[crate::fleet_ensure::model::DesiredCanister],
2856        state: &FleetEnsureStateRecord,
2857        observed: &mut BTreeMap<String, Option<LiveCanister>>,
2858    ) -> Result<(), IcpEnsurePlatformError> {
2859        let mut seen = BTreeSet::new();
2860        let requests = configured
2861            .iter()
2862            .filter_map(|canister| self.current_principal(state, &canister.name))
2863            .filter(|principal| seen.insert(*principal))
2864            .filter(|principal| matches!(self.cached_status(principal), StatusCacheEntry::Miss))
2865            .collect::<Vec<_>>();
2866        let icp = &self.icp;
2867        let mut results = super::bounded_observations::collect(&requests, |principal| {
2868            Ok::<_, std::convert::Infallible>((*principal, Self::read_status_with(icp, principal)))
2869        })
2870        .expect("read outcomes are retained for ordered authority decisions")
2871        .into_iter()
2872        .collect::<BTreeMap<_, _>>();
2873        for canister in configured {
2874            let Some(principal) = self.current_principal(state, &canister.name) else {
2875                observed.insert(canister.name.clone(), None);
2876                continue;
2877            };
2878            let status = if let StatusCacheEntry::Hit(cached) = self.cached_status(principal) {
2879                Ok(cached)
2880            } else {
2881                results
2882                    .remove(principal)
2883                    .unwrap_or_else(|| self.status_optional(principal))
2884            };
2885            if let Ok(live) = &status
2886                && let Some(snapshot) = self.observation_snapshot.borrow_mut().as_mut()
2887            {
2888                snapshot
2889                    .statuses
2890                    .insert(principal.to_string(), live.clone());
2891            }
2892            let live = self.configured_status_or_root(canister, principal, state, status)?;
2893            observed.insert(canister.name.clone(), live);
2894        }
2895        Ok(())
2896    }
2897
2898    fn cached_status(&self, principal: &str) -> StatusCacheEntry {
2899        self.observation_snapshot
2900            .borrow()
2901            .as_ref()
2902            .and_then(|snapshot| snapshot.statuses.get(principal).cloned())
2903            .map_or(StatusCacheEntry::Miss, StatusCacheEntry::Hit)
2904    }
2905
2906    fn reconcile_completed_reinstalls(
2907        &self,
2908        state: &FleetEnsureStateRecord,
2909        canisters: &mut BTreeMap<String, Option<LiveCanister>>,
2910    ) -> Result<(), IcpEnsurePlatformError> {
2911        for (name, live) in canisters.iter_mut() {
2912            let Some(live) = live.as_mut() else {
2913                continue;
2914            };
2915            let completed = self.completed_reinstall_is_current(state, name, live)?;
2916            let store_module_unobserved = live.module_sha256.is_none()
2917                && live.root_owned_lifecycle == Some(RootOwnedCanisterLifecycle::Store);
2918            if completed && store_module_unobserved {
2919                live.module_sha256 = state
2920                    .topology
2921                    .get(name)
2922                    .and_then(|topology| topology.module_hash.clone());
2923            }
2924        }
2925        Ok(())
2926    }
2927
2928    fn additional_pool_cycles(
2929        canisters: &BTreeMap<String, Option<LiveCanister>>,
2930        domains: &BTreeMap<String, EstateFundingDomainObservation>,
2931    ) -> Result<BTreeMap<String, u128>, IcpEnsurePlatformError> {
2932        let configured = canisters
2933            .values()
2934            .filter_map(|live| live.as_ref().map(|live| live.principal.as_str()))
2935            .collect::<BTreeSet<_>>();
2936        let mut additional = BTreeMap::new();
2937        for asset in domains
2938            .values()
2939            .filter_map(|domain| domain.pool.as_ref())
2940            .flat_map(|pool| &pool.assets)
2941        {
2942            if configured.contains(asset.principal.as_str()) {
2943                continue;
2944            }
2945            if additional
2946                .insert(asset.principal.clone(), asset.cycles)
2947                .is_some()
2948            {
2949                return Err(pool_configuration_error(format!(
2950                    "controlled pool canister {} is retained by more than one Root",
2951                    asset.principal
2952                )));
2953            }
2954        }
2955        Ok(additional)
2956    }
2957
2958    fn management_roots_are_current(
2959        &self,
2960        roots: &[(&crate::fleet_ensure::model::DesiredCanister, LiveCanister)],
2961        reviewed_targets: &BTreeSet<String>,
2962    ) -> Result<bool, IcpEnsurePlatformError> {
2963        let mut all_current = true;
2964        for (configured, live) in roots {
2965            if live.status != CanisterRuntimeStatus::Running || !reviewed_targets.is_empty() {
2966                all_current = false;
2967                continue;
2968            }
2969            if live.module_sha256.is_none()
2970                && is_unallocated_fresh_root(&self.desired, configured, reviewed_targets)
2971            {
2972                continue;
2973            }
2974            if let Some(wasm) = &configured.wasm {
2975                let expected = artifact_hash(&resolve_path(&self.root, wasm))?;
2976                if live.module_sha256.as_deref() != Some(expected.as_str()) {
2977                    all_current = false;
2978                }
2979            }
2980        }
2981        Ok(all_current)
2982    }
2983
2984    fn observe_protocol_readiness(
2985        &self,
2986        operation_id: &str,
2987        state: &FleetEnsureStateRecord,
2988        canisters: &BTreeMap<String, Option<LiveCanister>>,
2989    ) -> Result<BTreeMap<String, bool>, IcpEnsurePlatformError> {
2990        let principals = self.protocol_principals(state);
2991        self.desired
2992            .protocol_steps
2993            .iter()
2994            .map(|step| {
2995                let Some(principal) = principals.get(&step.canister) else {
2996                    return Ok((step.name.clone(), false));
2997                };
2998                let Some(live) = canisters.get(&step.canister).and_then(Option::as_ref) else {
2999                    return Ok((step.name.clone(), false));
3000                };
3001                let configured = self
3002                    .desired
3003                    .canisters
3004                    .iter()
3005                    .find(|configured| configured.name == step.canister)
3006                    .expect("protocol target was validated against desired canisters");
3007                if live.reinstall_required {
3008                    return Ok((step.name.clone(), false));
3009                }
3010                if let Some(wasm) = &configured.wasm {
3011                    let desired_hash = artifact_hash(&resolve_path(&self.root, wasm))?;
3012                    if live.module_sha256.as_deref() != Some(desired_hash.as_str()) {
3013                        return Ok((step.name.clone(), false));
3014                    }
3015                }
3016                let action = self.observed_protocol_action(step, principal.clone())?;
3017                protocol::observe(&self.icp, &self.root, operation_id, &principals, &action)
3018                    .map(|observation| (step.name.clone(), observation.applied))
3019                    .map_err(IcpEnsurePlatformError::from)
3020            })
3021            .collect()
3022    }
3023
3024    #[expect(
3025        clippy::too_many_arguments,
3026        reason = "one typed transfer effect carries the complete reviewed authority tuple"
3027    )]
3028    fn apply_transfer(
3029        &self,
3030        amount: u128,
3031        candid: &str,
3032        candid_sha256: &str,
3033        destination: &str,
3034        method: &str,
3035        operation_id: &str,
3036        principal: &str,
3037    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
3038        let candid = resolve_path(&self.root, candid);
3039        if !candid.is_file() {
3040            return Err(IcpEnsurePlatformError::CandidUnavailable(candid));
3041        }
3042        verify_artifact(&candid, candid_sha256, "drain Candid")?;
3043        let response: DrainResponse = self.icp.canister_call_candid(
3044            principal,
3045            method,
3046            &DrainRequest {
3047                operation_id: operation_id.to_string(),
3048                destination: parse_principal("treasury", destination)?,
3049                cycles: Nat::from(amount),
3050            },
3051            Some(&candid),
3052        )?;
3053        let transferred = match response {
3054            DrainResponse::Accepted { transferred_cycles }
3055            | DrainResponse::Replayed { transferred_cycles } => transferred_cycles,
3056        };
3057        if transferred != amount {
3058            return Err(IcpEnsurePlatformError::WrongTransferAmount {
3059                actual: transferred,
3060                expected: amount,
3061            });
3062        }
3063        Ok(EffectOutcome {
3064            created_principal: None,
3065            post_cycles: None,
3066            receipt: Some(operation_id.to_string()),
3067        })
3068    }
3069}
3070
3071impl EnsurePlatform for IcpEnsurePlatform {
3072    type Error = IcpEnsurePlatformError;
3073
3074    fn report_progress(&mut self, progress: FleetEnsureProgress) {
3075        if let Some(handler) = self.progress_handler.as_mut() {
3076            handler(progress);
3077        }
3078    }
3079
3080    fn require_retained_root_activation(
3081        &mut self,
3082        operation_id: &str,
3083        root: &str,
3084        state: &FleetEnsureStateRecord,
3085    ) -> Result<(), Self::Error> {
3086        let authorities = canic_init::compile_root_authorities(
3087            &self.root,
3088            &self.desired,
3089            &self.protocol_principals(state),
3090        )?;
3091        let (_, authority) = authorities
3092            .iter()
3093            .find(|(name, _)| name == root)
3094            .ok_or_else(|| canic_init::CanicInitError::MissingRoot(root.to_string()))?;
3095        current_protocol::require_store_installation_binding(
3096            &self.icp,
3097            &self.root_protocol_candid()?,
3098            root,
3099            authority,
3100            operation_id,
3101        )?;
3102        Ok(())
3103    }
3104
3105    fn bind_reviewed_desired(&mut self, desired: &DesiredFleet) -> Result<(), Self::Error> {
3106        self.desired = desired.clone();
3107        Ok(())
3108    }
3109
3110    fn pace_effect_observation(
3111        &mut self,
3112        _action: &EnsureAction,
3113        consecutive_unchanged_observations: u32,
3114    ) {
3115        thread::sleep(protocol_observation_delay(
3116            consecutive_unchanged_observations,
3117            self.initial_observation_delay,
3118            self.maximum_observation_delay,
3119        ));
3120    }
3121
3122    fn pace_root_owned_observation(
3123        &mut self,
3124        _target: &str,
3125        consecutive_retained_observations: u32,
3126    ) {
3127        thread::sleep(protocol_observation_delay(
3128            consecutive_retained_observations,
3129            self.initial_observation_delay,
3130            self.maximum_observation_delay,
3131        ));
3132    }
3133
3134    fn authority_sealed(
3135        &mut self,
3136        operation_id: &str,
3137        action: &EnsureAction,
3138    ) -> Result<bool, Self::Error> {
3139        Ok(super::authority_seal::observe(&self.icp, &self.root, operation_id, action)?.applied)
3140    }
3141
3142    fn reinstall_assets_match(
3143        &mut self,
3144        intent: &crate::fleet_ensure::model::FleetReinstallRecord,
3145        root_name: &str,
3146        check: super::ReinstallAssetCheck,
3147    ) -> Result<bool, Self::Error> {
3148        let binding = intent
3149            .authorities
3150            .iter()
3151            .find(|binding| binding.name == root_name)
3152            .ok_or_else(|| {
3153                IcpEnsurePlatformError::RootManagement("missing reset Root".to_string())
3154            })?;
3155        let principal = parse_principal("reset Root", &binding.principal)?;
3156        let candid = if let Some(source) = intent.source.as_deref()
3157            && check == super::ReinstallAssetCheck::BeforeReset
3158        {
3159            let path = source
3160                .reviewed_desired
3161                .desired()
3162                .protocol
3163                .as_ref()
3164                .map(|protocol| &protocol.root_candid)
3165                .ok_or_else(|| pool_configuration_error("missing source Root protocol".into()))?;
3166            let hash = super::artifact_sha256(&self.root, path)
3167                .map_err(|error| pool_configuration_error(error.to_string()))?;
3168            if source.candid_sha256_by_path.get(path) != Some(&hash) {
3169                return Err(pool_configuration_error(
3170                    "source Root protocol changed".into(),
3171                ));
3172            }
3173            self.root.join(path)
3174        } else {
3175            self.root_protocol_candid()?
3176        };
3177        for asset in intent.assets.iter().filter(|asset| asset.root == root_name) {
3178            let target = parse_principal("reset asset", &asset.principal)?;
3179            crate::canister_protocol::inspection::preflight_inspection(
3180                &self.icp, &candid, principal, target,
3181            )
3182            .map_err(current_protocol::CurrentProtocolError::from)?;
3183            let response: RootInspectionResponse = call_with_candid(
3184                &self.icp,
3185                &candid,
3186                principal,
3187                canic_protocol::CANIC_ROOT_COMMAND,
3188                &RootInspectionCommand::InspectCanister(CanisterInspectionRequest {
3189                    canister_id: target,
3190                }),
3191            )
3192            .map_err(current_protocol::CurrentProtocolError::from)?;
3193            let status = response
3194                .into_status(principal, target)
3195                .map_err(current_protocol::CurrentProtocolError::from)?;
3196            let mut controllers = status
3197                .settings
3198                .controllers
3199                .iter()
3200                .map(Principal::to_text)
3201                .collect::<Vec<_>>();
3202            controllers.sort();
3203            let module = status
3204                .module_hash
3205                .as_ref()
3206                .map(canic_core::cdk::utils::hash::hex_bytes);
3207            let module_matches =
3208                check == super::ReinstallAssetCheck::Terminal || module == asset.module_sha256;
3209            if controllers != asset.controllers || !module_matches {
3210                return Ok(false);
3211            }
3212        }
3213        Ok(true)
3214    }
3215
3216    fn reinstall_authorities(
3217        &mut self,
3218        state: &FleetEnsureStateRecord,
3219    ) -> Result<Option<BTreeMap<String, RootManagementCanisterObservation>>, Self::Error> {
3220        self.require_operator()?;
3221        let network = resolve_icp_build_network_from_root(&self.root, &self.desired.environment)
3222            .map_err(|error| IcpEnsurePlatformError::RootManagement(error.to_string()))?;
3223        let catalog = if network == BuildNetwork::Ic {
3224            let now = SystemTime::now()
3225                .duration_since(UNIX_EPOCH)
3226                .map_err(|_| {
3227                    IcpEnsurePlatformError::RootManagement(
3228                        "system clock is before the Unix epoch".to_string(),
3229                    )
3230                })?
3231                .as_secs();
3232            Some(
3233                load_cached_mainnet_subnet_catalog(&self.root, now)
3234                    .map_err(IcpEnsurePlatformError::SubnetCatalog)?,
3235            )
3236        } else {
3237            None
3238        };
3239        let mut authorities = BTreeMap::new();
3240        let root_targets = self
3241            .desired
3242            .canisters
3243            .iter()
3244            .filter(|c| c.kind == DesiredCanisterKind::Root)
3245            .map(|c| c.name.clone())
3246            .collect();
3247        let roots = self
3248            .observe_root_management(state, &root_targets)?
3249            .ok_or_else(|| {
3250                IcpEnsurePlatformError::RootManagement("missing Root authority".to_string())
3251            })?;
3252        for configured in &self.desired.canisters {
3253            if configured.kind == DesiredCanisterKind::Pool {
3254                continue;
3255            }
3256            let principal = self
3257                .current_principal(state, &configured.name)
3258                .ok_or_else(|| {
3259                    IcpEnsurePlatformError::UnresolvedCreated(configured.name.clone())
3260                })?;
3261            // Reuse only this pass's Root observation; a subsequent guard starts fresh.
3262            let live = if let Some(root) = roots.roots.get(&configured.name) {
3263                self.complete_install_status(principal, root.live.clone())?
3264            } else {
3265                self.install_status_optional(principal)?.ok_or_else(|| {
3266                    IcpEnsurePlatformError::RootManagement("missing infrastructure".to_string())
3267                })?
3268            };
3269            let subnet = if let Some(root) = roots.roots.get(&configured.name) {
3270                root.subnet.clone()
3271            } else if let Some(catalog) = &catalog {
3272                catalog
3273                    .catalog
3274                    .resolve_canister_route(principal)
3275                    .map(|route| route.subnet.to_text())
3276                    .map_err(|error| IcpEnsurePlatformError::RootManagement(error.to_string()))?
3277            } else {
3278                configured.subnet.clone()
3279            };
3280            authorities.insert(
3281                configured.name.clone(),
3282                RootManagementCanisterObservation {
3283                    live,
3284                    name: configured.name.clone(),
3285                    subnet,
3286                },
3287            );
3288        }
3289        Ok(Some(authorities))
3290    }
3291
3292    fn reinstall_inventory(
3293        &mut self,
3294        source_operation_id: &str,
3295        state: &FleetEnsureStateRecord,
3296    ) -> Result<Option<super::FleetReinstallObservation>, Self::Error> {
3297        let authorities = self.reinstall_authorities(state)?.ok_or_else(|| {
3298            IcpEnsurePlatformError::RootManagement("missing infrastructure".to_string())
3299        })?;
3300        let terminal = self.terminal_inventory(source_operation_id, state)?;
3301        let mut observation = self.observe(source_operation_id, state)?;
3302        for (name, observed) in &authorities {
3303            observation
3304                .canisters
3305                .insert(name.clone(), Some(observed.live.clone()));
3306        }
3307        let candid = self.root_protocol_candid()?;
3308        let candid_by_root = observation
3309            .estate_funding_domains
3310            .keys()
3311            .map(|name| (name.clone(), candid.clone()))
3312            .collect();
3313        let assets = self.inspect_reinstall_assets(
3314            state,
3315            &authorities,
3316            &mut observation,
3317            &candid_by_root,
3318            false,
3319        )?;
3320        let seen = assets
3321            .iter()
3322            .map(|asset| asset.principal.clone())
3323            .collect::<BTreeSet<_>>();
3324        let infrastructure = authorities
3325            .values()
3326            .map(|a| a.live.principal.as_str())
3327            .collect::<BTreeSet<_>>();
3328        if terminal
3329            .controlled_cycles_by_principal
3330            .keys()
3331            .any(|pid| !infrastructure.contains(pid.as_str()) && !seen.contains(pid))
3332        {
3333            return Err(IcpEnsurePlatformError::RootManagement(
3334                "terminal inventory escapes the reviewed reset estate".to_string(),
3335            ));
3336        }
3337        Ok(Some(super::FleetReinstallObservation {
3338            authorities,
3339            assets,
3340            observation,
3341        }))
3342    }
3343
3344    #[expect(
3345        clippy::too_many_lines,
3346        reason = "one source observation binds old protocol receipts, full pagination and physical management inspection"
3347    )]
3348    fn activation_reset_inventory(
3349        &mut self,
3350        source: &crate::fleet_ensure::model::FleetActivationSourceRecord,
3351        state: &FleetEnsureStateRecord,
3352    ) -> Result<Option<super::FleetActivationResetObservation>, Self::Error> {
3353        if source.operator != self.desired.operator
3354            || source.cycles_ledger != self.desired.cycles_ledger
3355        {
3356            return Err(pool_configuration_error(
3357                "source operator or Cycles Ledger differs from the requested reset".to_string(),
3358            ));
3359        }
3360        let authorities = self
3361            .reinstall_authorities(state)?
3362            .ok_or_else(|| pool_configuration_error("missing source infrastructure".to_string()))?;
3363        let mut observation = FleetObservation {
3364            additional_controlled_cycles: BTreeMap::new(),
3365            canisters: authorities
3366                .iter()
3367                .map(|(name, observed)| (name.clone(), Some(observed.live.clone())))
3368                .collect(),
3369            estate_funding_domains: BTreeMap::new(),
3370            ledger_fee_cycles: self
3371                .desired
3372                .ledger_fee_cycles
3373                .parse::<Cycles>()
3374                .map_err(|_| pool_configuration_error("invalid source fee bound".to_string()))?
3375                .to_u128(),
3376            operator_cycles: 0,
3377            protocol_ready: BTreeMap::new(),
3378        };
3379        let mut roots = Vec::new();
3380        let mut candid_by_root = BTreeMap::new();
3381        for configured in &self.desired.canisters {
3382            if configured.kind != DesiredCanisterKind::Root {
3383                continue;
3384            }
3385            let live = &authorities
3386                .get(&configured.name)
3387                .ok_or_else(|| pool_configuration_error("missing source Root".to_string()))?
3388                .live;
3389            let root = parse_principal("source Root", &live.principal)?;
3390            let evidence = current_protocol::observe_inactive_activation(
3391                &self.icp,
3392                &self.root,
3393                source,
3394                &configured.name,
3395                root,
3396            )?;
3397            let candid = source
3398                .registry_preparations
3399                .iter()
3400                .find_map(|action| match action {
3401                    EnsureAction::FleetProtocol {
3402                        principal, candid, ..
3403                    } if principal == &live.principal => Some(self.root.join(candid)),
3404                    _ => None,
3405                })
3406                .ok_or_else(|| {
3407                    pool_configuration_error("missing source Root read contract".to_string())
3408                })?;
3409            let mut inventory = EstatePoolInventoryAccumulator::default();
3410            let mut cursor = None;
3411            loop {
3412                let page = self.query_estate_pool_page(&candid, root, cursor)?;
3413                if !inactive_source_pool_has_full_inventory(&page, &evidence) {
3414                    return Err(pool_configuration_error(
3415                        "source pool has unsettled work or spare creation capacity".to_string(),
3416                    ));
3417                }
3418                let next = inventory.observe_page(&configured.name, page)?;
3419                if next.is_none() {
3420                    break;
3421                }
3422                if next == cursor {
3423                    return Err(pool_configuration_error(
3424                        "source pool cursor did not advance".to_string(),
3425                    ));
3426                }
3427                cursor = next;
3428            }
3429            observation.estate_funding_domains.insert(
3430                configured.name.clone(),
3431                EstateFundingDomainObservation {
3432                    balance_cycles: Some(self.cycles_ledger_balance(&live.principal)?),
3433                    cycles_ledger: self.desired.cycles_ledger.clone(),
3434                    pool: Some(inventory.finish(&configured.name)?),
3435                    root_principal: Some(live.principal.clone()),
3436                },
3437            );
3438            candid_by_root.insert(configured.name.clone(), candid);
3439            roots.push(evidence);
3440        }
3441        if roots.is_empty() || roots.len() != source.registry_preparations.len() {
3442            return Err(pool_configuration_error(
3443                "source Root inventory is incomplete".to_string(),
3444            ));
3445        }
3446        let assets = self.inspect_reinstall_assets(
3447            state,
3448            &authorities,
3449            &mut observation,
3450            &candid_by_root,
3451            false,
3452        )?;
3453        Ok(Some(super::FleetActivationResetObservation {
3454            inventory: super::FleetReinstallObservation {
3455                authorities,
3456                assets,
3457                observation,
3458            },
3459            roots,
3460        }))
3461    }
3462
3463    fn activation_reset_inventory_after_reset(
3464        &mut self,
3465        intent: &crate::fleet_ensure::model::FleetReinstallRecord,
3466        state: &FleetEnsureStateRecord,
3467    ) -> Result<Option<super::FleetReinstallObservation>, Self::Error> {
3468        let authorities = self
3469            .reinstall_authorities(state)?
3470            .ok_or_else(|| pool_configuration_error("missing reset infrastructure".to_string()))?;
3471        let mut observation = FleetObservation {
3472            canisters: authorities
3473                .iter()
3474                .map(|(name, entry)| (name.clone(), Some(entry.live.clone())))
3475                .collect(),
3476            additional_controlled_cycles: BTreeMap::new(),
3477            estate_funding_domains: BTreeMap::new(),
3478            ledger_fee_cycles: self
3479                .desired
3480                .ledger_fee_cycles
3481                .parse::<Cycles>()
3482                .map_err(|_| pool_configuration_error("invalid reset Ledger fee".to_string()))?
3483                .to_u128(),
3484            operator_cycles: 0,
3485            protocol_ready: BTreeMap::new(),
3486        };
3487        let candid = self.root_protocol_candid()?;
3488        let mut candid_by_root = BTreeMap::new();
3489        for configured in &self.desired.canisters {
3490            if configured.kind != DesiredCanisterKind::Root {
3491                continue;
3492            }
3493            let binding = authorities
3494                .get(&configured.name)
3495                .ok_or_else(|| pool_configuration_error("missing reset Root".to_string()))?;
3496            let root = parse_principal("reset Root", &binding.live.principal)?;
3497            let mut inventory = EstatePoolInventoryAccumulator::default();
3498            let mut cursor = None;
3499            loop {
3500                let page = self.query_estate_pool_page(&candid, root, cursor)?;
3501                let next = inventory.observe_page(&configured.name, page)?;
3502                if next.is_none() {
3503                    break;
3504                }
3505                if next == cursor {
3506                    return Err(pool_configuration_error(
3507                        "reset inventory cursor did not advance".to_string(),
3508                    ));
3509                }
3510                cursor = next;
3511            }
3512            let pool = inventory.finish(&configured.name)?;
3513            let expected = intent
3514                .assets
3515                .iter()
3516                .filter(|asset| asset.root == configured.name)
3517                .map(|asset| asset.principal.as_str())
3518                .collect::<BTreeSet<_>>();
3519            let observed = pool
3520                .assets
3521                .iter()
3522                .map(|asset| asset.principal.as_str())
3523                .collect::<BTreeSet<_>>();
3524            if observed != expected || pool.pending_creation.is_some() {
3525                return Err(pool_configuration_error(
3526                    "reset changed the complete physical estate".to_string(),
3527                ));
3528            }
3529            observation.estate_funding_domains.insert(
3530                configured.name.clone(),
3531                EstateFundingDomainObservation {
3532                    balance_cycles: Some(self.cycles_ledger_balance(&binding.live.principal)?),
3533                    cycles_ledger: self.desired.cycles_ledger.clone(),
3534                    pool: Some(pool),
3535                    root_principal: Some(binding.live.principal.clone()),
3536                },
3537            );
3538            candid_by_root.insert(configured.name.clone(), candid.clone());
3539        }
3540        let assets = self.inspect_reinstall_assets(
3541            state,
3542            &authorities,
3543            &mut observation,
3544            &candid_by_root,
3545            true,
3546        )?;
3547        Ok(Some(super::FleetReinstallObservation {
3548            authorities,
3549            assets,
3550            observation,
3551        }))
3552    }
3553
3554    fn observe_root_management(
3555        &mut self,
3556        state: &FleetEnsureStateRecord,
3557        reviewed_targets: &BTreeSet<String>,
3558    ) -> Result<Option<RootManagementObservation>, Self::Error> {
3559        self.timed_observation(FleetObservationStage::RootManagement, |platform| {
3560            let configured_roots = platform
3561                .desired
3562                .canisters
3563                .iter()
3564                .filter(|configured| {
3565                    configured.kind == DesiredCanisterKind::Root
3566                        && configured.presence
3567                            == crate::fleet_ensure::model::DesiredPresence::Present
3568                })
3569                .collect::<Vec<_>>();
3570            if configured_roots.is_empty() {
3571                return Ok(None);
3572            }
3573            platform.require_operator()?;
3574            let mut observed_roots = Vec::new();
3575            for configured in configured_roots {
3576                let Some(principal) = platform.current_principal(state, &configured.name) else {
3577                    if is_unallocated_fresh_root(&platform.desired, configured, reviewed_targets) {
3578                        continue;
3579                    }
3580                    return Err(IcpEnsurePlatformError::RootManagement(format!(
3581                        "configured Root {} has no exact Principal",
3582                        configured.name
3583                    )));
3584                };
3585                let live = platform.status_optional(principal)?.ok_or_else(|| {
3586                    IcpEnsurePlatformError::RootManagement(format!(
3587                        "configured Root {} is unavailable",
3588                        configured.name
3589                    ))
3590                })?;
3591                observed_roots.push((configured, live));
3592            }
3593            let all_current =
3594                platform.management_roots_are_current(&observed_roots, reviewed_targets)?;
3595            if reviewed_targets.is_empty() && all_current {
3596                return Ok(None);
3597            }
3598            let network =
3599                resolve_icp_build_network_from_root(&platform.root, &platform.desired.environment)
3600                    .map_err(|error| IcpEnsurePlatformError::RootManagement(error.to_string()))?;
3601            let catalog = if network == BuildNetwork::Ic {
3602                let now = SystemTime::now()
3603                    .duration_since(UNIX_EPOCH)
3604                    .map_err(|_| {
3605                        IcpEnsurePlatformError::RootManagement(
3606                            "system clock is before the Unix epoch".to_string(),
3607                        )
3608                    })?
3609                    .as_secs();
3610                Some(
3611                    load_cached_mainnet_subnet_catalog(&platform.root, now)
3612                        .map_err(IcpEnsurePlatformError::SubnetCatalog)?,
3613                )
3614            } else {
3615                None
3616            };
3617            let mut roots = BTreeMap::new();
3618            for (configured, live) in observed_roots {
3619                let principal = live.principal.clone();
3620                let subnet = catalog.as_ref().map_or_else(
3621                    || Ok(configured.subnet.clone()),
3622                    |catalog| {
3623                        catalog
3624                            .catalog
3625                            .resolve_canister_route(&principal)
3626                            .map(|route| route.subnet.to_text())
3627                            .map_err(|error| {
3628                                IcpEnsurePlatformError::RootManagement(error.to_string())
3629                            })
3630                    },
3631                )?;
3632                let name = configured.name.clone();
3633                if roots
3634                    .insert(
3635                        name.clone(),
3636                        RootManagementCanisterObservation { live, name, subnet },
3637                    )
3638                    .is_some()
3639                {
3640                    return Err(IcpEnsurePlatformError::RootManagement(
3641                        "configured Root names are not unique".to_string(),
3642                    ));
3643                }
3644            }
3645            let operator_cycles = platform
3646                .icp
3647                .identity_cycles_balance()
3648                .map_err(|error| IcpEnsurePlatformError::LedgerWithdraw(error.to_string()))?;
3649            Ok(Some(RootManagementObservation {
3650                operator_cycles,
3651                roots,
3652            }))
3653        })
3654    }
3655
3656    fn observe_operator_funding(
3657        &mut self,
3658    ) -> Result<Option<crate::fleet_ensure::view::OperatorFundingObservation>, Self::Error> {
3659        self.require_operator()?;
3660        let ledger_fee_cycles = ledger_fee_cycles(self.icp.canister_query_candid(
3661            &self.desired.cycles_ledger,
3662            "icrc1_fee",
3663            &(),
3664            None,
3665        )?)?;
3666        Ok(Some(
3667            crate::fleet_ensure::view::OperatorFundingObservation {
3668                cycles_ledger: self.desired.cycles_ledger.clone(),
3669                ledger_fee_cycles,
3670                operator_cycles: self.cycles_ledger_balance(&self.desired.operator)?,
3671            },
3672        ))
3673    }
3674
3675    fn observe_native_funding(
3676        &mut self,
3677        root: &str,
3678        state: &FleetEnsureStateRecord,
3679    ) -> Result<Option<crate::fleet_ensure::model::NativeFundingObservation>, Self::Error> {
3680        self.require_operator()?;
3681        let configured = self
3682            .desired
3683            .canisters
3684            .iter()
3685            .find(|configured| {
3686                configured.name == root
3687                    && configured.kind == DesiredCanisterKind::Root
3688                    && configured.presence == crate::fleet_ensure::model::DesiredPresence::Present
3689            })
3690            .ok_or_else(|| {
3691                IcpEnsurePlatformError::RootManagement(
3692                    "native funding requires a configured Root".into(),
3693                )
3694            })?;
3695        let principal = self
3696            .current_principal(state, &configured.name)
3697            .ok_or_else(|| {
3698                IcpEnsurePlatformError::RootManagement(
3699                    "native funding requires an allocated Root".into(),
3700                )
3701            })?;
3702        let live = self.status_optional(principal)?.ok_or_else(|| {
3703            IcpEnsurePlatformError::RootManagement("native funding Root is unavailable".into())
3704        })?;
3705        if let Some(wasm) = &configured.wasm {
3706            let expected = artifact_hash(&resolve_path(&self.root, wasm))?;
3707            if live.module_sha256.as_deref() != Some(&expected) {
3708                return Err(IcpEnsurePlatformError::RootManagement(
3709                    "native funding Root module differs from the reviewed artifact".into(),
3710                ));
3711            }
3712        }
3713        let fee = ledger_fee_cycles(self.icp.canister_query_candid(
3714            &self.desired.cycles_ledger,
3715            "icrc1_fee",
3716            &(),
3717            None,
3718        )?)?;
3719        let operator_cycles = self.cycles_ledger_balance(&self.desired.operator)?;
3720        Ok(Some(crate::fleet_ensure::model::NativeFundingObservation {
3721            cycles_ledger: self.desired.cycles_ledger.clone(),
3722            ledger_fee_cycles: fee,
3723            live,
3724            operator_cycles,
3725        }))
3726    }
3727
3728    fn observe(
3729        &mut self,
3730        operation_id: &str,
3731        state: &FleetEnsureStateRecord,
3732    ) -> Result<FleetObservation, Self::Error> {
3733        self.with_observation_snapshot(|platform| {
3734            platform.observe_fleet_snapshot(operation_id, state)
3735        })
3736    }
3737
3738    fn protocol_actions(
3739        &mut self,
3740        operation_id: &str,
3741        state: &FleetEnsureStateRecord,
3742    ) -> Result<Vec<EnsureAction>, Self::Error> {
3743        self.timed_observation(FleetObservationStage::ProtocolActions, |platform| {
3744            if platform.desired.protocol.is_none() {
3745                return Ok(Vec::new());
3746            }
3747            current_protocol::validate_component_pool_capacity(&platform.root, &platform.desired)?;
3748            if !platform.current_protocol_owners_are_ready(state)? {
3749                return Ok(Vec::new());
3750            }
3751            let reconciliation = current_protocol::compile_pool_reconciliation(
3752                &platform.root,
3753                &platform.desired,
3754                state,
3755                &platform.estate_observations,
3756            )?;
3757            if !reconciliation.is_empty() {
3758                return Ok(reconciliation);
3759            }
3760            current_protocol::compile(
3761                &platform.icp,
3762                &platform.root,
3763                &platform.desired,
3764                operation_id,
3765                state,
3766            )
3767            .map_err(Into::into)
3768        })
3769    }
3770
3771    fn fresh_protocol_actions(
3772        &mut self,
3773        operation_id: &str,
3774        state: &FleetEnsureStateRecord,
3775    ) -> Result<Vec<EnsureAction>, Self::Error> {
3776        current_protocol::compile_fresh_protocol(&self.root, &self.desired, state, operation_id)
3777            .map_err(Into::into)
3778    }
3779
3780    fn terminal_inventory(
3781        &mut self,
3782        operation_id: &str,
3783        state: &FleetEnsureStateRecord,
3784    ) -> Result<TerminalFleetInventory, Self::Error> {
3785        self.timed_observation(FleetObservationStage::TerminalInventory, |platform| {
3786            if platform.desired.protocol.is_none() {
3787                if state.active_registry.is_some() {
3788                    return Err(current_protocol::CurrentProtocolError::Configuration(
3789                        "managed terminal Fleet cannot drop its typed protocol intent".to_string(),
3790                    )
3791                    .into());
3792                }
3793                return Ok(TerminalFleetInventory::default());
3794            }
3795            super::current_inventory::terminal_inventory(
3796                &platform.icp,
3797                &platform.root,
3798                &platform.desired,
3799                operation_id,
3800                state,
3801            )
3802            .map_err(Into::into)
3803        })
3804    }
3805
3806    #[expect(
3807        clippy::too_many_lines,
3808        reason = "the platform keeps every effect's exact live terminal predicate in one exhaustive match"
3809    )]
3810    fn observe_effect(
3811        &mut self,
3812        operation_id: &str,
3813        action: &EnsureAction,
3814        record: &EffectRecord,
3815        state: &FleetEnsureStateRecord,
3816    ) -> Result<EffectObservation, Self::Error> {
3817        if matches!(action, EnsureAction::SealAuthority { .. }) {
3818            return super::authority_seal::observe(&self.icp, &self.root, operation_id, action)
3819                .map_err(Into::into);
3820        }
3821        let mut retry = EffectRetry::None;
3822        let mut post_cycles = None;
3823        let (applied, progress_identity) = match action {
3824            EnsureAction::SealAuthority { .. } => {
3825                unreachable!("authority seals use their typed observer")
3826            }
3827            EnsureAction::Create {
3828                name,
3829                requested_initial_cycles,
3830                ..
3831            } => {
3832                let (live_cycles, deferred) =
3833                    if record.created_principal.is_some() && record.receipt.is_some() {
3834                        self.created_canister_cycles(
3835                            name,
3836                            record
3837                                .created_principal
3838                                .as_deref()
3839                                .ok_or(IcpEnsurePlatformError::LedgerCreatePending)?,
3840                            state,
3841                        )?
3842                    } else {
3843                        (None, false)
3844                    };
3845                post_cycles = live_cycles;
3846                let maximum_observation_burn_cycles =
3847                    super::maximum_creation_observation_burn(&self.desired).ok_or(
3848                        IcpEnsurePlatformError::Arithmetic("Create execution and observation burn"),
3849                    )?;
3850                let applied = create_balance_is_terminal(
3851                    live_cycles,
3852                    *requested_initial_cycles,
3853                    maximum_observation_burn_cycles,
3854                );
3855                if live_cycles.is_some() && !applied {
3856                    retry = EffectRetry::ReplanRequiredAfterCreateBalanceDrift;
3857                } else if deferred {
3858                    retry = EffectRetry::DeferUntilControllerObservation;
3859                }
3860                (
3861                    applied,
3862                    format!(
3863                        "created:{:?}:actual:{live_cycles:?}:requested:{requested_initial_cycles}",
3864                        record.created_principal,
3865                    ),
3866                )
3867            }
3868            EnsureAction::Delete { principal, .. } => {
3869                let live = self.status_optional(Self::action_principal(state, principal)?)?;
3870                (live.is_none(), format!("delete:{live:?}"))
3871            }
3872            EnsureAction::Fund {
3873                amount,
3874                expected_post_cycles,
3875                funding_deficit_cycles,
3876                funding_margin_cycles,
3877                ..
3878            } => {
3879                let live_cycles = self.action_cycles(action, state)?;
3880                post_cycles = live_cycles;
3881                let observation = NativeFundingObservation {
3882                    amount: *amount,
3883                    expected_post_cycles: *expected_post_cycles,
3884                    funding_deficit_cycles: *funding_deficit_cycles,
3885                    funding_margin_cycles: *funding_margin_cycles,
3886                    live_cycles,
3887                    pre_cycles: record.pre_cycles,
3888                };
3889                (
3890                    native_funding_completion(observation, record.receipt.is_some())?,
3891                    format!(
3892                        "native-topup:ledger-withdraw:{}:actual:{live_cycles:?}:expected:{expected_post_cycles}:margin:{funding_margin_cycles}",
3893                        record.receipt.as_deref().unwrap_or("pending"),
3894                    ),
3895                )
3896            }
3897            EnsureAction::FundEstate {
3898                amount,
3899                expected_post_cycles,
3900                ledger_fee_cycles,
3901                principal,
3902                ..
3903            } => {
3904                let source_cycles = self
3905                    .icp
3906                    .identity_cycles_balance()
3907                    .map_err(|error| IcpEnsurePlatformError::LedgerTransfer(error.to_string()))?;
3908                let target = Self::action_principal(state, principal)?;
3909                let destination_cycles = self.cycles_ledger_balance(target)?;
3910                post_cycles = Some(source_cycles);
3911                (
3912                    record.receipt.is_some()
3913                        && estate_funding_applied(EstateFundingObservation {
3914                            amount: *amount,
3915                            destination_after: destination_cycles,
3916                            destination_before: record.destination_pre_cycles,
3917                            expected_destination_after: *expected_post_cycles,
3918                            ledger_fee_cycles: *ledger_fee_cycles,
3919                            source_after: source_cycles,
3920                            source_before: record.pre_cycles,
3921                        }),
3922                    format!(
3923                        "estate-funding:ledger-transfer:{}:source:{source_cycles}:destination:{destination_cycles}:expected:{expected_post_cycles}",
3924                        record.receipt.as_deref().unwrap_or("pending"),
3925                    ),
3926                )
3927            }
3928            EnsureAction::Install {
3929                mode,
3930                reinstall_witness,
3931                principal,
3932                wasm_sha256,
3933                ..
3934            } => {
3935                let live =
3936                    self.install_status_optional(Self::action_principal(state, principal)?)?;
3937                let applied = if let Some(witness) = reinstall_witness {
3938                    let current = self.reinstall_authorities(state)?;
3939                    let authority = current
3940                        .as_ref()
3941                        .and_then(|a| a.get(&witness.authority.name))
3942                        .and_then(super::reinstall::authority_binding);
3943                    let history_authority = authority
3944                        .as_ref()
3945                        .and_then(|current| {
3946                            crate::fleet_ensure::policy::reinstall::history_authority(
3947                                witness,
3948                                current,
3949                                principal,
3950                                wasm_sha256,
3951                                &record.state,
3952                            )
3953                        })
3954                        .ok_or_else(|| IcpEnsurePlatformError::ReinstallHistoryConflict {
3955                            canister: witness.authority.principal.clone(),
3956                        })?;
3957                    let live = live.as_ref().ok_or_else(|| {
3958                        IcpEnsurePlatformError::ReinstallHistoryConflict {
3959                            canister: principal.clone(),
3960                        }
3961                    })?;
3962                    let before = record.pre_canister_version.ok_or_else(|| {
3963                        IcpEnsurePlatformError::ReinstallHistoryConflict {
3964                            canister: principal.clone(),
3965                        }
3966                    })?;
3967                    let history = super::install_history::observe(
3968                        &self.icp,
3969                        &self.root,
3970                        &history_authority,
3971                        parse_principal("operator", &self.desired.operator)?,
3972                        before,
3973                        live,
3974                    )?;
3975                    match super::install_history::reconcile(
3976                        history,
3977                        wasm_sha256,
3978                        &witness.prior_module_sha256,
3979                        before,
3980                        live,
3981                    ) {
3982                        super::install_history::HistoryEffect::Applied => true,
3983                        super::install_history::HistoryEffect::NotApplied => false,
3984                        super::install_history::HistoryEffect::Conflict => {
3985                            return Err(IcpEnsurePlatformError::ReinstallHistoryConflict {
3986                                canister: live.principal.clone(),
3987                            });
3988                        }
3989                    }
3990                } else {
3991                    live.as_ref().is_some_and(|live| {
3992                        install_effect_applied(
3993                            *mode,
3994                            wasm_sha256,
3995                            live.module_sha256.as_deref(),
3996                            record.pre_canister_version,
3997                            live.canister_version,
3998                        )
3999                    })
4000                };
4001                let progress_identity = live.as_ref().map_or_else(
4002                    || "install:missing".to_string(),
4003                    |live| {
4004                        format!(
4005                            "install:{:?}:version:{:?}",
4006                            live.module_sha256, live.canister_version
4007                        )
4008                    },
4009                );
4010                (applied, progress_identity)
4011            }
4012            EnsureAction::FleetProtocol {
4013                action: current_action,
4014                ..
4015            } => {
4016                let mut observation = match current_protocol::observe(&self.icp, &self.root, action)
4017                {
4018                    Ok(observation) => observation,
4019                    Err(error)
4020                        if matches!(
4021                            current_action.as_ref(),
4022                            crate::fleet_ensure::model::CurrentFleetProtocolAction::AdoptStore { .. }
4023                        ) && recoverable_current_protocol_error(&error) =>
4024                    {
4025                        EffectObservation {
4026                            provisioning_progress: None,
4027                            provisioning_failure: None,
4028                            applied: false,
4029                            estate_funding_required: None,
4030                            post_cycles: None,
4031                            progress_identity: "store-adoption:protected-status-unavailable"
4032                                .to_string(),
4033                            retry: EffectRetry::None,
4034                        }
4035                    }
4036                    Err(error) => return Err(error.into()),
4037                };
4038                self.refresh_estate_funding_observation(&mut observation, state)?;
4039                return Ok(observation);
4040            }
4041            EnsureAction::Protocol { .. } => {
4042                let observation = protocol::observe(
4043                    &self.icp,
4044                    &self.root,
4045                    operation_id,
4046                    &self.protocol_principals(state),
4047                    &resolved_protocol_action(action, state)?,
4048                )?;
4049                (observation.applied, observation.progress_identity)
4050            }
4051            EnsureAction::SetControllers {
4052                controller_canisters,
4053                controllers,
4054                name,
4055                principal,
4056                ..
4057            } => {
4058                let expected =
4059                    self.resolved_controllers(state, controllers, controller_canisters)?;
4060                let principal = Self::action_principal(state, principal)?;
4061                let configured = self
4062                    .desired
4063                    .canisters
4064                    .iter()
4065                    .find(|configured| configured.name == *name);
4066                let mut observed_controllers = if let Some(configured) =
4067                    configured.filter(|configured| configured.kind == DesiredCanisterKind::Pool)
4068                {
4069                    self.inspect_root_owned_canister(configured, principal, state)?
4070                        .map(|response| {
4071                            response
4072                                .settings
4073                                .controllers
4074                                .into_iter()
4075                                .map(|controller| controller.to_text())
4076                                .collect::<Vec<_>>()
4077                        })
4078                } else {
4079                    self.status_optional(principal)?
4080                        .map(|live| live.controllers)
4081                };
4082                if let Some(controllers) = &mut observed_controllers {
4083                    controllers.sort();
4084                    controllers.dedup();
4085                }
4086                (
4087                    observed_controllers.as_ref() == Some(&expected),
4088                    format!("controllers:{observed_controllers:?}"),
4089                )
4090            }
4091            EnsureAction::Start { principal, .. } => {
4092                let live = self.status_optional(Self::action_principal(state, principal)?)?;
4093                (
4094                    live.as_ref()
4095                        .is_some_and(|live| live.status == CanisterRuntimeStatus::Running),
4096                    format!("start:{:?}", live.map(|live| live.status)),
4097                )
4098            }
4099            EnsureAction::Stop { principal, .. } => {
4100                let live = self.status_optional(Self::action_principal(state, principal)?)?;
4101                (
4102                    live.as_ref()
4103                        .is_some_and(|live| live.status == CanisterRuntimeStatus::Stopped),
4104                    format!("stop:{:?}", live.map(|live| live.status)),
4105                )
4106            }
4107            EnsureAction::Transfer {
4108                amount,
4109                maximum_execution_burn_cycles,
4110                name,
4111                principal,
4112                destination,
4113                ..
4114            } => {
4115                let source = self
4116                    .status_optional(Self::action_principal(state, principal)?)?
4117                    .map(|live| live.cycles)
4118                    .ok_or_else(|| IcpEnsurePlatformError::MissingTransferBalance {
4119                        canister: name.clone(),
4120                        side: "live source",
4121                    })?;
4122                let destination = self
4123                    .status_optional(self.current_principal(state, destination).ok_or_else(
4124                        || IcpEnsurePlatformError::UnresolvedCreated(destination.clone()),
4125                    )?)?
4126                    .map(|live| live.cycles)
4127                    .ok_or_else(|| IcpEnsurePlatformError::MissingTransferBalance {
4128                        canister: name.clone(),
4129                        side: "live treasury",
4130                    })?;
4131                let source_pre = record.pre_cycles.ok_or_else(|| {
4132                    IcpEnsurePlatformError::MissingTransferBalance {
4133                        canister: name.clone(),
4134                        side: "retained source",
4135                    }
4136                })?;
4137                let destination_pre = record.destination_pre_cycles.ok_or_else(|| {
4138                    IcpEnsurePlatformError::MissingTransferBalance {
4139                        canister: name.clone(),
4140                        side: "retained treasury",
4141                    }
4142                })?;
4143                let reconciliation = reconcile_retirement_transfer(RetirementTransferBalances {
4144                    destination_after: destination,
4145                    destination_before: destination_pre,
4146                    maximum_execution_burn: *maximum_execution_burn_cycles,
4147                    source_after: source,
4148                    source_before: source_pre,
4149                    transfer_amount: *amount,
4150                });
4151                let (applied, source_debit, destination_credit) = match reconciliation {
4152                    Ok(RetirementTransferReconciliation::Pending) => (false, 0, 0),
4153                    Ok(RetirementTransferReconciliation::Conserved {
4154                        destination_credit,
4155                        source_debit,
4156                    }) => (true, source_debit, destination_credit),
4157                    Err(RetirementTransferInvariantError::ArithmeticOverflow) => {
4158                        return Err(IcpEnsurePlatformError::Arithmetic(
4159                            "retirement transfer debit",
4160                        ));
4161                    }
4162                    Err(RetirementTransferInvariantError::BalanceMovedInInvalidDirection) => {
4163                        return Err(IcpEnsurePlatformError::InvalidTransferBalance {
4164                            canister: name.clone(),
4165                        });
4166                    }
4167                    Err(RetirementTransferInvariantError::Unbalanced {
4168                        destination_credit,
4169                        maximum_source_debit,
4170                        source_debit,
4171                    }) => {
4172                        return Err(IcpEnsurePlatformError::UnbalancedTransfer {
4173                            canister: name.clone(),
4174                            destination_credit,
4175                            expected: *amount,
4176                            maximum_source_debit,
4177                            source_debit,
4178                        });
4179                    }
4180                };
4181                (
4182                    applied,
4183                    format!("transfer:{source_debit}:{destination_credit}"),
4184                )
4185            }
4186        };
4187        Ok(EffectObservation {
4188            provisioning_progress: None,
4189            provisioning_failure: None,
4190            applied,
4191            estate_funding_required: None,
4192            post_cycles,
4193            progress_identity,
4194            retry,
4195        })
4196    }
4197
4198    #[expect(
4199        clippy::too_many_lines,
4200        reason = "the platform keeps every approved single-step effect in one exhaustive match"
4201    )]
4202    fn apply(
4203        &mut self,
4204        operation_id: &str,
4205        action: &EnsureAction,
4206        _record: &EffectRecord,
4207        state: &FleetEnsureStateRecord,
4208    ) -> Result<EffectOutcome, Self::Error> {
4209        self.require_operator()?;
4210        match action {
4211            EnsureAction::SealAuthority { .. } => {
4212                super::authority_seal::apply(&self.icp, &self.root, operation_id, action)
4213                    .map_err(Into::into)
4214            }
4215            EnsureAction::Create {
4216                controller_canisters,
4217                controllers,
4218                created_at_time,
4219                ledger,
4220                requested_initial_cycles,
4221                subnet,
4222                ..
4223            } => self.apply_create(
4224                CreateCanisterAuthority {
4225                    controller_canisters,
4226                    controllers,
4227                    created_at_time: *created_at_time,
4228                    ledger,
4229                    requested_initial_cycles: *requested_initial_cycles,
4230                    subnet,
4231                },
4232                state,
4233            ),
4234            EnsureAction::Delete {
4235                maximum_remaining_cycles,
4236                principal,
4237                ..
4238            } => {
4239                let principal = Self::action_principal(state, principal)?;
4240                if let Some(live) = self.status_optional(principal)? {
4241                    if live.cycles > *maximum_remaining_cycles {
4242                        return Err(IcpEnsurePlatformError::MaterialCycles {
4243                            actual: live.cycles,
4244                            canister: principal.to_string(),
4245                            maximum: *maximum_remaining_cycles,
4246                        });
4247                    }
4248                    if live.status != CanisterRuntimeStatus::Stopped {
4249                        return Err(IcpEnsurePlatformError::NotStopped {
4250                            canister: principal.to_string(),
4251                        });
4252                    }
4253                    self.icp.delete_canister_without_cycle_recovery(principal)?;
4254                }
4255                Ok(empty_outcome())
4256            }
4257            EnsureAction::Fund {
4258                amount,
4259                created_at_time,
4260                ledger,
4261                principal,
4262                ..
4263            } => self.apply_fund(
4264                *amount,
4265                *created_at_time,
4266                ledger,
4267                Self::action_principal(state, principal)?,
4268            ),
4269            EnsureAction::FundEstate {
4270                amount,
4271                created_at_time,
4272                ledger,
4273                ledger_fee_cycles,
4274                principal,
4275                ..
4276            } => self.apply_estate_fund(
4277                *amount,
4278                *created_at_time,
4279                ledger,
4280                Self::action_principal(state, principal)?,
4281                *ledger_fee_cycles,
4282            ),
4283            EnsureAction::Install {
4284                canic_init,
4285                init_arg,
4286                init_arg_sha256,
4287                init_candid,
4288                init_candid_sha256,
4289                mode,
4290                principal,
4291                wasm,
4292                wasm_sha256,
4293                ..
4294            } => self.apply_install(
4295                operation_id,
4296                &self.protocol_principals(state),
4297                canic_init.as_ref(),
4298                init_arg.as_deref(),
4299                init_arg_sha256.as_deref(),
4300                init_candid.as_deref(),
4301                init_candid_sha256.as_deref(),
4302                *mode,
4303                Self::action_principal(state, principal)?,
4304                wasm,
4305                wasm_sha256,
4306            ),
4307            EnsureAction::FleetProtocol { .. } => {
4308                current_protocol::apply(&self.icp, &self.root, action).map_err(Into::into)
4309            }
4310            EnsureAction::Protocol { .. } => {
4311                let action = resolved_protocol_action(action, state)?;
4312                protocol::apply(
4313                    &self.icp,
4314                    &self.root,
4315                    operation_id,
4316                    &self.protocol_principals(state),
4317                    &action,
4318                )?;
4319                Ok(EffectOutcome {
4320                    created_principal: None,
4321                    post_cycles: None,
4322                    receipt: Some(operation_id.to_string()),
4323                })
4324            }
4325            EnsureAction::SetControllers {
4326                controller_canisters,
4327                controllers,
4328                principal,
4329                ..
4330            } => self.apply_controllers(
4331                Self::action_principal(state, principal)?,
4332                &self.resolved_controllers(state, controllers, controller_canisters)?,
4333            ),
4334            EnsureAction::Start { principal, .. } => {
4335                self.icp
4336                    .start_canister(Self::action_principal(state, principal)?)?;
4337                Ok(empty_outcome())
4338            }
4339            EnsureAction::Stop { principal, .. } => {
4340                self.icp
4341                    .stop_canister(Self::action_principal(state, principal)?)?;
4342                Ok(empty_outcome())
4343            }
4344            EnsureAction::Transfer {
4345                amount,
4346                candid,
4347                candid_sha256,
4348                destination,
4349                method,
4350                principal,
4351                ..
4352            } => self.apply_transfer(
4353                *amount,
4354                candid,
4355                candid_sha256,
4356                self.current_principal(state, destination).ok_or_else(|| {
4357                    IcpEnsurePlatformError::UnresolvedCreated(destination.clone())
4358                })?,
4359                method,
4360                operation_id,
4361                Self::action_principal(state, principal)?,
4362            ),
4363        }
4364    }
4365
4366    fn action_cycles(
4367        &mut self,
4368        action: &EnsureAction,
4369        state: &FleetEnsureStateRecord,
4370    ) -> Result<Option<u128>, Self::Error> {
4371        if matches!(action, EnsureAction::FundEstate { .. }) {
4372            return self
4373                .icp
4374                .identity_cycles_balance()
4375                .map(Some)
4376                .map_err(|error| IcpEnsurePlatformError::LedgerTransfer(error.to_string()));
4377        }
4378        if let EnsureAction::Fund {
4379            pool_funding: Some(authority),
4380            name,
4381            principal,
4382            ..
4383        } = action
4384        {
4385            let principal = Self::action_principal(state, principal)?;
4386            let module = self.pool_funding_module(name, authority, principal)?;
4387            return self
4388                .inspect_pool_balance(name, &authority.root, principal, module)
4389                .map(Some);
4390        }
4391        let (name, principal) = match action {
4392            EnsureAction::Create { .. } => return Ok(None),
4393            EnsureAction::SealAuthority {
4394                name, principal, ..
4395            }
4396            | EnsureAction::Delete {
4397                name, principal, ..
4398            }
4399            | EnsureAction::FleetProtocol {
4400                name, principal, ..
4401            }
4402            | EnsureAction::Fund {
4403                name, principal, ..
4404            }
4405            | EnsureAction::FundEstate {
4406                name, principal, ..
4407            }
4408            | EnsureAction::Install {
4409                name, principal, ..
4410            }
4411            | EnsureAction::Protocol {
4412                name, principal, ..
4413            }
4414            | EnsureAction::SetControllers {
4415                name, principal, ..
4416            }
4417            | EnsureAction::Start {
4418                name, principal, ..
4419            }
4420            | EnsureAction::Stop {
4421                name, principal, ..
4422            }
4423            | EnsureAction::Transfer {
4424                name, principal, ..
4425            } => (name, Self::action_principal(state, principal)?),
4426        };
4427        let observed = self
4428            .desired
4429            .canisters
4430            .iter()
4431            .find(|configured| configured.name == *name)
4432            .map_or_else(
4433                || self.status_optional(principal),
4434                |configured| self.observe_configured_canister(configured, principal, state),
4435            )?;
4436        Ok(observed.map(|live| live.cycles))
4437    }
4438
4439    fn action_destination_cycles(
4440        &mut self,
4441        action: &EnsureAction,
4442        state: &FleetEnsureStateRecord,
4443    ) -> Result<Option<u128>, Self::Error> {
4444        if let EnsureAction::FundEstate { principal, .. } = action {
4445            let owner = Self::action_principal(state, principal)?;
4446            return self.cycles_ledger_balance(owner).map(Some);
4447        }
4448        let EnsureAction::Transfer { destination, .. } = action else {
4449            return Ok(None);
4450        };
4451        let destination = self
4452            .current_principal(state, destination)
4453            .ok_or_else(|| IcpEnsurePlatformError::UnresolvedCreated(destination.clone()))?;
4454        Ok(self.status_optional(destination)?.map(|live| live.cycles))
4455    }
4456
4457    fn action_canister_version(
4458        &mut self,
4459        action: &EnsureAction,
4460        state: &FleetEnsureStateRecord,
4461    ) -> Result<Option<u64>, Self::Error> {
4462        let EnsureAction::Install { principal, .. } = action else {
4463            return Ok(None);
4464        };
4465        let principal = Self::action_principal(state, principal)?;
4466        let version = self
4467            .install_status_optional(principal)?
4468            .and_then(|live| live.canister_version)
4469            .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
4470                canister: principal.to_string(),
4471                field: "canister_version",
4472            })?;
4473        Ok(Some(version))
4474    }
4475}
4476
4477fn exact_install_canister_status(
4478    icp: &IcpCli,
4479    canister: &str,
4480    projected_canister_version: Option<u64>,
4481    projected_module_sha256: Option<String>,
4482) -> Result<ExactInstallCanisterStatus, IcpEnsurePlatformError> {
4483    exact_install_canister_status_with(
4484        canister,
4485        projected_canister_version,
4486        projected_module_sha256,
4487        |canister_id| {
4488            icp.management_canister_status_candid::<_, ManagementCanisterStatusResponse>(
4489                canister_id,
4490                &ManagementCanisterStatusRequest { canister_id },
4491            )
4492        },
4493    )
4494}
4495
4496fn exact_install_canister_status_with(
4497    canister: &str,
4498    projected_canister_version: Option<u64>,
4499    projected_module_sha256: Option<String>,
4500    management_status: impl FnOnce(
4501        Principal,
4502    )
4503        -> Result<ManagementCanisterStatusResponse, IcpManagementCallError>,
4504) -> Result<ExactInstallCanisterStatus, IcpEnsurePlatformError> {
4505    if let Some(canister_version) = projected_canister_version {
4506        return Ok(ExactInstallCanisterStatus {
4507            canister_version,
4508            module_sha256: projected_module_sha256,
4509        });
4510    }
4511    let canister_id = parse_principal("install target", canister)?;
4512    let response = management_status(canister_id).map_err(|source| {
4513        IcpEnsurePlatformError::InstallVersionProofUnavailable {
4514            canister: canister.to_string(),
4515            source: Box::new(source),
4516        }
4517    })?;
4518    Ok(ExactInstallCanisterStatus {
4519        canister_version: response.version,
4520        module_sha256: response
4521            .module_hash
4522            .map(|hash| canic_core::cdk::utils::hash::hex_bytes(&hash)),
4523    })
4524}
4525
4526fn exact_retained_root_owned_topology<'a>(
4527    state: &'a FleetEnsureStateRecord,
4528    name: &str,
4529    kind: DesiredCanisterKind,
4530    parent: &str,
4531    principal: &str,
4532    root: &str,
4533) -> Option<&'a crate::fleet_ensure::model::FleetEnsureTopologyRecord> {
4534    let child_identity_matches = retained_state_principal_matches(state, name, principal);
4535    let root_identity_matches = retained_state_principal_matches(state, parent, root);
4536    let topology = state
4537        .topology
4538        .get(name)
4539        .filter(|topology| topology.kind == kind && topology.parent.as_deref() == Some(parent));
4540    (child_identity_matches && root_identity_matches)
4541        .then_some(topology)
4542        .flatten()
4543}
4544
4545fn retained_state_principal_matches(
4546    state: &FleetEnsureStateRecord,
4547    name: &str,
4548    principal: &str,
4549) -> bool {
4550    let pending = state.pending_principals.get(name).map(String::as_str);
4551    let terminal = state.principals.get(name).map(String::as_str);
4552    let retained = pending.is_some() || terminal.is_some();
4553    retained
4554        && pending.is_none_or(|retained| retained == principal)
4555        && terminal.is_none_or(|retained| retained == principal)
4556}
4557
4558fn completed_reinstall_continuity(
4559    pre_canister_version: u64,
4560    principal_matches: bool,
4561    topology_matches: bool,
4562    live_module_sha256: Option<&str>,
4563    desired_module_sha256: &str,
4564    live_canister_version: Option<u64>,
4565) -> Result<(), &'static str> {
4566    if !principal_matches {
4567        return Err("Principal");
4568    }
4569    if !topology_matches {
4570        return Err("Root/parent/kind topology");
4571    }
4572    if live_module_sha256 != Some(desired_module_sha256) {
4573        return Err("module SHA-256");
4574    }
4575    if live_canister_version.is_some_and(|version| version <= pre_canister_version) {
4576        return Err("canister version");
4577    }
4578    Ok(())
4579}
4580
4581fn recoverable_root_status_error(error: &CanisterProtocolError) -> bool {
4582    error.is_rejected_with(canic_core::diagnostics::codes::STATE_CONFLICT)
4583        || error.is_rejected_with(canic_core::diagnostics::codes::STATE_UNAVAILABLE)
4584}
4585
4586fn recoverable_current_protocol_error(error: &current_protocol::CurrentProtocolError) -> bool {
4587    matches!(
4588        error,
4589        current_protocol::CurrentProtocolError::Transport(source)
4590            if recoverable_root_status_error(source)
4591    )
4592}
4593
4594pub fn install_effect_applied(
4595    mode: InstallMode,
4596    expected_hash: &str,
4597    live_hash: Option<&str>,
4598    pre_canister_version: Option<u64>,
4599    live_canister_version: Option<u64>,
4600) -> bool {
4601    if live_hash != Some(expected_hash) {
4602        return false;
4603    }
4604    match mode {
4605        InstallMode::Install => true,
4606        InstallMode::Reinstall => pre_canister_version
4607            .zip(live_canister_version)
4608            .is_some_and(|(before, after)| after > before),
4609    }
4610}
4611
4612/// Exact retained and live evidence for one Cycles Ledger withdrawal.
4613#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4614pub struct NativeFundingObservation {
4615    pub amount: u128,
4616    pub expected_post_cycles: u128,
4617    pub funding_deficit_cycles: u128,
4618    pub funding_margin_cycles: u128,
4619    pub live_cycles: Option<u128>,
4620    pub pre_cycles: Option<u128>,
4621}
4622
4623/// Require the reviewed floor after all planning-to-completion burn within its margin.
4624pub const fn native_funding_applied(observation: NativeFundingObservation) -> bool {
4625    let Some(pre_cycles) = observation.pre_cycles else {
4626        return false;
4627    };
4628    let Some(live_cycles) = observation.live_cycles else {
4629        return false;
4630    };
4631    let Some(maximum_live_cycles) = pre_cycles.checked_add(observation.amount) else {
4632        return false;
4633    };
4634    let Some(reviewed_pre_cycles) = observation
4635        .expected_post_cycles
4636        .checked_sub(observation.amount)
4637    else {
4638        return false;
4639    };
4640    let Some(minimum_live_cycles) = observation
4641        .expected_post_cycles
4642        .checked_sub(observation.funding_margin_cycles)
4643    else {
4644        return false;
4645    };
4646    let Some(minimum_from_deficit) =
4647        reviewed_pre_cycles.checked_add(observation.funding_deficit_cycles)
4648    else {
4649        return false;
4650    };
4651    let reviewed_amount_matches =
4652        observation.funding_deficit_cycles > 0 && minimum_live_cycles == minimum_from_deficit;
4653    let observation_is_bounded = pre_cycles <= reviewed_pre_cycles
4654        && live_cycles >= minimum_live_cycles
4655        && live_cycles <= maximum_live_cycles;
4656    reviewed_amount_matches && observation_is_bounded
4657}
4658
4659fn native_funding_completion(
4660    observation: NativeFundingObservation,
4661    has_receipt: bool,
4662) -> Result<bool, IcpEnsurePlatformError> {
4663    if !has_receipt || observation.live_cycles.is_none() {
4664        return Ok(false);
4665    }
4666    if !native_funding_applied(observation) {
4667        return Err(IcpEnsurePlatformError::NativeFundingBalanceDrift {
4668            observation: Box::new(observation),
4669        });
4670    }
4671    Ok(true)
4672}
4673
4674/// Exact retained and live evidence for one operator-to-Root Ledger transfer.
4675#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4676pub struct EstateFundingObservation {
4677    pub amount: u128,
4678    pub destination_after: u128,
4679    pub destination_before: Option<u128>,
4680    pub expected_destination_after: u128,
4681    pub ledger_fee_cycles: u128,
4682    pub source_after: u128,
4683    pub source_before: Option<u128>,
4684}
4685
4686/// Require both sides of an estate-funding transfer to match its reviewed debit.
4687#[must_use]
4688pub const fn estate_funding_applied(observation: EstateFundingObservation) -> bool {
4689    let Some(source_before) = observation.source_before else {
4690        return false;
4691    };
4692    let Some(destination_before) = observation.destination_before else {
4693        return false;
4694    };
4695    let Some(expected_source_debit) = observation
4696        .amount
4697        .checked_add(observation.ledger_fee_cycles)
4698    else {
4699        return false;
4700    };
4701    let Some(source_debit) = source_before.checked_sub(observation.source_after) else {
4702        return false;
4703    };
4704    let Some(destination_credit) = observation
4705        .destination_after
4706        .checked_sub(destination_before)
4707    else {
4708        return false;
4709    };
4710    source_debit == expected_source_debit
4711        && destination_credit == observation.amount
4712        && observation.destination_after == observation.expected_destination_after
4713}
4714
4715/// Creation and Ready funding require empty modules; reviewed reset funding accepts installed assets.
4716#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4717enum InspectedModule {
4718    Any,
4719    Empty,
4720}
4721
4722fn pool_funding_inspected_module(
4723    canister: &str,
4724    reviewed: EstatePoolAssetLifecycle,
4725    observed: &CanisterPoolAssetStatus,
4726) -> Result<InspectedModule, IcpEnsurePlatformError> {
4727    match (reviewed, observed) {
4728        (EstatePoolAssetLifecycle::Ready, CanisterPoolAssetStatus::Ready) => {
4729            Ok(InspectedModule::Empty)
4730        }
4731        (EstatePoolAssetLifecycle::PendingReset, CanisterPoolAssetStatus::PendingReset)
4732        | (EstatePoolAssetLifecycle::Failed, CanisterPoolAssetStatus::Failed { .. }) => {
4733            Ok(InspectedModule::Any)
4734        }
4735        _ => Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
4736            canister: canister.to_string(),
4737            field: "reviewed pool lifecycle",
4738        }),
4739    }
4740}
4741
4742fn validate_pending_fresh_pool_inspection(
4743    canister: &str,
4744    root: &str,
4745    operator: &str,
4746    controllers: &[Principal],
4747    module_hash: Option<&[u8]>,
4748    cycles: &Nat,
4749) -> Result<u128, IcpEnsurePlatformError> {
4750    let mut actual = controllers
4751        .iter()
4752        .map(Principal::to_text)
4753        .collect::<Vec<_>>();
4754    actual.sort();
4755    let mut temporary = vec![root.to_string(), operator.to_string()];
4756    temporary.sort();
4757    if actual != [root] && actual != temporary {
4758        return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
4759            canister: canister.to_string(),
4760            field: "reviewed fresh pool controllers",
4761        });
4762    }
4763    // Controller finalization is itself journalled. Until it completes, the exact
4764    // temporary operator remains a controller of this issued fresh Create only.
4765    validate_inspected_cycles(canister, InspectedModule::Empty, module_hash, cycles)
4766}
4767
4768fn validate_root_controlled_inspection(
4769    canister: &str,
4770    module: InspectedModule,
4771    root: &str,
4772    controllers: &[Principal],
4773    module_hash: Option<&[u8]>,
4774    cycles: &Nat,
4775) -> Result<u128, IcpEnsurePlatformError> {
4776    if controllers.len() != 1 || controllers[0].to_text() != root {
4777        return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
4778            canister: canister.to_string(),
4779            field: "Root-only controllers",
4780        });
4781    }
4782    validate_inspected_cycles(canister, module, module_hash, cycles)
4783}
4784
4785fn validate_inspected_cycles(
4786    canister: &str,
4787    module: InspectedModule,
4788    module_hash: Option<&[u8]>,
4789    cycles: &Nat,
4790) -> Result<u128, IcpEnsurePlatformError> {
4791    if matches!(module, InspectedModule::Empty) && module_hash.is_some() {
4792        return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
4793            canister: canister.to_string(),
4794            field: "module-free pool asset",
4795        });
4796    }
4797    u128::try_from(cycles.0.clone()).map_err(|_| IcpEnsurePlatformError::InvalidStatusCycles {
4798        canister: canister.to_string(),
4799        value: cycles.to_string(),
4800    })
4801}
4802
4803fn created_canister_outcome(
4804    canister_id: Principal,
4805    receipt: String,
4806    requested_initial_cycles: u128,
4807) -> EffectOutcome {
4808    EffectOutcome {
4809        created_principal: Some(canister_id.to_text()),
4810        post_cycles: Some(requested_initial_cycles),
4811        receipt: Some(receipt),
4812    }
4813}
4814
4815fn create_canister_response_outcome(
4816    response: Result<CreateCanisterSuccess, CreateCanisterError>,
4817    requested_initial_cycles: u128,
4818) -> Result<EffectOutcome, IcpEnsurePlatformError> {
4819    match response {
4820        Ok(success) => Ok(created_canister_outcome(
4821            success.canister_id,
4822            success.block_id.to_string(),
4823            requested_initial_cycles,
4824        )),
4825        Err(CreateCanisterError::Duplicate {
4826            duplicate_of,
4827            canister_id: Some(canister_id),
4828        }) => Ok(created_canister_outcome(
4829            canister_id,
4830            duplicate_of.to_string(),
4831            requested_initial_cycles,
4832        )),
4833        Err(CreateCanisterError::Duplicate {
4834            canister_id: None, ..
4835        }) => Err(IcpEnsurePlatformError::LedgerCreatePending),
4836        Err(error) => Err(IcpEnsurePlatformError::LedgerCreate(render_create_error(
4837            error,
4838        ))),
4839    }
4840}
4841
4842fn ledger_fee_cycles(value: Nat) -> Result<u128, IcpEnsurePlatformError> {
4843    let rendered = value.to_string();
4844    u128::try_from(value.0).map_err(|_| IcpEnsurePlatformError::InvalidLedgerFee(rendered))
4845}
4846
4847const fn empty_outcome() -> EffectOutcome {
4848    EffectOutcome {
4849        created_principal: None,
4850        post_cycles: None,
4851        receipt: None,
4852    }
4853}
4854
4855fn parse_status_cycles(value: &str) -> Option<u128> {
4856    value.replace('_', "").trim().parse().ok()
4857}
4858
4859fn normalize_hash(value: &str) -> String {
4860    value
4861        .strip_prefix("0x")
4862        .unwrap_or(value)
4863        .to_ascii_lowercase()
4864}
4865
4866fn parse_principal(field: &'static str, value: &str) -> Result<Principal, IcpEnsurePlatformError> {
4867    Principal::from_text(value).map_err(|_| IcpEnsurePlatformError::InvalidPrincipal {
4868        field,
4869        value: value.to_string(),
4870    })
4871}
4872
4873fn reject_public_status_projection(
4874    report: &IcpCanisterStatusReport,
4875    principal: &str,
4876) -> Result<(), IcpEnsurePlatformError> {
4877    if report.status.is_none()
4878        && report.settings.is_none()
4879        && report.cycles.is_none()
4880        && report.public_controllers.is_some()
4881    {
4882        return Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable {
4883            canister: principal.to_string(),
4884        });
4885    }
4886    Ok(())
4887}
4888
4889fn resolve_path(root: &Path, path: &str) -> PathBuf {
4890    let path = Path::new(path);
4891    if path.is_absolute() {
4892        path.to_path_buf()
4893    } else {
4894        root.join(path)
4895    }
4896}
4897
4898fn verify_artifact(
4899    path: &Path,
4900    expected: &str,
4901    kind: &'static str,
4902) -> Result<(), IcpEnsurePlatformError> {
4903    let bytes = std::fs::read(path)
4904        .map_err(|_| IcpEnsurePlatformError::ArtifactUnavailable(path.to_path_buf()))?;
4905    let actual = canic_core::cdk::utils::hash::sha256_hex(&bytes);
4906    if actual != expected {
4907        return Err(IcpEnsurePlatformError::ArtifactDigestMismatch {
4908            actual,
4909            expected: expected.to_string(),
4910            kind,
4911        });
4912    }
4913    Ok(())
4914}
4915
4916fn artifact_hash(path: &Path) -> Result<String, IcpEnsurePlatformError> {
4917    let bytes = std::fs::read(path)
4918        .map_err(|_| IcpEnsurePlatformError::ArtifactUnavailable(path.to_path_buf()))?;
4919    Ok(canic_core::cdk::utils::hash::sha256_hex(&bytes))
4920}
4921
4922fn resolved_protocol_action(
4923    action: &EnsureAction,
4924    state: &FleetEnsureStateRecord,
4925) -> Result<EnsureAction, IcpEnsurePlatformError> {
4926    let mut resolved = action.clone();
4927    if let EnsureAction::Protocol { principal, .. } = &mut resolved {
4928        *principal = IcpEnsurePlatform::action_principal(state, principal)?.to_string();
4929        return Ok(resolved);
4930    }
4931    Err(IcpEnsurePlatformError::Protocol(
4932        protocol::ProtocolEffectError::WrongAction,
4933    ))
4934}
4935
4936fn render_create_error(error: CreateCanisterError) -> String {
4937    match error {
4938        CreateCanisterError::CreatedInFuture { ledger_time } => {
4939            format!("created in future of ledger time {ledger_time}")
4940        }
4941        CreateCanisterError::Duplicate { duplicate_of, .. } => {
4942            format!("duplicate block {duplicate_of}")
4943        }
4944        CreateCanisterError::FailedToCreate {
4945            error,
4946            fee_block,
4947            refund_block,
4948        } => format!("failed: {error}; fee_block={fee_block:?}; refund_block={refund_block:?}"),
4949        CreateCanisterError::GenericError {
4950            error_code,
4951            message,
4952        } => format!("error {error_code}: {message}"),
4953        CreateCanisterError::InsufficientFunds { balance } => {
4954            format!("insufficient funds: balance={balance}")
4955        }
4956        CreateCanisterError::TemporarilyUnavailable => "temporarily unavailable".to_string(),
4957        CreateCanisterError::TooOld => "request is too old".to_string(),
4958    }
4959}
4960
4961fn render_withdraw_error(error: WithdrawError) -> String {
4962    match error {
4963        WithdrawError::BadFee { expected_fee } => format!("bad fee; expected {expected_fee}"),
4964        WithdrawError::CreatedInFuture { ledger_time } => {
4965            format!("created in future of ledger time {ledger_time}")
4966        }
4967        WithdrawError::Duplicate { duplicate_of } => format!("duplicate block {duplicate_of}"),
4968        WithdrawError::FailedToWithdraw {
4969            fee_block,
4970            rejection_reason,
4971            ..
4972        } => format!("withdraw failed: {rejection_reason}; fee_block={fee_block:?}"),
4973        WithdrawError::GenericError {
4974            error_code,
4975            message,
4976        } => format!("error {error_code}: {message}"),
4977        WithdrawError::InsufficientFunds { balance } => {
4978            format!("insufficient funds: balance={balance}")
4979        }
4980        WithdrawError::InvalidReceiver { receiver } => format!("invalid receiver {receiver}"),
4981        WithdrawError::TemporarilyUnavailable => "temporarily unavailable".to_string(),
4982        WithdrawError::TooOld => "request is too old".to_string(),
4983    }
4984}
4985
4986fn render_ledger_transfer_error(error: CyclesLedgerTransferError) -> String {
4987    match error {
4988        CyclesLedgerTransferError::BadBurn { min_burn_amount } => {
4989            format!("bad burn; minimum {min_burn_amount}")
4990        }
4991        CyclesLedgerTransferError::BadFee { expected_fee } => {
4992            format!("bad fee; expected {expected_fee}")
4993        }
4994        CyclesLedgerTransferError::CreatedInFuture { ledger_time } => {
4995            format!("created in future of ledger time {ledger_time}")
4996        }
4997        CyclesLedgerTransferError::Duplicate { duplicate_of } => {
4998            format!("duplicate block {duplicate_of}")
4999        }
5000        CyclesLedgerTransferError::GenericError {
5001            error_code,
5002            message,
5003        } => format!("error {error_code}: {message}"),
5004        CyclesLedgerTransferError::InsufficientFunds { balance } => {
5005            format!("insufficient funds: balance={balance}")
5006        }
5007        CyclesLedgerTransferError::TemporarilyUnavailable => "temporarily unavailable".to_string(),
5008        CyclesLedgerTransferError::TooOld => "request is too old".to_string(),
5009    }
5010}
5011
5012#[expect(
5013    dead_code,
5014    reason = "Candid variant is retained for exact response decoding"
5015)]
5016const fn rejection_code_name(code: RejectionCode) -> &'static str {
5017    match code {
5018        RejectionCode::CanisterError => "canister_error",
5019        RejectionCode::CanisterReject => "canister_reject",
5020        RejectionCode::DestinationInvalid => "destination_invalid",
5021        RejectionCode::NoError => "no_error",
5022        RejectionCode::SysFatal => "sys_fatal",
5023        RejectionCode::SysTransient => "sys_transient",
5024        RejectionCode::Unknown => "unknown",
5025    }
5026}
5027
5028#[cfg(test)]
5029mod tests {
5030    use super::*;
5031    use canic_core::dto::pool::CanisterPoolAssetStatus;
5032
5033    #[test]
5034    fn identity_binding_keeps_operator_fence_before_effects() {
5035        let mut fixture = ProtocolOwnersFixture::new();
5036        std::fs::write(
5037            fixture.root.join("icp"),
5038            r#"#!/bin/sh
5039if [ "$1" = --version ]; then echo 'icp 1.5.0'; exit 0; fi
5040while [ "$1" = --project-root-override ] || [ "$1" = --identity-password-file ]; do shift 2; done
5041if [ "$1 $2" = 'identity default' ]; then echo selected; exit 0; fi
5042if [ "$1 $2" = 'identity principal' ]; then cat principal; exit 0; fi
5043echo effect >> effects
5044"#,
5045        )
5046        .unwrap();
5047        std::fs::write(fixture.root.join("principal"), "operator").unwrap();
5048        fixture.platform.require_operator().unwrap();
5049        std::fs::write(fixture.root.join("principal"), "changed-principal").unwrap();
5050        let action = EnsureAction::Start {
5051            name: "root".into(),
5052            principal: "root".into(),
5053        };
5054        let receipt = EffectRecord {
5055            maintenance_attempts: 0,
5056            publication_attempts: 0,
5057            action_sha256: crate::fleet_ensure::ops::action_sha256(&action),
5058            created_principal: None,
5059            destination_post_cycles: None,
5060            destination_pre_cycles: None,
5061            post_cycles: None,
5062            pre_cycles: None,
5063            pre_canister_version: None,
5064            progress_identity: None,
5065            receipt: None,
5066            state: crate::fleet_ensure::model::EffectState::Issued,
5067        };
5068        assert!(matches!(
5069            fixture
5070                .platform
5071                .apply("operation", &action, &receipt, &fixture.state),
5072            Err(IcpEnsurePlatformError::OperatorMismatch { .. })
5073        ));
5074        assert!(!fixture.root.join("effects").exists());
5075        std::fs::remove_dir_all(fixture.root).unwrap();
5076    }
5077
5078    #[test]
5079    fn inactive_source_inventory_rejects_spare_capacity_pending_work_and_omissions() {
5080        let root = crate::fleet_ensure::model::RootActivationResetRecord {
5081            root: "root".to_string(),
5082            activation_operation_id: [1; 32],
5083            inventory_hash: [2; 32],
5084            provisioning_receipt_hash: [3; 32],
5085            component_count: 1,
5086            managed_descendants: 2,
5087        };
5088        let page = inactive_source_pool_page();
5089        assert!(inactive_source_pool_has_full_inventory(&page, &root));
5090        let mut complete = EstatePoolInventoryAccumulator::default();
5091        assert_eq!(
5092            complete.observe_page("root", page.clone()).expect("page"),
5093            None
5094        );
5095        assert_eq!(complete.finish("root").expect("complete").assets.len(), 4);
5096
5097        let mut spare = page.clone();
5098        spare.config.maximum_size += 1;
5099        let mut insufficient_reserve = page.clone();
5100        insufficient_reserve.config.minimum_size = 2;
5101        let mut pending = page.clone();
5102        pending.pending_reset = 1;
5103        let mut wrong_workload = page.clone();
5104        wrong_workload.workload -= 1;
5105        wrong_workload.ready += 1;
5106        let mut overflow = page.clone();
5107        overflow.workload = u32::MAX;
5108        for changed in [
5109            spare,
5110            insufficient_reserve,
5111            pending,
5112            wrong_workload,
5113            overflow,
5114        ] {
5115            assert!(!inactive_source_pool_has_full_inventory(&changed, &root));
5116        }
5117
5118        let mut omitted = page;
5119        omitted.entries.pop();
5120        let mut inventory = EstatePoolInventoryAccumulator::default();
5121        inventory
5122            .observe_page("root", omitted)
5123            .expect("bounded page");
5124        assert!(matches!(
5125            inventory.finish("root"),
5126            Err(IcpEnsurePlatformError::CurrentProtocol(
5127                current_protocol::CurrentProtocolError::Configuration(_)
5128            ))
5129        ));
5130    }
5131
5132    fn inactive_source_pool_page() -> CanisterPoolResponse {
5133        let claim = canic_core::dto::pool::CanisterPoolClaim {
5134            component: canic_core::ids::ComponentInstanceId::from_generated_bytes([4; 32]),
5135            operation_id: [5; 32],
5136        };
5137        let entries = (0..5)
5138            .map(|index| CanisterPoolAsset {
5139                canister_id: Principal::from_slice(&[index]),
5140                creation_receipt: None,
5141                cycles: Cycles::new(100),
5142                origin: if index == 0 {
5143                    CanisterPoolAssetOrigin::InfrastructureStore
5144                } else {
5145                    CanisterPoolAssetOrigin::Imported
5146                },
5147                status: match index {
5148                    0 => CanisterPoolAssetStatus::Store,
5149                    1 => CanisterPoolAssetStatus::Ready,
5150                    _ => CanisterPoolAssetStatus::Workload {
5151                        claim: claim.clone(),
5152                    },
5153                },
5154                added_at_ns: 1,
5155                updated_at_ns: 1,
5156            })
5157            .collect();
5158        CanisterPoolResponse {
5159            config: canic_core::ids::FleetSubnetCanisterPoolConfig {
5160                minimum_size: 1,
5161                maximum_size: 4,
5162                canister_cycles: Cycles::new(100),
5163                creation_execution_margin: Cycles::new(1),
5164            },
5165            tracked: 5,
5166            store: 1,
5167            store_deletion_pending: 0,
5168            pooled: 1,
5169            workload: 3,
5170            surplus: 0,
5171            ready: 1,
5172            pending_reset: 0,
5173            claimed: 0,
5174            recycling: 0,
5175            handing_off: 0,
5176            failed: 0,
5177            completed_handoffs: 0,
5178            pending_creation: None,
5179            pending_handoff: None,
5180            entries,
5181            next_start_after: None,
5182        }
5183    }
5184
5185    #[cfg(unix)]
5186    #[test]
5187    fn pool_inspection_preflight_stops_before_update_and_requeries_on_retry() {
5188        let mut fixture = PoolInspectionFixture::new();
5189        let root = fixture.root_id;
5190        let target = fixture.target;
5191        let path = fixture.owners.root.clone();
5192        fixture.inspection_hook(&format!(
5193            "touch '{}'",
5194            path.join("inspection-attempted").display()
5195        ));
5196        PoolInspectionFixture::reserve_response(&path, root, target, 99);
5197        fixture
5198            .owners
5199            .platform
5200            .with_observation_snapshot(|platform| {
5201                let error = platform
5202                    .inspect_pool_balance(
5203                        "pool",
5204                        &root.to_text(),
5205                        &target.to_text(),
5206                        InspectedModule::Any,
5207                    )
5208                    .unwrap_err();
5209                let IcpEnsurePlatformError::CurrentProtocol(
5210                    current_protocol::CurrentProtocolError::Transport(
5211                        crate::CanisterProtocolError::InspectionPreflightReserve(evidence),
5212                    ),
5213                ) = error
5214                else {
5215                    panic!("preflight reserve shortfall");
5216                };
5217                assert_eq!(evidence.available_liquid_cycles, 99);
5218                assert!(!path.join("inspection-attempted").exists());
5219                PoolInspectionFixture::reserve_response(&path, root, target, 100);
5220                assert_eq!(
5221                    platform.inspect_pool_balance(
5222                        "pool",
5223                        &root.to_text(),
5224                        &target.to_text(),
5225                        InspectedModule::Any,
5226                    )?,
5227                    1000
5228                );
5229                assert!(path.join("inspection-attempted").exists());
5230                Ok(())
5231            })
5232            .unwrap();
5233        std::fs::remove_dir_all(path).unwrap();
5234    }
5235
5236    #[cfg(unix)]
5237    #[test]
5238    fn pool_inspection_retains_protected_reserve_evidence() {
5239        let mut fixture = PoolInspectionFixture::new();
5240        let root = fixture.root_id;
5241        let target = fixture.target;
5242        let path = fixture.owners.root.clone();
5243        let evidence = canic_core::dto::canister::CanisterInspectionReserveResponse {
5244            caller: root,
5245            canister_id: target,
5246            native_cycles: 1000,
5247            available_liquid_cycles: 50,
5248            required_liquid_cycles: 100,
5249        };
5250        let bytes = candid::encode_one(Ok::<_, canic_core::dto::error::Error>(
5251            RootInspectionResponse::InspectionReserveRequired(evidence.clone()),
5252        ))
5253        .unwrap();
5254        std::fs::write(
5255            path.join("inspection.json"),
5256            serde_json::json!({"response_bytes": hex_bytes(bytes)}).to_string(),
5257        )
5258        .unwrap();
5259        fixture
5260            .owners
5261            .platform
5262            .with_observation_snapshot(|platform| {
5263                let error = platform
5264                    .inspect_pool_balance(
5265                        "pool",
5266                        &root.to_text(),
5267                        &target.to_text(),
5268                        InspectedModule::Any,
5269                    )
5270                    .unwrap_err();
5271                let IcpEnsurePlatformError::CurrentProtocol(
5272                    current_protocol::CurrentProtocolError::Transport(
5273                        crate::CanisterProtocolError::InspectionReserve(actual),
5274                    ),
5275                ) = error
5276                else {
5277                    panic!("typed reserve failure");
5278                };
5279                assert_eq!(*actual, evidence);
5280                PoolInspectionFixture::response(&path, root, 1000);
5281                assert_eq!(
5282                    platform.inspect_pool_balance(
5283                        "pool",
5284                        &root.to_text(),
5285                        &target.to_text(),
5286                        InspectedModule::Any
5287                    )?,
5288                    1000
5289                );
5290                Ok(())
5291            })
5292            .unwrap();
5293        std::fs::remove_dir_all(path).unwrap();
5294    }
5295
5296    #[test]
5297    fn pool_funding_binds_reset_lifecycle_without_weakening_ready_inspection() {
5298        let failed = CanisterPoolAssetStatus::Failed {
5299            reason: "reset stopped".to_string(),
5300        };
5301        for (lifecycle, status, expected) in [
5302            (
5303                EstatePoolAssetLifecycle::Ready,
5304                CanisterPoolAssetStatus::Ready,
5305                InspectedModule::Empty,
5306            ),
5307            (
5308                EstatePoolAssetLifecycle::PendingReset,
5309                CanisterPoolAssetStatus::PendingReset,
5310                InspectedModule::Any,
5311            ),
5312            (
5313                EstatePoolAssetLifecycle::Failed,
5314                failed,
5315                InspectedModule::Any,
5316            ),
5317        ] {
5318            let module = pool_funding_inspected_module("pool", lifecycle, &status).unwrap();
5319            assert_eq!(module, expected);
5320            let installed =
5321                validate_inspected_cycles("pool", module, Some(&[1; 32]), &Nat::from(100_u8));
5322            assert_eq!(installed.is_ok(), module == InspectedModule::Any);
5323            for wrong in [
5324                EstatePoolAssetLifecycle::Ready,
5325                EstatePoolAssetLifecycle::PendingReset,
5326                EstatePoolAssetLifecycle::Failed,
5327                EstatePoolAssetLifecycle::Workload,
5328            ] {
5329                if wrong != lifecycle {
5330                    assert!(matches!(
5331                        pool_funding_inspected_module("pool", wrong, &status),
5332                        Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { .. })
5333                    ));
5334                }
5335            }
5336        }
5337    }
5338
5339    #[cfg(unix)]
5340    #[test]
5341    fn funding_pause_uses_fresh_ledger_balance_without_completing_protocol_work() {
5342        let mut fixture = ProtocolOwnersFixture::new();
5343        let root = Principal::from_slice(&[1]);
5344        let ledger = Principal::from_slice(&[2]);
5345        fixture.platform.desired.cycles_ledger = ledger.to_text();
5346        fixture.platform.desired.canisters[1].principal = Some(root.to_text());
5347        std::fs::write(
5348            fixture.root.join("icp"),
5349            format!(
5350                "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp 1.5.0'; exit 0; fi\ncat '{}'/balance.json\n",
5351                fixture.root.display(),
5352            ),
5353        )
5354        .unwrap();
5355        let pause = canic_core::dto::component_provisioning::RootEstateFundingRequired {
5356            available: Cycles::new(0),
5357            attempt_count: 0,
5358            creation_amount: Cycles::new(100),
5359            cycles_ledger: ledger,
5360            execution_margin: Cycles::new(10),
5361            last_attempt_at_ns: None,
5362            ledger_fee: Cycles::new(1),
5363            management_creation_fee: Cycles::new(20),
5364            operation_id: [1; 32],
5365            readiness_floor: Cycles::new(70),
5366            required: Cycles::new(101),
5367            retry_at_ns: 60,
5368            root,
5369            shortfall: Cycles::new(101),
5370        };
5371        for available in [0_u128, 40, 101, 404] {
5372            std::fs::write(
5373                fixture.root.join("balance.json"),
5374                serde_json::json!({
5375                    "response_bytes": hex_bytes(candid::encode_one(Nat::from(available)).unwrap()),
5376                })
5377                .to_string(),
5378            )
5379            .unwrap();
5380            let mut observed = EffectObservation {
5381                provisioning_progress: None,
5382                provisioning_failure: None,
5383                applied: false,
5384                estate_funding_required: Some(pause.clone()),
5385                post_cycles: None,
5386                progress_identity: "pending-creation".to_string(),
5387                retry: EffectRetry::ContinuePoolMaintenance,
5388            };
5389            fixture
5390                .platform
5391                .refresh_estate_funding_observation(&mut observed, &fixture.state)
5392                .unwrap();
5393            assert!(!observed.applied);
5394            assert_eq!(observed.retry, EffectRetry::ContinuePoolMaintenance);
5395            assert_eq!(observed.progress_identity, "pending-creation");
5396            let expected = (available < 101).then(|| {
5397                let mut expected = pause.clone();
5398                expected.available = Cycles::new(available);
5399                expected.shortfall = Cycles::new(101 - available);
5400                expected
5401            });
5402            assert_eq!(observed.estate_funding_required, expected);
5403        }
5404        for (root, ledger) in [
5405            (Principal::anonymous(), ledger),
5406            (root, Principal::anonymous()),
5407        ] {
5408            let mut changed = pause.clone();
5409            changed.root = root;
5410            changed.cycles_ledger = ledger;
5411            let mut observed = EffectObservation {
5412                provisioning_progress: None,
5413                provisioning_failure: None,
5414                applied: false,
5415                estate_funding_required: Some(changed),
5416                post_cycles: None,
5417                progress_identity: String::new(),
5418                retry: EffectRetry::None,
5419            };
5420            assert!(matches!(
5421                fixture
5422                    .platform
5423                    .refresh_estate_funding_observation(&mut observed, &fixture.state),
5424                Err(IcpEnsurePlatformError::CurrentProtocol(
5425                    current_protocol::CurrentProtocolError::ResponseMismatch
5426                ))
5427            ));
5428        }
5429        std::fs::remove_dir_all(&fixture.root).unwrap();
5430    }
5431
5432    #[test]
5433    fn completed_withdrawal_outside_bounds_fails_without_endless_observation() {
5434        let observation = NativeFundingObservation {
5435            amount: 350,
5436            expected_post_cycles: 800,
5437            funding_deficit_cycles: 150,
5438            funding_margin_cycles: 200,
5439            live_cycles: Some(599),
5440            pre_cycles: Some(449),
5441        };
5442        assert!(!native_funding_completion(observation, false).unwrap());
5443        assert!(matches!(
5444            native_funding_completion(observation, true),
5445            Err(IcpEnsurePlatformError::NativeFundingBalanceDrift {
5446                observation: actual,
5447            }) if *actual == observation
5448        ));
5449        assert!(
5450            native_funding_completion(
5451                NativeFundingObservation {
5452                    live_cycles: Some(600),
5453                    ..observation
5454                },
5455                true
5456            )
5457            .unwrap()
5458        );
5459    }
5460
5461    #[cfg(unix)]
5462    struct ProtocolOwnersFixture {
5463        root: PathBuf,
5464        platform: IcpEnsurePlatform,
5465        state: FleetEnsureStateRecord,
5466    }
5467
5468    #[cfg(unix)]
5469    impl ProtocolOwnersFixture {
5470        fn new() -> Self {
5471            use std::{fs, os::unix::fs::PermissionsExt};
5472
5473            let root = crate::test_support::temp_dir("canic-protocol-owner-observations");
5474            fs::create_dir_all(&root).unwrap();
5475            fs::write(root.join("owner.wasm"), b"current-owner-module").unwrap();
5476            let executable = root.join("icp");
5477            fs::write(&executable, format!(
5478                "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp 1.5.0'; exit 0; fi\nwhile [ \"$#\" -gt 0 ] && [ \"$1\" != status ]; do shift; done\nshift\nprintf '%s\\n' \"$1\" >> '{}'/calls\ncat '{}'/\"$1\".json\n",
5479                root.display(), root.display(),
5480            )).unwrap();
5481            fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
5482            let canisters = ["coordinator", "root", "store"].map(|name| {
5483                serde_json::json!({
5484                    "controller_canisters": [], "controllers": [], "drain": null,
5485                    "initial_cycles": "1T", "init_arg": null, "init_candid": null,
5486                    "kind": name, "minimum_cycles": "1T", "name": name,
5487                    "parent": null, "presence": "present", "principal": name,
5488                    "replace": false, "subnet": "subnet", "wasm": "owner.wasm",
5489                })
5490            });
5491            let desired = serde_json::from_value(serde_json::json!({
5492                "bootstrap": null, "canisters": canisters, "cycles_ledger": "ledger",
5493                "environment": "local", "fleet": "owners", "ledger_fee_cycles": "100M",
5494                "management_creation_fee_cycles": "500B", "material_cycle_threshold": "1B",
5495                "maximum_observation_burn_cycles": "1B", "maximum_stalled_observations": 4,
5496                "maximum_update_burn_cycles": "1B", "operator": "operator", "protocol": null,
5497                "schema_version": 1, "treasury": "treasury",
5498            }))
5499            .unwrap();
5500            let platform = IcpEnsurePlatform::new(desired, executable.to_str().unwrap(), &root);
5501            let paths = crate::fleet_ensure::ops::EnsurePaths::under(&root, "local", "owners");
5502            let state = crate::fleet_ensure::ops::read_state(&paths, "owners").unwrap();
5503            let fixture = Self {
5504                root,
5505                platform,
5506                state,
5507            };
5508            for name in ["coordinator", "root", "store"] {
5509                fixture.status(name, "Running", true);
5510            }
5511            fixture
5512        }
5513
5514        fn status(&self, name: &str, status: &str, current_module: bool) {
5515            let module = if current_module {
5516                artifact_hash(&self.root.join("owner.wasm")).unwrap()
5517            } else {
5518                "ff".repeat(32)
5519            };
5520            std::fs::write(
5521                self.root.join(format!("{name}.json")),
5522                serde_json::json!({
5523                    "id": name, "status": status, "settings": { "controllers": [] },
5524                    "module_hash": module, "cycles": "1000000000000",
5525                })
5526                .to_string(),
5527            )
5528            .unwrap();
5529        }
5530    }
5531
5532    #[cfg(unix)]
5533    struct PoolInspectionFixture {
5534        owners: ProtocolOwnersFixture,
5535        root_id: Principal,
5536        target: Principal,
5537    }
5538
5539    #[cfg(unix)]
5540    impl PoolInspectionFixture {
5541        fn new() -> Self {
5542            let mut owners = ProtocolOwnersFixture::new();
5543            let root_id = Principal::from_slice(&[2]);
5544            let operator = Principal::from_slice(&[9]);
5545            owners.platform.desired.operator = operator.to_text();
5546            owners.platform.desired.protocol =
5547                Some(crate::fleet_ensure::model::DesiredFleetProtocol {
5548                    app_config: "unused.toml".to_string(),
5549                    component_group_placements: Vec::new(),
5550                    root_candid: "root.did".to_string(),
5551                    coordinator_candid: "unused.did".to_string(),
5552                    store_candid: "unused.did".to_string(),
5553                });
5554            std::fs::write(owners.root.join("root.did"),
5555                "service : { canic_observability : (variant { InspectionReserve : record { canister_id : principal } }) -> () query }"
5556            ).unwrap();
5557            std::fs::write(owners.root.join("operator"), operator.to_text()).unwrap();
5558            for id in [root_id, Principal::from_slice(&[3])] {
5559                std::fs::write(
5560                    owners.root.join(format!("{id}.json")),
5561                    serde_json::json!({
5562                        "id": id.to_text(), "status": "Running", "settings": {"controllers": []},
5563                        "module_hash": null, "cycles": "10000",
5564                    })
5565                    .to_string(),
5566                )
5567                .unwrap();
5568            }
5569            std::fs::write(owners.root.join("icp"), format!(
5570                "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp 1.5.0'; exit 0; fi\nwhile [ \"$#\" -gt 0 ] && [ \"$1\" != canister ] && [ \"$1\" != identity ]; do shift; done\ncase \"$1\" in\nidentity) cat '{}'/operator;;\ncanister) shift; case \"$1\" in\nstatus) shift; cat '{}'/\"$1\".json;;\ncall) if [ -e '{}'/fail ]; then exit 1; fi; cat '{}'/inspection.json;;\n*) exit 1;;\nesac;;\n*) exit 1;;\nesac\n",
5571                owners.root.display(), owners.root.display(), owners.root.display(), owners.root.display(),
5572            )).unwrap();
5573            let script = std::fs::read_to_string(owners.root.join("icp")).unwrap();
5574            let reserve_route = format!(
5575                r#"call) if [ "$3" = canic_observability ]; then
5576for expected in '{}'/reserve-"$2"-*.bin; do
5577    if cmp -s "$5" "$expected"; then cat "${{expected%.bin}}.json"; exit; fi
5578done
5579exit 1
5580fi
5581if"#,
5582                owners.root.display()
5583            );
5584            std::fs::write(
5585                owners.root.join("icp"),
5586                script.replace("call) if", &reserve_route),
5587            )
5588            .unwrap();
5589            Self::response(&owners.root, root_id, 1_000);
5590            for (caller, target) in [
5591                (root_id, Principal::from_slice(&[4])),
5592                (root_id, Principal::from_slice(&[5])),
5593                (Principal::from_slice(&[3]), Principal::from_slice(&[4])),
5594            ] {
5595                Self::reserve_response(&owners.root, caller, target, 1_000);
5596            }
5597            Self {
5598                owners,
5599                root_id,
5600                target: Principal::from_slice(&[4]),
5601            }
5602        }
5603
5604        fn reserve_response(path: &Path, caller: Principal, target: Principal, liquid: u128) {
5605            use crate::canister_protocol::inspection::{
5606                InspectionReserveRequest, InspectionReserveResponse,
5607            };
5608            let base = path.join(format!("reserve-{caller}-{target}"));
5609            std::fs::write(
5610                base.with_extension("bin"),
5611                candid::encode_one(InspectionReserveRequest::InspectionReserve(
5612                    CanisterInspectionRequest {
5613                        canister_id: target,
5614                    },
5615                ))
5616                .unwrap(),
5617            )
5618            .unwrap();
5619            let response = Ok::<_, canic_core::dto::error::Error>(
5620                InspectionReserveResponse::InspectionReserve(
5621                    canic_core::dto::canister::CanisterInspectionReserveResponse {
5622                        caller,
5623                        canister_id: target,
5624                        native_cycles: 1000,
5625                        available_liquid_cycles: liquid,
5626                        required_liquid_cycles: 100,
5627                    },
5628                ),
5629            );
5630            std::fs::write(
5631                base.with_extension("json"),
5632                serde_json::json!({
5633                    "response_bytes": hex_bytes(candid::encode_one(response).unwrap()),
5634                })
5635                .to_string(),
5636            )
5637            .unwrap();
5638        }
5639
5640        fn response(path: &Path, controller: Principal, cycles: u128) {
5641            let response = Ok::<_, canic_core::dto::error::Error>(
5642                RootInspectionResponse::InspectCanister(RootInspectionStatus {
5643                    status: canic_core::dto::canister::CanisterStatusType::Running,
5644                    settings: ManagementCanisterObservationSettings {
5645                        controllers: vec![controller],
5646                    },
5647                    module_hash: Some(vec![1; 32]),
5648                    cycles: Nat::from(cycles),
5649                }),
5650            );
5651            std::fs::write(
5652                path.join("inspection.json"),
5653                serde_json::json!({
5654                    "response_bytes": hex_bytes(candid::encode_one(response).unwrap()),
5655                })
5656                .to_string(),
5657            )
5658            .unwrap();
5659        }
5660    }
5661
5662    // Exercise preparation and completion directly when checking scope expiry.
5663    #[cfg(unix)]
5664    impl IcpEnsurePlatform {
5665        fn inspect_pending_pool_balance(
5666            &self,
5667            configured: &crate::fleet_ensure::model::DesiredCanister,
5668            principal: &str,
5669            state: &FleetEnsureStateRecord,
5670        ) -> Result<u128, IcpEnsurePlatformError> {
5671            let inspection = self.prepare_pending_pool_inspection(configured, principal, state)?;
5672            let response = self.read_pool_inspection(inspection.authority)?;
5673            self.complete_pool_inspection(inspection, response)
5674        }
5675    }
5676
5677    #[cfg(unix)]
5678    impl PoolInspectionFixture {
5679        fn fresh(pool_count: u32) -> Self {
5680            let mut fixture = Self::new();
5681            let owners = &mut fixture.owners;
5682            let operator = Principal::from_text(&owners.platform.desired.operator).unwrap();
5683            let mut root = owners.platform.desired.canisters[1].clone();
5684            root.principal = Some(fixture.root_id.to_text());
5685            root.controllers = vec![operator.to_text()];
5686            owners.platform.desired.canisters = vec![root.clone()];
5687            let config = canic_core::bootstrap::parse_config_model(
5688                "[app]\nname = 'inspection'\n[roles.root]\nkind = 'root'\n",
5689            )
5690            .unwrap();
5691            owners.platform.desired.bootstrap = Some(crate::fleet_ensure::model::DesiredFleetBootstrap {
5692            admission_identity_origin: None,
5693                admission: canic_core::shared_support::fleet_admission_policy::compile_fleet_admission_policy_template(vec![operator], Vec::new()).unwrap(),
5694                app: canic_core::ids::AppId::from("inspection"),
5695                canonical_network_id: canic_core::ids::CanonicalNetworkId::ic_mainnet(),
5696                component_deployment_configuration: config.compile_component_deployment_configuration().unwrap(),
5697                coordinator: "coordinator".into(),
5698                coordinator_subnet: canic_core::ids::SubnetId::from_principal(fixture.root_id),
5699                fleet_id: canic_core::ids::FleetId::from_generated_bytes([5; 32]),
5700                fresh_estate: true,
5701                release_build_id: canic_core::ids::ReleaseBuildId::from_nonce(canic_core::ids::ReleaseBuildNonce::from_random_bytes([7; 32])),
5702                root_funding: None,
5703                roots: Vec::new(),
5704            });
5705            std::fs::write(
5706                owners.root.join(format!("{}.json", fixture.root_id)),
5707                serde_json::json!({
5708                    "id": fixture.root_id.to_text(), "status": "Running",
5709                    "settings": {"controllers": [operator.to_text()]},
5710                    "module_hash": artifact_hash(&owners.root.join("owner.wasm")).unwrap(),
5711                    "cycles": "10000",
5712                })
5713                .to_string(),
5714            )
5715            .unwrap();
5716            let mut entries = Vec::new();
5717            for index in 0..pool_count {
5718                let target = Principal::from_slice(&[u8::try_from(index + 10).unwrap()]);
5719                Self::reserve_response(&owners.root, fixture.root_id, target, 1_000);
5720                let mut configured = root.clone();
5721                configured.name = format!("pool-{index:03}");
5722                configured.kind = DesiredCanisterKind::Pool;
5723                configured.principal = None;
5724                configured.parent = Some("root".into());
5725                owners
5726                    .state
5727                    .pending_principals
5728                    .insert(configured.name.clone(), target.to_text());
5729                owners.platform.desired.canisters.push(configured);
5730                entries.push(CanisterPoolAsset {
5731                    canister_id: target,
5732                    creation_receipt: None,
5733                    cycles: Cycles::new(0),
5734                    origin: CanisterPoolAssetOrigin::Created,
5735                    status: CanisterPoolAssetStatus::PendingReset,
5736                    added_at_ns: 1,
5737                    updated_at_ns: 1,
5738                });
5739            }
5740            fixture.target = entries[0].canister_id;
5741            Self::fresh_pool_page(&owners.root, entries);
5742            let script = std::fs::read_to_string(owners.root.join("icp")).unwrap();
5743            let script = script.replace(
5744                "call) if",
5745                &format!(
5746                    "call) case \"$*\" in *canic_root_status*) cat '{}'/pool.json; exit;; esac; if",
5747                    owners.root.display()
5748                ),
5749            );
5750            std::fs::write(owners.root.join("icp"), script).unwrap();
5751            Self::fresh_response(&owners.root, vec![fixture.root_id], None, 1_000);
5752            fixture
5753        }
5754
5755        fn fresh_pool_page(path: &Path, entries: Vec<CanisterPoolAsset>) {
5756            let pool_count = u32::try_from(entries.len()).unwrap();
5757            let page = CanisterPoolResponse {
5758                config: canic_core::ids::FleetSubnetCanisterPoolConfig {
5759                    minimum_size: 1,
5760                    maximum_size: 128,
5761                    canister_cycles: Cycles::new(1_000),
5762                    creation_execution_margin: Cycles::new(100),
5763                },
5764                tracked: pool_count,
5765                store: 0,
5766                store_deletion_pending: 0,
5767                pooled: pool_count,
5768                workload: 0,
5769                surplus: 0,
5770                ready: 0,
5771                pending_reset: pool_count,
5772                claimed: 0,
5773                recycling: 0,
5774                handing_off: 0,
5775                failed: 0,
5776                completed_handoffs: 0,
5777                pending_creation: None,
5778                pending_handoff: None,
5779                entries,
5780                next_start_after: None,
5781            };
5782            let response = Ok::<_, canic_core::dto::error::Error>(RootPoolStatusResponse::Pool(
5783                Box::new(page),
5784            ));
5785            std::fs::write(
5786                path.join("pool.json"),
5787                serde_json::json!({
5788                    "response_bytes": hex_bytes(candid::encode_one(response).unwrap()),
5789                })
5790                .to_string(),
5791            )
5792            .unwrap();
5793        }
5794
5795        fn fresh_response(
5796            path: &Path,
5797            controllers: Vec<Principal>,
5798            module_hash: Option<Vec<u8>>,
5799            cycles: u128,
5800        ) {
5801            let response = Ok::<_, canic_core::dto::error::Error>(
5802                RootInspectionResponse::InspectCanister(RootInspectionStatus {
5803                    status: canic_core::dto::canister::CanisterStatusType::Running,
5804                    settings: ManagementCanisterObservationSettings { controllers },
5805                    module_hash,
5806                    cycles: Nat::from(cycles),
5807                }),
5808            );
5809            std::fs::write(
5810                path.join("inspection.json"),
5811                serde_json::json!({
5812                    "response_bytes": hex_bytes(candid::encode_one(response).unwrap()),
5813                })
5814                .to_string(),
5815            )
5816            .unwrap();
5817        }
5818    }
5819
5820    #[cfg(unix)]
5821    impl PoolInspectionFixture {
5822        fn retained_observation(&self) -> FleetObservation {
5823            let assets = self
5824                .owners
5825                .state
5826                .pending_principals
5827                .values()
5828                .map(|principal| EstatePoolAssetObservation {
5829                    creation_receipt: None,
5830                    cycles: 500,
5831                    lifecycle: EstatePoolAssetLifecycle::Workload,
5832                    origin: EstatePoolAssetOrigin::Created,
5833                    principal: principal.clone(),
5834                })
5835                .collect::<Vec<_>>();
5836            FleetObservation {
5837                additional_controlled_cycles: assets
5838                    .iter()
5839                    .map(|asset| (asset.principal.clone(), asset.cycles))
5840                    .collect(),
5841                canisters: BTreeMap::new(),
5842                estate_funding_domains: BTreeMap::from([(
5843                    "root".into(),
5844                    EstateFundingDomainObservation {
5845                        balance_cycles: Some(0),
5846                        cycles_ledger: self.owners.platform.desired.cycles_ledger.clone(),
5847                        pool: Some(EstatePoolInventoryObservation {
5848                            assets,
5849                            maximum_size: 128,
5850                            minimum_size: 1,
5851                            pending_creation: None,
5852                            readiness_floor_cycles: 1_000,
5853                            creation_execution_margin_cycles: 100,
5854                        }),
5855                        root_principal: Some(self.root_id.to_text()),
5856                    },
5857                )]),
5858                ledger_fee_cycles: 100,
5859                operator_cycles: 1_000,
5860                protocol_ready: BTreeMap::new(),
5861            }
5862        }
5863
5864        fn inspect_retained(
5865            &self,
5866            observation: &mut FleetObservation,
5867            allow_pending: bool,
5868        ) -> Result<
5869            Vec<crate::fleet_ensure::model::FleetReinstallAssetRecord>,
5870            IcpEnsurePlatformError,
5871        > {
5872            let authorities = BTreeMap::from([(
5873                "root".into(),
5874                RootManagementCanisterObservation {
5875                    live: LiveCanister {
5876                        canister_version: None,
5877                        controllers: vec![self.owners.platform.desired.operator.clone()],
5878                        cycles: 10_000,
5879                        module_sha256: Some("01".repeat(32)),
5880                        principal: self.root_id.to_text(),
5881                        reinstall_required: false,
5882                        root_owned_lifecycle: None,
5883                        status: CanisterRuntimeStatus::Running,
5884                    },
5885                    name: "root".into(),
5886                    subnet: "subnet".into(),
5887                },
5888            )]);
5889            let candid = BTreeMap::from([(
5890                "root".into(),
5891                self.owners.platform.root_protocol_candid().unwrap(),
5892            )]);
5893            self.owners.platform.inspect_reinstall_assets(
5894                &self.owners.state,
5895                &authorities,
5896                observation,
5897                &candid,
5898                allow_pending,
5899            )
5900        }
5901
5902        fn inspection_hook(&self, hook: &str) {
5903            let path = &self.owners.root;
5904            let script = std::fs::read_to_string(path.join("icp")).unwrap();
5905            let response = format!("cat '{}'/inspection.json", path.display());
5906            assert!(script.contains(&response));
5907            std::fs::write(
5908                path.join("icp"),
5909                script.replace(&response, &format!("{hook}\n{response}")),
5910            )
5911            .unwrap();
5912        }
5913
5914        fn route_inspection_targets(&self) {
5915            let path = &self.owners.root;
5916            for configured in self
5917                .owners
5918                .platform
5919                .desired
5920                .canisters
5921                .iter()
5922                .filter(|canister| canister.kind == DesiredCanisterKind::Pool)
5923            {
5924                let target = &self.owners.state.pending_principals[&configured.name];
5925                let request = RootInspectionCommand::InspectCanister(CanisterInspectionRequest {
5926                    canister_id: Principal::from_text(target).unwrap(),
5927                });
5928                std::fs::write(
5929                    path.join(format!("request-{}.bin", configured.name)),
5930                    candid::encode_one(request).unwrap(),
5931                )
5932                .unwrap();
5933            }
5934            self.inspection_hook(&format!(
5935                r#"
5936cd '{}'
5937request_name=
5938for expected in request-*.bin; do
5939    if cmp -s "$5" "$expected"; then
5940        request_name=${{expected#request-}}
5941        request_name=${{request_name%.bin}}
5942        break
5943    fi
5944done
5945[ -n "$request_name" ] || exit 1
5946printf 'start:%s\n' "$request_name" >> events
5947if [ -f "$request_name.fail" ]; then
5948    printf 'finish:%s\n' "$request_name" >> events
5949    exit 1
5950fi
5951if [ -f "$request_name.response" ]; then
5952    attempts=0
5953    while ! grep -q '^finish:pool-001$' events; do
5954        attempts=$((attempts + 1))
5955        [ "$attempts" -lt 500 ] || exit 1
5956        sleep 0.01
5957    done
5958    printf 'finish:%s\n' "$request_name" >> events
5959    cat "$request_name.response"
5960    exit
5961fi
5962printf 'finish:%s\n' "$request_name" >> events
5963"#,
5964                path.display()
5965            ));
5966        }
5967    }
5968
5969    #[cfg(unix)]
5970    #[test]
5971    fn reinstall_inspections_overlap_and_retain_exact_inventory() {
5972        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
5973        let count = bound * 2 + 1;
5974        let fixture = PoolInspectionFixture::fresh(u32::try_from(count).unwrap());
5975        let path = &fixture.owners.root;
5976        fixture.inspection_hook(&format!(
5977            r#"
5978cd '{}'
5979printf 'start\n' >> events
5980started=$(grep -c '^start$' events)
5981goal=$(( (started + {bound} - 1) / {bound} * {bound} ))
5982[ "$goal" -le {count} ] || goal={count}
5983attempts=0
5984while [ "$(grep -c '^start$' events)" -lt "$goal" ]; do
5985    attempts=$((attempts + 1))
5986    [ "$attempts" -lt 500 ] || exit 1
5987    sleep 0.01
5988done
5989printf 'finish\n' >> events
5990"#,
5991            path.display()
5992        ));
5993        let mut observation = fixture.retained_observation();
5994        let assets = fixture.inspect_retained(&mut observation, false).unwrap();
5995        assert_eq!(assets.len(), count);
5996        assert!(
5997            assets
5998                .windows(2)
5999                .all(|pair| pair[0].principal < pair[1].principal)
6000        );
6001        assert!(
6002            assets
6003                .iter()
6004                .all(|asset| asset.controllers == [fixture.root_id.to_text()]
6005                    && asset.module_sha256.is_none())
6006        );
6007        assert_eq!(observation.canisters.len(), count);
6008        assert!(observation.additional_controlled_cycles.is_empty());
6009        assert!(
6010            observation
6011                .canisters
6012                .values()
6013                .all(|live| live.as_ref().unwrap().cycles == 1_000)
6014        );
6015        let (mut active, mut maximum) = (0, 0);
6016        for event in std::fs::read_to_string(path.join("events"))
6017            .unwrap()
6018            .lines()
6019        {
6020            match event {
6021                "start" => {
6022                    active += 1;
6023                    maximum = maximum.max(active);
6024                }
6025                "finish" => {
6026                    assert!(active > 0);
6027                    active -= 1;
6028                }
6029                _ => panic!("unknown inspection event"),
6030            }
6031            assert!(active <= bound);
6032        }
6033        assert_eq!((active, maximum), (0, bound));
6034        assert_eq!(
6035            fixture.owners.platform.icp.remote_call_count(),
6036            2 * u64::try_from(count).unwrap()
6037        );
6038        std::fs::remove_dir_all(path).unwrap();
6039    }
6040
6041    #[cfg(unix)]
6042    #[test]
6043    fn reinstall_inspections_drain_failure_in_order_and_retry_fresh() {
6044        let fixture = PoolInspectionFixture::fresh(5);
6045        let path = &fixture.owners.root;
6046        PoolInspectionFixture::fresh_response(
6047            path,
6048            vec![fixture.root_id, Principal::anonymous()],
6049            None,
6050            1_000,
6051        );
6052        std::fs::copy(path.join("inspection.json"), path.join("pool-000.response")).unwrap();
6053        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 2_000);
6054        std::fs::write(path.join("pool-001.fail"), []).unwrap();
6055        fixture.route_inspection_targets();
6056        let mut observation = fixture.retained_observation();
6057        let before = observation.clone();
6058        assert!(
6059            matches!(fixture.inspect_retained(&mut observation, false), Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { field: "exact Root controllers", canister }) if canister == fixture.target.to_text())
6060        );
6061        assert_eq!(observation, before);
6062        let events = std::fs::read_to_string(path.join("events")).unwrap();
6063        for index in 0..4 {
6064            assert!(events.contains(&format!("finish:pool-{index:03}")));
6065        }
6066        assert!(!events.contains("pool-004"));
6067        assert!(events.find("finish:pool-001").unwrap() < events.find("finish:pool-000").unwrap());
6068        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 8);
6069        std::fs::remove_file(path.join("pool-000.response")).unwrap();
6070        std::fs::remove_file(path.join("pool-001.fail")).unwrap();
6071        for cycles in [2_000, 3_000] {
6072            PoolInspectionFixture::fresh_response(
6073                path,
6074                vec![fixture.root_id],
6075                Some(vec![1; 32]),
6076                cycles,
6077            );
6078            let assets = fixture.inspect_retained(&mut observation, false).unwrap();
6079            assert_eq!(assets.len(), 5);
6080            assert!(
6081                assets
6082                    .iter()
6083                    .all(|asset| asset.module_sha256.as_deref() == Some(&"01".repeat(32)))
6084            );
6085            assert!(
6086                observation
6087                    .canisters
6088                    .values()
6089                    .all(|live| live.as_ref().unwrap().cycles == cycles)
6090            );
6091        }
6092        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 28);
6093        std::fs::remove_dir_all(path).unwrap();
6094    }
6095
6096    #[cfg(unix)]
6097    #[test]
6098    fn reinstall_inspections_reject_duplicates_and_preserve_lifecycle_admission() {
6099        let fixture = PoolInspectionFixture::fresh(5);
6100        let mut observation = fixture.retained_observation();
6101        let pool = observation
6102            .estate_funding_domains
6103            .get_mut("root")
6104            .unwrap()
6105            .pool
6106            .as_mut()
6107            .unwrap();
6108        pool.assets[4].principal = pool.assets[0].principal.clone();
6109        assert!(matches!(
6110            fixture.inspect_retained(&mut observation, false),
6111            Err(IcpEnsurePlatformError::RootManagement(_))
6112        ));
6113        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 8);
6114        for lifecycle in [
6115            EstatePoolAssetLifecycle::PendingReset,
6116            EstatePoolAssetLifecycle::Failed,
6117            EstatePoolAssetLifecycle::Claimed,
6118        ] {
6119            let mut observation = fixture.retained_observation();
6120            let pool = observation
6121                .estate_funding_domains
6122                .get_mut("root")
6123                .unwrap()
6124                .pool
6125                .as_mut()
6126                .unwrap();
6127            pool.assets.truncate(1);
6128            pool.assets[0].lifecycle = lifecycle;
6129            let before = fixture.owners.platform.icp.remote_call_count();
6130            assert!(matches!(
6131                fixture.inspect_retained(&mut observation, false),
6132                Err(IcpEnsurePlatformError::RootManagement(_))
6133            ));
6134            assert_eq!(fixture.owners.platform.icp.remote_call_count(), before);
6135            let result = fixture.inspect_retained(&mut observation, true);
6136            if lifecycle == EstatePoolAssetLifecycle::Claimed {
6137                assert!(matches!(
6138                    result,
6139                    Err(IcpEnsurePlatformError::RootManagement(_))
6140                ));
6141                assert_eq!(fixture.owners.platform.icp.remote_call_count(), before);
6142            } else {
6143                assert_eq!(result.unwrap().len(), 1);
6144                assert_eq!(fixture.owners.platform.icp.remote_call_count(), before + 2);
6145            }
6146        }
6147        std::fs::remove_dir_all(&fixture.owners.root).unwrap();
6148    }
6149
6150    #[cfg(unix)]
6151    #[test]
6152    fn reinstall_inspections_require_fresh_reserve_even_with_cached_evidence() {
6153        let fixture = PoolInspectionFixture::fresh(1);
6154        let platform = &fixture.owners.platform;
6155        *platform.observation_snapshot.borrow_mut() = Some(FleetObservationSnapshot::default());
6156        platform.retain_pool_inspection(
6157            PoolInspectionAuthority {
6158                root: fixture.root_id,
6159                target: fixture.target,
6160            },
6161            RootInspectionStatus {
6162                status: canic_core::dto::canister::CanisterStatusType::Running,
6163                settings: ManagementCanisterObservationSettings {
6164                    controllers: vec![fixture.root_id],
6165                },
6166                module_hash: None,
6167                cycles: Nat::from(9_999_u64),
6168            },
6169        );
6170        let mut observation = fixture.retained_observation();
6171        PoolInspectionFixture::reserve_response(
6172            &fixture.owners.root,
6173            fixture.root_id,
6174            fixture.target,
6175            0,
6176        );
6177        assert!(matches!(fixture.inspect_retained(&mut observation, false),
6178            Err(IcpEnsurePlatformError::CurrentProtocol(current_protocol::CurrentProtocolError::Transport(
6179                CanisterProtocolError::InspectionPreflightReserve(evidence)
6180            ))) if evidence.canister_id == fixture.target && evidence.caller == fixture.root_id
6181        ));
6182        assert_eq!(platform.icp.remote_call_count(), 1);
6183        PoolInspectionFixture::reserve_response(
6184            &fixture.owners.root,
6185            fixture.root_id,
6186            fixture.target,
6187            1_000,
6188        );
6189        fixture.inspect_retained(&mut observation, false).unwrap();
6190        assert_eq!(
6191            observation.canisters["pool-000"].as_ref().unwrap().cycles,
6192            1_000
6193        );
6194        assert_eq!(platform.icp.remote_call_count(), 3);
6195        std::fs::remove_dir_all(&fixture.owners.root).unwrap();
6196    }
6197
6198    #[cfg(unix)]
6199    #[test]
6200    fn pool_balances_overlap_only_pending_and_failed_assets_within_the_bound() {
6201        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
6202        let count = bound * 2 + 1;
6203        let mut fixture = PoolInspectionFixture::fresh(u32::try_from(count + 1).unwrap());
6204        let path = &fixture.owners.root;
6205        fixture.inspection_hook(&format!(
6206            r#"
6207cd '{}'
6208printf 'start\n' >> events
6209started=$(grep -c '^start$' events)
6210goal=$(( (started + {bound} - 1) / {bound} * {bound} ))
6211[ "$goal" -le {count} ] || goal={count}
6212attempts=0
6213while [ "$(grep -c '^start$' events)" -lt "$goal" ]; do
6214    attempts=$((attempts + 1))
6215    [ "$attempts" -lt 500 ] || exit 1
6216    sleep 0.01
6217done
6218printf 'finish\n' >> events
6219"#,
6220            path.display()
6221        ));
6222        let mut observed = fixture.retained_observation();
6223        let assets = &mut observed
6224            .estate_funding_domains
6225            .get_mut("root")
6226            .unwrap()
6227            .pool
6228            .as_mut()
6229            .unwrap()
6230            .assets;
6231        for (index, asset) in assets.iter_mut().take(count).enumerate() {
6232            asset.lifecycle = if index % 2 == 0 {
6233                EstatePoolAssetLifecycle::Failed
6234            } else {
6235                EstatePoolAssetLifecycle::PendingReset
6236            };
6237        }
6238        fixture
6239            .owners
6240            .platform
6241            .with_observation_snapshot(|platform| {
6242                platform.refresh_pool_balances(&fixture.root_id.to_text(), assets)
6243            })
6244            .unwrap();
6245        assert!(assets[..count].iter().all(|asset| asset.cycles == 1_000));
6246        assert_eq!(assets[count].cycles, 500);
6247        let mut active = 0;
6248        let mut maximum = 0;
6249        for event in std::fs::read_to_string(path.join("events"))
6250            .unwrap()
6251            .lines()
6252        {
6253            match event {
6254                "start" => {
6255                    active += 1;
6256                    maximum = maximum.max(active);
6257                }
6258                "finish" => {
6259                    assert!(active > 0);
6260                    active -= 1;
6261                }
6262                _ => panic!("unknown inspection event"),
6263            }
6264            assert!(active <= bound);
6265        }
6266        assert_eq!((active, maximum), (0, bound));
6267        assert_eq!(
6268            fixture.owners.platform.icp.remote_call_count(),
6269            2 * u64::try_from(count).unwrap() + 1
6270        );
6271        std::fs::remove_dir_all(path).unwrap();
6272    }
6273
6274    #[cfg(unix)]
6275    #[test]
6276    fn pool_balances_drain_failed_batch_preserve_inventory_and_retry_fresh() {
6277        let mut fixture = PoolInspectionFixture::fresh(5);
6278        let path = &fixture.owners.root;
6279        PoolInspectionFixture::fresh_response(path, vec![Principal::from_slice(&[99])], None, 900);
6280        std::fs::copy(path.join("inspection.json"), path.join("pool-000.response")).unwrap();
6281        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 1_000);
6282        std::fs::write(path.join("pool-001.fail"), []).unwrap();
6283        fixture.route_inspection_targets();
6284        let mut observed = fixture.retained_observation();
6285        let assets = &mut observed
6286            .estate_funding_domains
6287            .get_mut("root")
6288            .unwrap()
6289            .pool
6290            .as_mut()
6291            .unwrap()
6292            .assets;
6293        for asset in assets.iter_mut() {
6294            asset.lifecycle = EstatePoolAssetLifecycle::Failed;
6295        }
6296        let unchanged = assets.clone();
6297        let failed = fixture
6298            .owners
6299            .platform
6300            .with_observation_snapshot(|platform| {
6301                platform.refresh_pool_balances(&fixture.root_id.to_text(), assets)
6302            });
6303        assert!(
6304            matches!(failed, Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { canister, field: "Root-only controllers" }) if canister == unchanged[0].principal)
6305        );
6306        assert_eq!(*assets, unchanged);
6307        let events = std::fs::read_to_string(path.join("events")).unwrap();
6308        for index in 0..4 {
6309            assert!(events.contains(&format!("finish:pool-{index:03}")));
6310        }
6311        assert!(!events.contains("pool-004"));
6312        assert!(events.find("finish:pool-001").unwrap() < events.find("finish:pool-000").unwrap());
6313        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 9);
6314        std::fs::remove_file(path.join("pool-000.response")).unwrap();
6315        std::fs::remove_file(path.join("pool-001.fail")).unwrap();
6316        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 2_000);
6317        fixture
6318            .owners
6319            .platform
6320            .with_observation_snapshot(|platform| {
6321                platform.refresh_pool_balances(&fixture.root_id.to_text(), assets)
6322            })
6323            .unwrap();
6324        assert!(assets.iter().all(|asset| asset.cycles == 2_000));
6325        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 20);
6326        assert!(
6327            fixture
6328                .owners
6329                .platform
6330                .observation_snapshot
6331                .borrow()
6332                .is_none()
6333        );
6334        std::fs::remove_dir_all(path).unwrap();
6335    }
6336
6337    #[cfg(unix)]
6338    #[test]
6339    fn configured_pool_inspections_overlap_within_the_bound_and_drain_partial_batches() {
6340        let bound = crate::fleet_ensure::ops::bounded_observations::MAX_IN_FLIGHT;
6341        let pool_count = bound * 2 + 1;
6342        let mut fixture = PoolInspectionFixture::fresh(u32::try_from(pool_count).unwrap());
6343        let path = &fixture.owners.root;
6344        fixture.inspection_hook(&format!(
6345            r#"
6346cd '{}'
6347printf 'start\n' >> events
6348started=$(grep -c '^start$' events)
6349goal=$(( (started + {bound} - 1) / {bound} * {bound} ))
6350[ "$goal" -le {pool_count} ] || goal={pool_count}
6351attempts=0
6352while [ "$(grep -c '^start$' events)" -lt "$goal" ]; do
6353    attempts=$((attempts + 1))
6354    [ "$attempts" -lt 500 ] || exit 1
6355    sleep 0.01
6356done
6357printf 'finish\n' >> events
6358"#,
6359            path.display()
6360        ));
6361        let observed = fixture
6362            .owners
6363            .platform
6364            .with_observation_snapshot(|platform| {
6365                platform.observe_configured_canisters(&fixture.owners.state)
6366            })
6367            .unwrap();
6368        assert_eq!(
6369            observed.len(),
6370            fixture.owners.platform.desired.canisters.len()
6371        );
6372        let events = std::fs::read_to_string(path.join("events")).unwrap();
6373        let mut active = 0;
6374        let mut maximum = 0;
6375        for event in events.lines() {
6376            match event {
6377                "start" => {
6378                    active += 1;
6379                    maximum = maximum.max(active);
6380                }
6381                "finish" => {
6382                    assert!(active > 0);
6383                    active -= 1;
6384                }
6385                _ => panic!("unknown fixture event"),
6386            }
6387            assert!(active <= bound);
6388        }
6389        assert_eq!(active, 0);
6390        assert_eq!(maximum, bound);
6391        assert_eq!(
6392            fixture.owners.platform.icp.remote_call_count(),
6393            2 * u64::try_from(pool_count).unwrap() + 2
6394        );
6395        assert!(
6396            fixture
6397                .owners
6398                .platform
6399                .observation_snapshot
6400                .borrow()
6401                .is_none()
6402        );
6403        std::fs::remove_dir_all(path).unwrap();
6404    }
6405
6406    #[cfg(unix)]
6407    #[test]
6408    fn pool_batch_keeps_consumer_error_before_later_transport_error_and_retries_fresh() {
6409        let mut fixture = PoolInspectionFixture::fresh(5);
6410        let path = &fixture.owners.root;
6411        PoolInspectionFixture::fresh_response(
6412            path,
6413            vec![fixture.root_id],
6414            Some(vec![1; 32]),
6415            1_000,
6416        );
6417        std::fs::copy(path.join("inspection.json"), path.join("pool-000.response")).unwrap();
6418        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 1_000);
6419        std::fs::write(path.join("pool-001.fail"), []).unwrap();
6420        fixture.route_inspection_targets();
6421        let failed = fixture
6422            .owners
6423            .platform
6424            .with_observation_snapshot(|platform| {
6425                platform.observe_configured_canisters(&fixture.owners.state)
6426            });
6427        assert!(
6428            matches!(failed, Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { canister, field: "module-free pool asset" }) if canister == "pool-000")
6429        );
6430        let events = std::fs::read_to_string(path.join("events")).unwrap();
6431        for index in 0..4 {
6432            assert!(events.contains(&format!("finish:pool-{index:03}")));
6433        }
6434        assert!(!events.contains("pool-004"));
6435        assert!(events.find("finish:pool-001").unwrap() < events.find("finish:pool-000").unwrap());
6436        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 10);
6437        assert!(
6438            fixture
6439                .owners
6440                .platform
6441                .observation_snapshot
6442                .borrow()
6443                .is_none()
6444        );
6445        std::fs::remove_file(path.join("pool-000.response")).unwrap();
6446        std::fs::remove_file(path.join("pool-001.fail")).unwrap();
6447        let observed = fixture
6448            .owners
6449            .platform
6450            .with_observation_snapshot(|platform| {
6451                platform.observe_configured_canisters(&fixture.owners.state)
6452            })
6453            .unwrap();
6454        assert_eq!(observed.len(), 6);
6455        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 22);
6456        std::fs::remove_dir_all(path).unwrap();
6457    }
6458
6459    #[cfg(unix)]
6460    #[test]
6461    fn pool_batch_preserves_preparation_and_transport_error_precedence() {
6462        for invalid_index in [1, 2] {
6463            let mut fixture = PoolInspectionFixture::fresh(5);
6464            let path = &fixture.owners.root;
6465            // Index zero is the Root; reject authority for exactly one pool request.
6466            fixture.owners.platform.desired.canisters[invalid_index].parent =
6467                Some("missing-root".into());
6468            fixture.route_inspection_targets();
6469            for index in 0..4 {
6470                std::fs::write(path.join(format!("pool-{index:03}.fail")), []).unwrap();
6471            }
6472            let failed = fixture
6473                .owners
6474                .platform
6475                .with_observation_snapshot(|platform| {
6476                    platform.observe_configured_canisters(&fixture.owners.state)
6477                });
6478            if invalid_index == 1 {
6479                assert!(matches!(
6480                    failed,
6481                    Err(IcpEnsurePlatformError::CurrentProtocol(
6482                        current_protocol::CurrentProtocolError::Configuration(_)
6483                    ))
6484                ));
6485            } else {
6486                assert!(matches!(
6487                    failed,
6488                    Err(IcpEnsurePlatformError::CurrentProtocol(
6489                        current_protocol::CurrentProtocolError::Transport(_)
6490                    ))
6491                ));
6492            }
6493            let events = std::fs::read_to_string(path.join("events")).unwrap();
6494            assert_eq!(
6495                events
6496                    .lines()
6497                    .filter(|line| line.starts_with("finish:"))
6498                    .count(),
6499                3
6500            );
6501            assert!(!events.contains(&format!("pool-{:03}", invalid_index - 1)));
6502            assert!(!events.contains("pool-004"));
6503            assert_eq!(fixture.owners.platform.icp.remote_call_count(), 8);
6504            assert!(
6505                fixture
6506                    .owners
6507                    .platform
6508                    .observation_snapshot
6509                    .borrow()
6510                    .is_none()
6511            );
6512            std::fs::remove_dir_all(path).unwrap();
6513        }
6514    }
6515
6516    #[cfg(unix)]
6517    #[test]
6518    fn pool_batch_benchmark_matches_serial_observation_with_bounded_reads() {
6519        for pool_count in [1, 14, 27] {
6520            let mut serial_observed = None;
6521            for concurrent in [false, true] {
6522                let mut fixture = PoolInspectionFixture::fresh(pool_count);
6523                fixture.inspection_hook("sleep 0.02");
6524                let observed = fixture.owners.platform.with_observation_snapshot(|platform| {
6525                    let started = std::time::Instant::now();
6526                    let observed = if concurrent {
6527                        platform.observe_configured_canisters(&fixture.owners.state)?
6528                    } else {
6529                        platform.desired.canisters.iter().map(|configured| {
6530                            let principal = platform.current_principal(&fixture.owners.state, &configured.name).unwrap();
6531                            platform.observe_configured_canister(configured, principal, &fixture.owners.state).map(|live| (configured.name.clone(), live))
6532                        }).collect::<Result<BTreeMap<_, _>, _>>()?
6533                    };
6534                    let configured_ms = started.elapsed().as_millis();
6535                    let configured_calls = platform.icp.remote_call_count();
6536                    for live in observed.values().filter_map(Option::as_ref).filter(|live| live.principal != fixture.root_id.to_text()) {
6537                        assert_eq!(platform.inspect_pool_balance("pool", &fixture.root_id.to_text(), &live.principal, InspectedModule::Any)?, live.cycles);
6538                    }
6539                    assert_eq!(platform.icp.remote_call_count(), configured_calls);
6540                    assert_eq!(configured_calls, 2 * u64::from(pool_count) + 2);
6541                    println!("pool_batch_benchmark assets={pool_count} concurrent={concurrent} configured_ms={configured_ms} configured_calls={configured_calls} extra_balance_calls=0 latency_ms=20");
6542                    Ok(observed)
6543                }).unwrap();
6544                if let Some(serial) = &serial_observed {
6545                    assert_eq!(&observed, serial);
6546                } else {
6547                    serial_observed = Some(observed);
6548                }
6549                std::fs::remove_dir_all(&fixture.owners.root).unwrap();
6550            }
6551        }
6552    }
6553
6554    #[cfg(unix)]
6555    #[test]
6556    fn fresh_pool_observation_shares_inspections_with_balance_review() {
6557        let mut redundant_calls = 0;
6558        for pool_count in [1, 14, 27] {
6559            let mut fixture = PoolInspectionFixture::fresh(pool_count);
6560            let root = fixture.root_id.to_text();
6561            let path = &fixture.owners.root;
6562            fixture.owners.platform.with_observation_snapshot(|platform| {
6563                let started = std::time::Instant::now();
6564                let observed = platform.observe_configured_canisters(&fixture.owners.state)?;
6565                let configured_elapsed = started.elapsed();
6566                let configured_calls = platform.icp.remote_call_count();
6567                assert_eq!(observed.len(), usize::try_from(pool_count).unwrap() + 1);
6568                assert_eq!(configured_calls, 2 * u64::from(pool_count) + 2);
6569                let started = std::time::Instant::now();
6570                for live in observed.values().filter_map(Option::as_ref).filter(|live| live.principal != root) {
6571                    assert_eq!(live.cycles, 1_000);
6572                    assert_eq!(platform.inspect_pool_balance("pool", &root, &live.principal, InspectedModule::Any)?, 1_000);
6573                }
6574                let balance_calls = platform.icp.remote_call_count() - configured_calls;
6575                println!("fresh_pool_observation assets={pool_count} configured_ms={} configured_calls={configured_calls} balance_ms={} balance_calls={balance_calls}", configured_elapsed.as_millis(), started.elapsed().as_millis());
6576                redundant_calls += balance_calls;
6577                Ok(())
6578            }).unwrap();
6579            PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 900);
6580            let configured = &fixture.owners.platform.desired.canisters[1];
6581            assert_eq!(
6582                fixture
6583                    .owners
6584                    .platform
6585                    .inspect_pending_pool_balance(
6586                        configured,
6587                        &fixture.target.to_text(),
6588                        &fixture.owners.state
6589                    )
6590                    .unwrap(),
6591                900
6592            );
6593            std::fs::remove_dir_all(path).unwrap();
6594        }
6595        assert_eq!(redundant_calls, 0);
6596    }
6597
6598    #[cfg(unix)]
6599    #[test]
6600    fn fresh_pool_inspection_preserves_consumer_authority_and_failed_scope_expiry() {
6601        let mut fixture = PoolInspectionFixture::fresh(1);
6602        let root = fixture.root_id.to_text();
6603        let target = fixture.target.to_text();
6604        let operator = Principal::from_text(&fixture.owners.platform.desired.operator).unwrap();
6605        let configured = fixture.owners.platform.desired.canisters[1].clone();
6606        let path = &fixture.owners.root;
6607        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id, operator], None, 1_000);
6608        let failed = fixture
6609            .owners
6610            .platform
6611            .with_observation_snapshot(|platform| {
6612                assert_eq!(
6613                    platform.inspect_pending_pool_balance(
6614                        &configured,
6615                        &target,
6616                        &fixture.owners.state
6617                    )?,
6618                    1_000
6619                );
6620                let calls = platform.icp.remote_call_count();
6621                assert!(matches!(
6622                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any),
6623                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
6624                        field: "Root-only controllers",
6625                        ..
6626                    })
6627                ));
6628                assert_eq!(platform.icp.remote_call_count(), calls);
6629                std::fs::write(path.join("operator"), Principal::from_slice(&[8]).to_text())
6630                    .unwrap();
6631                assert!(matches!(
6632                    platform.inspect_pending_pool_balance(
6633                        &configured,
6634                        &target,
6635                        &fixture.owners.state
6636                    ),
6637                    Err(IcpEnsurePlatformError::OperatorMismatch { .. })
6638                ));
6639                Err::<(), _>(IcpEnsurePlatformError::Arithmetic(
6640                    "injected observation failure",
6641                ))
6642            });
6643        assert!(matches!(failed, Err(IcpEnsurePlatformError::Arithmetic(_))));
6644        assert!(
6645            fixture
6646                .owners
6647                .platform
6648                .observation_snapshot
6649                .borrow()
6650                .is_none()
6651        );
6652        std::fs::write(path.join("operator"), operator.to_text()).unwrap();
6653        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], Some(vec![1; 32]), 900);
6654        fixture
6655            .owners
6656            .platform
6657            .with_observation_snapshot(|platform| {
6658                assert_eq!(
6659                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
6660                    900
6661                );
6662                assert!(matches!(
6663                    platform.inspect_pending_pool_balance(
6664                        &configured,
6665                        &target,
6666                        &fixture.owners.state
6667                    ),
6668                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
6669                        field: "module-free pool asset",
6670                        ..
6671                    })
6672                ));
6673                Ok(())
6674            })
6675            .unwrap();
6676        std::fs::remove_dir_all(path).unwrap();
6677    }
6678
6679    #[cfg(unix)]
6680    #[test]
6681    fn fresh_pool_inspection_retries_failures_and_rechecks_root_authority_on_cache_hits() {
6682        let mut fixture = PoolInspectionFixture::fresh(1);
6683        let target = fixture.target.to_text();
6684        let root = fixture.root_id.to_text();
6685        let configured = fixture.owners.platform.desired.canisters[1].clone();
6686        let path = &fixture.owners.root;
6687        fixture
6688            .owners
6689            .platform
6690            .with_observation_snapshot(|platform| {
6691                let inspect = |platform: &IcpEnsurePlatform| {
6692                    platform.inspect_pending_pool_balance(
6693                        &configured,
6694                        &target,
6695                        &fixture.owners.state,
6696                    )
6697                };
6698                std::fs::write(path.join("fail"), []).unwrap();
6699                assert!(matches!(
6700                    inspect(platform),
6701                    Err(IcpEnsurePlatformError::CurrentProtocol(
6702                        current_protocol::CurrentProtocolError::Transport(_)
6703                    ))
6704                ));
6705                std::fs::remove_file(path.join("fail")).unwrap();
6706                PoolInspectionFixture::fresh_response(
6707                    path,
6708                    vec![Principal::from_slice(&[8])],
6709                    None,
6710                    900,
6711                );
6712                assert!(matches!(
6713                    inspect(platform),
6714                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
6715                        field: "reviewed fresh pool controllers",
6716                        ..
6717                    })
6718                ));
6719                PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 800);
6720                assert_eq!(inspect(platform)?, 800);
6721                assert_eq!(platform.icp.remote_call_count(), 7);
6722                // Funding accepts the same response, but fresh observation must still
6723                // establish its own exact Root artifact and controller authority.
6724                assert_eq!(
6725                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
6726                    800
6727                );
6728                std::fs::write(path.join("owner.wasm"), b"changed-root-artifact").unwrap();
6729                assert!(matches!(
6730                    inspect(platform),
6731                    Err(
6732                        IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
6733                            field: "Root module SHA-256",
6734                            ..
6735                        }
6736                    )
6737                ));
6738                std::fs::write(path.join("owner.wasm"), b"current-owner-module").unwrap();
6739                platform.desired.canisters[0].controllers =
6740                    vec![Principal::from_slice(&[8]).to_text()];
6741                assert!(matches!(
6742                    inspect(platform),
6743                    Err(
6744                        IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
6745                            field: "Root controllers",
6746                            ..
6747                        }
6748                    )
6749                ));
6750                Ok(())
6751            })
6752            .unwrap();
6753        std::fs::remove_dir_all(path).unwrap();
6754    }
6755
6756    #[cfg(unix)]
6757    #[test]
6758    fn pool_inspection_honors_bound_source_contract_without_bypassing_query_failures() {
6759        let fixture = PoolInspectionFixture::new();
6760        let root = fixture.root_id.to_text();
6761        let target = fixture.target.to_text();
6762        let path = &fixture.owners.root;
6763        let platform = &fixture.owners.platform;
6764        let candid_path = path.join("root.did");
6765        let current = std::fs::read_to_string(&candid_path).unwrap();
6766        std::fs::write(
6767            &candid_path,
6768            "service : { canic_observability : (variant { CycleBalance }) -> () query }",
6769        )
6770        .unwrap();
6771        let inspect =
6772            || platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any);
6773        assert_eq!(inspect().unwrap(), 1000);
6774        // Source observation still checks Root authority and executes protected inspection.
6775        assert_eq!(platform.icp.remote_call_count(), 2);
6776        std::fs::write(&candid_path, &current).unwrap();
6777        PoolInspectionFixture::reserve_response(path, fixture.root_id, fixture.target, 99);
6778        let before = platform.icp.remote_call_count();
6779        assert!(matches!(
6780            inspect(),
6781            Err(IcpEnsurePlatformError::CurrentProtocol(
6782                current_protocol::CurrentProtocolError::Transport(
6783                    CanisterProtocolError::InspectionPreflightReserve(_)
6784                )
6785            ))
6786        ));
6787        assert_eq!(platform.icp.remote_call_count() - before, 2);
6788        // A declared query that fails cannot fall through to the inspection update.
6789        std::fs::write(
6790            path.join(format!(
6791                "reserve-{}-{}.json",
6792                fixture.root_id, fixture.target
6793            )),
6794            "invalid response",
6795        )
6796        .unwrap();
6797        let before = platform.icp.remote_call_count();
6798        assert!(matches!(
6799            inspect(),
6800            Err(IcpEnsurePlatformError::CurrentProtocol(
6801                current_protocol::CurrentProtocolError::Transport(
6802                    CanisterProtocolError::Response { .. }
6803                )
6804            ))
6805        ));
6806        assert_eq!(platform.icp.remote_call_count() - before, 2);
6807        std::fs::write(&candid_path, "invalid Candid").unwrap();
6808        let before = platform.icp.remote_call_count();
6809        assert!(matches!(
6810            inspect(),
6811            Err(IcpEnsurePlatformError::CurrentProtocol(
6812                current_protocol::CurrentProtocolError::Transport(
6813                    CanisterProtocolError::InspectionContract { .. }
6814                )
6815            ))
6816        ));
6817        assert_eq!(platform.icp.remote_call_count() - before, 1);
6818        std::fs::write(&candid_path, current).unwrap();
6819        PoolInspectionFixture::reserve_response(path, fixture.root_id, fixture.target, 1000);
6820        let before = platform.icp.remote_call_count();
6821        assert_eq!(inspect().unwrap(), 1000);
6822        assert_eq!(platform.icp.remote_call_count() - before, 3);
6823        std::fs::remove_dir_all(path).unwrap();
6824    }
6825
6826    #[cfg(unix)]
6827    #[test]
6828    fn pool_inspection_snapshot_expires_after_success_and_failure() {
6829        let mut fixture = PoolInspectionFixture::new();
6830        let root = fixture.root_id.to_text();
6831        let target = fixture.target.to_text();
6832        let path = &fixture.owners.root;
6833        let inspect = |platform: &IcpEnsurePlatform| {
6834            platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)
6835        };
6836        fixture
6837            .owners
6838            .platform
6839            .with_observation_snapshot(|platform| {
6840                assert_eq!(inspect(platform)?, 1_000);
6841                PoolInspectionFixture::response(path, fixture.root_id, 900);
6842                assert_eq!(inspect(platform)?, 1_000);
6843                // Root status, reserve preflight and inspection serve both projections.
6844                assert_eq!(platform.icp.remote_call_count(), 3);
6845                Ok(())
6846            })
6847            .unwrap();
6848        assert!(
6849            fixture
6850                .owners
6851                .platform
6852                .observation_snapshot
6853                .borrow()
6854                .is_none()
6855        );
6856        assert_eq!(inspect(&fixture.owners.platform).unwrap(), 900);
6857        let failed: Result<(), IcpEnsurePlatformError> = fixture
6858            .owners
6859            .platform
6860            .with_observation_snapshot(|platform| {
6861                assert_eq!(inspect(platform)?, 900);
6862                PoolInspectionFixture::response(path, fixture.root_id, 800);
6863                Err(IcpEnsurePlatformError::Arithmetic(
6864                    "injected observation failure",
6865                ))
6866            });
6867        assert!(matches!(failed, Err(IcpEnsurePlatformError::Arithmetic(_))));
6868        assert!(
6869            fixture
6870                .owners
6871                .platform
6872                .observation_snapshot
6873                .borrow()
6874                .is_none()
6875        );
6876        assert_eq!(inspect(&fixture.owners.platform).unwrap(), 800);
6877        fixture
6878            .owners
6879            .platform
6880            .with_observation_snapshot(|platform| {
6881                assert_eq!(inspect(platform)?, 800);
6882                Ok(())
6883            })
6884            .unwrap();
6885        std::fs::remove_dir_all(path).unwrap();
6886    }
6887
6888    #[cfg(unix)]
6889    #[test]
6890    fn pool_inspection_snapshot_binds_root_target_module_and_operator() {
6891        let mut fixture = PoolInspectionFixture::new();
6892        let root = fixture.root_id.to_text();
6893        let target = fixture.target.to_text();
6894        let other_root = Principal::from_slice(&[3]);
6895        let other_target = Principal::from_slice(&[5]).to_text();
6896        let path = &fixture.owners.root;
6897        fixture
6898            .owners
6899            .platform
6900            .with_observation_snapshot(|platform| {
6901                assert_eq!(
6902                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
6903                    1_000
6904                );
6905                assert!(matches!(
6906                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Empty),
6907                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
6908                        field: "module-free pool asset",
6909                        ..
6910                    })
6911                ));
6912                PoolInspectionFixture::response(path, fixture.root_id, 900);
6913                assert_eq!(
6914                    platform.inspect_pool_balance(
6915                        "other asset",
6916                        &root,
6917                        &other_target,
6918                        InspectedModule::Any
6919                    )?,
6920                    900
6921                );
6922                PoolInspectionFixture::response(path, other_root, 800);
6923                assert_eq!(
6924                    platform.inspect_pool_balance(
6925                        "other Root",
6926                        &other_root.to_text(),
6927                        &target,
6928                        InspectedModule::Any
6929                    )?,
6930                    800
6931                );
6932                assert_eq!(
6933                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
6934                    1_000
6935                );
6936                // Cached data cannot authorize an operator selected after the first inspection.
6937                std::fs::write(path.join("operator"), other_root.to_text()).unwrap();
6938                assert!(matches!(
6939                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any),
6940                    Err(IcpEnsurePlatformError::OperatorMismatch { .. })
6941                ));
6942                Ok(())
6943            })
6944            .unwrap();
6945        std::fs::remove_dir_all(path).unwrap();
6946    }
6947
6948    #[cfg(unix)]
6949    #[test]
6950    fn pool_inspection_snapshot_does_not_retain_transport_or_authority_failures() {
6951        let mut fixture = PoolInspectionFixture::new();
6952        let root = fixture.root_id.to_text();
6953        let target = fixture.target.to_text();
6954        let path = &fixture.owners.root;
6955        fixture
6956            .owners
6957            .platform
6958            .with_observation_snapshot(|platform| {
6959                std::fs::write(path.join("fail"), []).unwrap();
6960                assert!(matches!(
6961                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any),
6962                    Err(IcpEnsurePlatformError::CurrentProtocol(
6963                        current_protocol::CurrentProtocolError::Transport(_)
6964                    ))
6965                ));
6966                std::fs::remove_file(path.join("fail")).unwrap();
6967                PoolInspectionFixture::response(path, Principal::from_slice(&[3]), 900);
6968                assert!(matches!(
6969                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any),
6970                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
6971                        field: "Root-only controllers",
6972                        ..
6973                    })
6974                ));
6975                PoolInspectionFixture::response(path, fixture.root_id, 800);
6976                assert_eq!(
6977                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
6978                    800
6979                );
6980                assert_eq!(platform.icp.remote_call_count(), 7);
6981                Ok(())
6982            })
6983            .unwrap();
6984        std::fs::remove_dir_all(path).unwrap();
6985    }
6986
6987    #[cfg(unix)]
6988    #[test]
6989    #[expect(
6990        clippy::too_many_lines,
6991        reason = "one measured transport fixture keeps equivalent retained inventories and call accounting together"
6992    )]
6993    fn configured_observation_benchmark_retains_snapshot_call_bound() {
6994        use std::{
6995            fs,
6996            os::unix::fs::PermissionsExt,
6997            sync::{Arc, Mutex},
6998        };
6999        for pool_count in [0_u32, 8, 24, 96] {
7000            let mut fixture = ProtocolOwnersFixture::new();
7001            let root_pid = Principal::from_slice(&[2]);
7002            for (index, canister) in fixture.platform.desired.canisters.iter_mut().enumerate() {
7003                let pid = Principal::from_slice(&[u8::try_from(index + 1).unwrap()]).to_text();
7004                let mut status: serde_json::Value = serde_json::from_slice(
7005                    &fs::read(fixture.root.join(format!("{}.json", canister.name))).unwrap(),
7006                )
7007                .unwrap();
7008                status["id"] = serde_json::Value::String(pid.clone());
7009                fs::write(fixture.root.join(format!("{pid}.json")), status.to_string()).unwrap();
7010                canister.principal = Some(pid);
7011            }
7012            fixture.platform.desired.protocol =
7013                Some(crate::fleet_ensure::model::DesiredFleetProtocol {
7014                    app_config: "unused.toml".to_string(),
7015                    component_group_placements: Vec::new(),
7016                    root_candid: "root.did".to_string(),
7017                    coordinator_candid: "unused.did".to_string(),
7018                    store_candid: "unused.did".to_string(),
7019                });
7020            fs::write(fixture.root.join("root.did"), "service : {}").unwrap();
7021            let mut entries = Vec::new();
7022            for index in 0..pool_count {
7023                let pid = Principal::from_slice(&[u8::try_from(index + 10).unwrap()]);
7024                let mut configured = fixture.platform.desired.canisters[0].clone();
7025                configured.name = format!("pool-{index:03}");
7026                configured.kind = DesiredCanisterKind::Pool;
7027                configured.principal = Some(pid.to_text());
7028                configured.parent = Some("root".to_string());
7029                fixture.platform.desired.canisters.push(configured);
7030                entries.push(CanisterPoolAsset {
7031                    canister_id: pid,
7032                    creation_receipt: None,
7033                    cycles: Cycles::new(1_000_000_000_000),
7034                    origin: CanisterPoolAssetOrigin::Imported,
7035                    status: CanisterPoolAssetStatus::Ready,
7036                    added_at_ns: 1,
7037                    updated_at_ns: 1,
7038                });
7039            }
7040            let page = CanisterPoolResponse {
7041                config: canic_core::ids::FleetSubnetCanisterPoolConfig {
7042                    minimum_size: 5,
7043                    maximum_size: 128,
7044                    canister_cycles: Cycles::new(1_000_000_000_000),
7045                    creation_execution_margin: Cycles::new(1_000_000_000),
7046                },
7047                tracked: pool_count,
7048                store: 0,
7049                store_deletion_pending: 0,
7050                pooled: pool_count,
7051                workload: 0,
7052                surplus: 0,
7053                ready: pool_count,
7054                pending_reset: 0,
7055                claimed: 0,
7056                recycling: 0,
7057                handing_off: 0,
7058                failed: 0,
7059                completed_handoffs: 0,
7060                pending_creation: None,
7061                pending_handoff: None,
7062                entries,
7063                next_start_after: None,
7064            };
7065            let response = Ok::<_, canic_core::dto::error::Error>(RootPoolStatusResponse::Pool(
7066                Box::new(page),
7067            ));
7068            fs::write(fixture.root.join("pool.json"), serde_json::json!({"response_bytes": hex_bytes(candid::encode_one(response).unwrap())}).to_string()).unwrap();
7069            let executable = fixture.root.join("icp");
7070            fs::write(&executable, format!(
7071                "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp 1.5.0'; exit 0; fi\nwhile [ \"$#\" -gt 0 ] && [ \"$1\" != canister ]; do shift; done\nshift\nsleep 0.05\ncase \"$1\" in\nstatus) shift; cat '{}'/\"$1\".json;;\ncall) cat '{}'/pool.json;;\n*) exit 1;;\nesac\n",
7072                fixture.root.display(), fixture.root.display())).unwrap();
7073            fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
7074            let timings = Arc::new(Mutex::new(Vec::new()));
7075            let sink = Arc::clone(&timings);
7076            fixture.platform = fixture
7077                .platform
7078                .with_observation_handler(move |timing| sink.lock().unwrap().push(timing));
7079            let observed = fixture
7080                .platform
7081                .with_observation_snapshot(|platform| {
7082                    platform
7083                        .timed_observation(FleetObservationStage::ConfiguredCanisters, |platform| {
7084                            platform.observe_configured_canisters(&fixture.state)
7085                        })
7086                })
7087                .unwrap();
7088            assert_eq!(observed.len(), usize::try_from(pool_count).unwrap() + 3);
7089            assert!(observed.values().all(Option::is_some));
7090            assert_eq!(
7091                observed["root"].as_ref().unwrap().principal,
7092                root_pid.to_text()
7093            );
7094            let timing = timings.lock().unwrap().pop().unwrap();
7095            assert_eq!(timing.remote_call_attempts, 3 + u64::from(pool_count > 0));
7096            assert!(timing.succeeded);
7097            println!(
7098                "configured_observation pool_assets={pool_count} elapsed_ms={} calls={}",
7099                timing.elapsed_millis, timing.remote_call_attempts
7100            );
7101            fs::remove_dir_all(fixture.root).unwrap();
7102        }
7103    }
7104
7105    #[cfg(unix)]
7106    #[test]
7107    fn configured_observation_failure_drains_batch_and_keeps_first_error() {
7108        use std::{
7109            fs,
7110            sync::{Arc, Mutex},
7111        };
7112        let mut fixture = ProtocolOwnersFixture::new();
7113        let timings = Arc::new(Mutex::new(Vec::new()));
7114        let sink = Arc::clone(&timings);
7115        fixture.platform = fixture
7116            .platform
7117            .with_observation_handler(move |timing| sink.lock().unwrap().push(timing));
7118        let mut first: serde_json::Value =
7119            serde_json::from_slice(&fs::read(fixture.root.join("coordinator.json")).unwrap())
7120                .unwrap();
7121        first["id"] = serde_json::json!("different-principal");
7122        fs::write(fixture.root.join("coordinator.json"), first.to_string()).unwrap();
7123        fs::remove_file(fixture.root.join("root.json")).unwrap();
7124        let failure = fixture.platform.with_observation_snapshot(|platform| {
7125            platform.timed_observation(FleetObservationStage::ConfiguredCanisters, |platform| {
7126                platform.observe_configured_canisters(&fixture.state)
7127            })
7128        });
7129        assert!(
7130            matches!(failure, Err(IcpEnsurePlatformError::StatusIdentityMismatch { expected, .. }) if expected == "coordinator")
7131        );
7132        let timing = timings.lock().unwrap().pop().unwrap();
7133        assert!(!timing.succeeded);
7134        assert_eq!(timing.remote_call_attempts, 3);
7135        assert!(fixture.platform.observation_snapshot.borrow().is_none());
7136        fixture.status("coordinator", "Running", true);
7137        fixture.status("root", "Running", true);
7138        assert_eq!(
7139            fixture
7140                .platform
7141                .with_observation_snapshot(
7142                    |platform| platform.observe_configured_canisters(&fixture.state)
7143                )
7144                .unwrap()
7145                .len(),
7146            3
7147        );
7148        assert_eq!(fixture.platform.icp.remote_call_count(), 6);
7149        fs::remove_dir_all(fixture.root).unwrap();
7150    }
7151
7152    #[cfg(unix)]
7153    #[test]
7154    fn reinstall_authority_pass_reads_statuses_and_refreshes_after_failure() {
7155        let mut fixture = ProtocolOwnersFixture::new();
7156        let path = &fixture.root;
7157        std::fs::write(path.join("icp"), r#"#!/bin/sh
7158if [ "$1" = '--version' ]; then echo 'icp 1.5.0'; exit 0; fi
7159while [ "$#" -gt 0 ] && [ "$1" != canister ] && [ "$1" != identity ] && [ "$1" != cycles ]; do shift; done
7160case "$1" in
7161identity) echo operator;;
7162cycles) echo '{"balance":"1000000000000 cycles"}';;
7163canister) shift; [ "$1" = status ] || exit 1; shift; printf '%s\n' "$1" >> calls; cat "$1.json";;
7164*) exit 1;;
7165esac
7166"#).unwrap();
7167        for name in ["root", "coordinator", "store"] {
7168            let file = path.join(format!("{name}.json"));
7169            let mut status: serde_json::Value =
7170                serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
7171            status["version"] = serde_json::json!(7);
7172            std::fs::write(file, serde_json::to_vec(&status).unwrap()).unwrap();
7173        }
7174        let first = fixture
7175            .platform
7176            .reinstall_authorities(&fixture.state)
7177            .unwrap()
7178            .unwrap();
7179        assert_eq!(first.len(), 3);
7180        assert!(
7181            first
7182                .values()
7183                .all(|value| value.live.canister_version == Some(7))
7184        );
7185        let calls = std::fs::read_to_string(path.join("calls")).unwrap();
7186        eprintln!(
7187            "reinstall authority status calls: {calls:?}; remote calls: {}",
7188            fixture.platform.icp.remote_call_count()
7189        );
7190        assert_eq!(calls, "root\ncoordinator\nstore\n");
7191        assert_eq!(fixture.platform.icp.remote_call_count(), 4);
7192        let file = path.join("root.json");
7193        let mut status: serde_json::Value =
7194            serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
7195        status["version"] = serde_json::json!(8);
7196        status["status"] = serde_json::json!("Stopped");
7197        status["module_hash"] = serde_json::json!("ab".repeat(32));
7198        status["settings"]["controllers"] = serde_json::json!(["changed-controller"]);
7199        std::fs::write(&file, serde_json::to_vec(&status).unwrap()).unwrap();
7200        std::fs::rename(path.join("store.json"), path.join("store.saved")).unwrap();
7201        assert!(
7202            fixture
7203                .platform
7204                .reinstall_authorities(&fixture.state)
7205                .is_err()
7206        );
7207        std::fs::rename(path.join("store.saved"), path.join("store.json")).unwrap();
7208        let after = fixture
7209            .platform
7210            .reinstall_authorities(&fixture.state)
7211            .unwrap()
7212            .unwrap();
7213        let root = &after["root"].live;
7214        assert_eq!(root.canister_version, Some(8));
7215        assert_eq!(root.status, CanisterRuntimeStatus::Stopped);
7216        assert_eq!(
7217            root.module_sha256.as_deref(),
7218            Some("ab".repeat(32).as_str())
7219        );
7220        assert_eq!(root.controllers, vec!["changed-controller"]);
7221        std::fs::remove_dir_all(path).unwrap();
7222    }
7223
7224    #[cfg(unix)]
7225    #[test]
7226    fn protocol_owner_observations_read_each_owner_once_and_refresh_between_calls() {
7227        let fixture = ProtocolOwnersFixture::new();
7228        assert!(
7229            fixture
7230                .platform
7231                .current_protocol_owners_are_ready(&fixture.state)
7232                .unwrap()
7233        );
7234        assert_eq!(
7235            std::fs::read_to_string(fixture.root.join("calls")).unwrap(),
7236            "coordinator\nroot\nstore\n"
7237        );
7238        fixture.status("store", "Stopping", true);
7239        assert!(
7240            !fixture
7241                .platform
7242                .current_protocol_owners_are_ready(&fixture.state)
7243                .unwrap()
7244        );
7245        fixture.status("store", "Stopped", true);
7246        assert!(
7247            !fixture
7248                .platform
7249                .current_protocol_owners_are_ready(&fixture.state)
7250                .unwrap()
7251        );
7252        fixture.status("store", "Running", false);
7253        assert!(
7254            !fixture
7255                .platform
7256                .current_protocol_owners_are_ready(&fixture.state)
7257                .unwrap()
7258        );
7259        fixture.status("store", "Running", true);
7260        assert!(
7261            fixture
7262                .platform
7263                .current_protocol_owners_are_ready(&fixture.state)
7264                .unwrap()
7265        );
7266        assert_eq!(
7267            std::fs::read_to_string(fixture.root.join("calls"))
7268                .unwrap()
7269                .lines()
7270                .count(),
7271            15
7272        );
7273        std::fs::remove_dir_all(fixture.root).unwrap();
7274    }
7275
7276    #[cfg(unix)]
7277    #[test]
7278    fn protocol_owner_observations_preserve_missing_owner_and_transport_failures() {
7279        let mut fixture = ProtocolOwnersFixture::new();
7280        fixture.platform.desired.canisters[0].principal = None;
7281        assert!(
7282            !fixture
7283                .platform
7284                .current_protocol_owners_are_ready(&fixture.state)
7285                .unwrap()
7286        );
7287        std::fs::remove_file(fixture.root.join("store.json")).unwrap();
7288        assert!(matches!(
7289            fixture
7290                .platform
7291                .current_protocol_owners_are_ready(&fixture.state),
7292            Err(IcpEnsurePlatformError::Icp(_))
7293        ));
7294        fixture.platform.desired.canisters[0].principal = Some("coordinator".to_string());
7295        fixture.status("coordinator", "Running", false);
7296        assert!(matches!(
7297            fixture
7298                .platform
7299                .current_protocol_owners_are_ready(&fixture.state),
7300            Err(IcpEnsurePlatformError::Icp(_))
7301        ));
7302        std::fs::remove_dir_all(fixture.root).unwrap();
7303    }
7304
7305    #[cfg(unix)]
7306    #[test]
7307    #[expect(
7308        clippy::too_many_lines,
7309        reason = "one transport sequence proves snapshot reuse and expiry across success, failure and effects"
7310    )]
7311    fn observation_snapshot_expires_before_effects_and_after_failed_observation() {
7312        use std::{fs, os::unix::fs::PermissionsExt};
7313
7314        let root = crate::test_support::temp_dir("canic-observation-snapshot");
7315        fs::create_dir_all(&root).expect("create observation fixture");
7316        let executable = root.join("icp");
7317        let commands = root.join("commands.log");
7318        let response = root.join("status.json");
7319        let canister = "rrkah-fqaaa-aaaaa-aaaaq-cai";
7320        let operator = "rdmx6-jaaaa-aaaaa-aaadq-cai";
7321        let status = |cycles: u128| {
7322            serde_json::json!({
7323                "id": canister,
7324                "status": "Running",
7325                "settings": { "controllers": [operator] },
7326                "module_hash": null,
7327                "cycles": cycles.to_string(),
7328            })
7329            .to_string()
7330        };
7331        fs::write(&response, status(1_000)).expect("write initial live balance");
7332        fs::write(&executable, format!(
7333            "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp 1.5.0'; exit 0; fi\nprintf '%s\\n' \"$*\" >> '{}'\ncat '{}'\n",
7334            commands.display(), response.display(),
7335        )).expect("write observation transport");
7336        fs::set_permissions(&executable, fs::Permissions::from_mode(0o755))
7337            .expect("make observation transport executable");
7338        let desired = DesiredFleet {
7339            bootstrap: None,
7340            canisters: Vec::new(),
7341            cycles_ledger: "um5iw-rqaaa-aaaaq-qaaba-cai".to_string(),
7342            environment: "local".to_string(),
7343            fleet: "observation-snapshot".to_string(),
7344            ledger_fee_cycles: "100M".to_string(),
7345            management_creation_fee_cycles: "500B".to_string(),
7346            material_cycle_threshold: "1B".to_string(),
7347            maximum_observation_burn_cycles: "1B".to_string(),
7348            maximum_stalled_observations: 4,
7349            maximum_update_burn_cycles: "1B".to_string(),
7350            operator: operator.to_string(),
7351            protocol: None,
7352            protocol_steps: Vec::new(),
7353            schema_version: 1,
7354            treasury: "treasury".to_string(),
7355        };
7356        let mut platform =
7357            IcpEnsurePlatform::new(desired, executable.to_str().expect("transport path"), &root);
7358        let failed: Result<(), IcpEnsurePlatformError> =
7359            platform.with_observation_snapshot(|platform| {
7360                assert_eq!(
7361                    platform
7362                        .status_optional(canister)?
7363                        .expect("live canister")
7364                        .cycles,
7365                    1_000
7366                );
7367                fs::write(&response, status(900)).expect("advance simulated live balance");
7368                assert_eq!(
7369                    platform
7370                        .status_optional(canister)?
7371                        .expect("same snapshot")
7372                        .cycles,
7373                    1_000
7374                );
7375                Err(IcpEnsurePlatformError::Arithmetic(
7376                    "injected observation failure",
7377                ))
7378            });
7379        assert!(matches!(failed, Err(IcpEnsurePlatformError::Arithmetic(_))));
7380        assert_eq!(
7381            fs::read_to_string(&commands)
7382                .expect("first observation calls")
7383                .lines()
7384                .count(),
7385            1
7386        );
7387        assert_eq!(
7388            platform
7389                .status_optional(canister)
7390                .expect("fresh action read")
7391                .expect("live canister")
7392                .cycles,
7393            900
7394        );
7395        platform
7396            .with_observation_snapshot(|platform| {
7397                assert_eq!(
7398                    platform
7399                        .status_optional(canister)?
7400                        .expect("next observation")
7401                        .cycles,
7402                    900
7403                );
7404                fs::write(&response, status(800)).expect("advance live balance again");
7405                assert_eq!(
7406                    platform
7407                        .status_optional(canister)?
7408                        .expect("next snapshot")
7409                        .cycles,
7410                    900
7411                );
7412                Ok(())
7413            })
7414            .expect("next observation succeeds");
7415        assert_eq!(
7416            platform
7417                .status_optional(canister)
7418                .expect("post-observation action read")
7419                .expect("live canister")
7420                .cycles,
7421            800
7422        );
7423        assert_eq!(
7424            fs::read_to_string(&commands)
7425                .expect("all observation calls")
7426                .lines()
7427                .count(),
7428            4
7429        );
7430        fs::remove_dir_all(root).expect("remove observation fixture");
7431    }
7432
7433    #[test]
7434    fn protocol_observation_delay_uses_bounded_exponential_backoff() {
7435        let production_cap = MAXIMUM_PROTOCOL_OBSERVATION_DELAY;
7436        assert_eq!(
7437            protocol_observation_delay(0, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
7438            Duration::from_millis(250)
7439        );
7440        assert_eq!(
7441            protocol_observation_delay(1, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
7442            Duration::from_millis(250)
7443        );
7444        assert_eq!(
7445            protocol_observation_delay(2, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
7446            Duration::from_millis(500)
7447        );
7448        assert_eq!(
7449            protocol_observation_delay(5, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
7450            Duration::from_secs(4)
7451        );
7452        assert_eq!(
7453            protocol_observation_delay(6, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
7454            Duration::from_secs(5)
7455        );
7456        assert_eq!(
7457            protocol_observation_delay(
7458                u32::MAX,
7459                INITIAL_PROTOCOL_OBSERVATION_DELAY,
7460                production_cap
7461            ),
7462            Duration::from_secs(5)
7463        );
7464        assert_eq!(
7465            protocol_observation_delay(u32::MAX, Duration::from_secs(1), Duration::from_secs(1)),
7466            Duration::from_secs(1)
7467        );
7468    }
7469
7470    #[cfg(unix)]
7471    #[test]
7472    fn icp_1_5_public_non_controller_status_is_typed_unavailable_evidence() {
7473        use std::{fs, os::unix::fs::PermissionsExt};
7474
7475        let canister = "rrkah-fqaaa-aaaaa-aaaaq-cai";
7476        let root = crate::test_support::temp_dir("canic-public-status-projection");
7477        fs::create_dir_all(&root).expect("create public status fixture");
7478        let executable = root.join("icp");
7479        let commands = root.join("commands.log");
7480        let script = format!(
7481            "#!/bin/sh\n\
7482             if [ \"$1\" = \"--version\" ]; then echo 'icp 1.5.0'; exit 0; fi\n\
7483             printf '%s\\n' \"$*\" >> '{}'\n\
7484             printf '%s\\n' '{{\"id\":\"{canister}\",\"controllers\":[\"rdmx6-jaaaa-aaaaa-aaadq-cai\"],\"module_hash\":null}}'\n",
7485            commands.display(),
7486        );
7487        fs::write(&executable, script).expect("write fake ICP 1.5.0");
7488        fs::set_permissions(&executable, fs::Permissions::from_mode(0o755))
7489            .expect("make fake ICP executable");
7490        let desired = DesiredFleet {
7491            bootstrap: None,
7492            canisters: Vec::new(),
7493            cycles_ledger: "um5iw-rqaaa-aaaaq-qaaba-cai".to_string(),
7494            environment: "local".to_string(),
7495            fleet: "public-status".to_string(),
7496            ledger_fee_cycles: "100M".to_string(),
7497            management_creation_fee_cycles: "500B".to_string(),
7498            material_cycle_threshold: "1B".to_string(),
7499            maximum_observation_burn_cycles: "1B".to_string(),
7500            maximum_stalled_observations: 4,
7501            maximum_update_burn_cycles: "1B".to_string(),
7502            operator: "rdmx6-jaaaa-aaaaa-aaadq-cai".to_string(),
7503            protocol: None,
7504            protocol_steps: Vec::new(),
7505            schema_version: 1,
7506            treasury: "treasury".to_string(),
7507        };
7508        let platform =
7509            IcpEnsurePlatform::new(desired, executable.to_string_lossy().as_ref(), &root);
7510
7511        assert!(matches!(
7512            platform.status_optional(canister),
7513            Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable {
7514                canister: unavailable,
7515            }) if unavailable == canister
7516        ));
7517        assert!(
7518            fs::read_to_string(commands)
7519                .expect("read fake ICP commands")
7520                .contains("canister status rrkah-fqaaa-aaaaa-aaaaq-cai --json")
7521        );
7522    }
7523
7524    #[cfg(unix)]
7525    #[test]
7526    fn versionless_icp_status_uses_exact_typed_management_version() {
7527        #[derive(CandidType)]
7528        struct CanonicalManagementCanisterStatusFixture {
7529            version: u64,
7530            module_hash: Option<Vec<u8>>,
7531        }
7532
7533        use std::{fs, os::unix::fs::PermissionsExt};
7534
7535        let root = crate::test_support::temp_dir("canic-install-version-fallback");
7536        fs::create_dir_all(&root).expect("create version fallback fixture");
7537        let executable = root.join("icp");
7538        let commands = root.join("commands.log");
7539        let canister = "rrkah-fqaaa-aaaaa-aaaaq-cai";
7540        let status_json = format!(
7541            r#"{{"id":"{canister}","name":"coordinator","status":"Running","settings":{{"controllers":["rdmx6-jaaaa-aaaaa-aaadq-cai"]}},"module_hash":"0x{}","memory_size":"1","cycles":"1000000000000","query_stats":{{"num_calls_total":"0","num_instructions_total":"0","request_payload_bytes_total":"0","response_payload_bytes_total":"0"}}}}"#,
7542            "11".repeat(32),
7543        );
7544        let response_bytes = candid::encode_one(CanonicalManagementCanisterStatusFixture {
7545            version: 42,
7546            module_hash: Some(vec![0x22; 32]),
7547        })
7548        .expect("encode independently modelled management status");
7549        let script = format!(
7550            "#!/bin/sh\n\
7551             if [ \"$1\" = \"--version\" ]; then printf '%s\\n' 'icp 1.5.0'; exit 0; fi\n\
7552             printf '%s\\n' \"$*\" >> '{}'\n\
7553             case \"$*\" in\n\
7554               *\"canister status {canister}\"*) printf '%s\\n' '{}' ;;\n\
7555               *) printf '%s\\n' 'unexpected fake ICP command' >&2; exit 23 ;;\n\
7556             esac\n",
7557            commands.display(),
7558            status_json,
7559        );
7560        fs::write(&executable, script).expect("write fake ICP 1.5.0");
7561        fs::set_permissions(&executable, fs::Permissions::from_mode(0o755))
7562            .expect("make fake ICP executable");
7563        let icp = IcpCli::new(executable.to_string_lossy(), Some("ic".to_string()));
7564
7565        let projected = icp
7566            .canister_status_report(canister)
7567            .expect("read ICP 1.5.0 status projection");
7568        assert_eq!(projected.canister_version, None);
7569        let exact = exact_install_canister_status_with(
7570            canister,
7571            projected.canister_version,
7572            projected.module_hash.map(|hash| normalize_hash(&hash)),
7573            |effective_canister_id| {
7574                assert_eq!(
7575                    effective_canister_id,
7576                    canister.parse().expect("target Principal")
7577                );
7578                candid::decode_one(&response_bytes).map_err(IcpManagementCallError::CandidResponse)
7579            },
7580        )
7581        .expect("obtain exact typed management status");
7582        assert_eq!(exact.canister_version, 42);
7583        assert_eq!(exact.module_sha256, Some("22".repeat(32)));
7584        let commands = fs::read_to_string(&commands).expect("read fake ICP commands");
7585        assert!(commands.contains("canister status rrkah-fqaaa-aaaaa-aaaaq-cai"));
7586        assert!(!commands.contains("canister call aaaaa-aa canister_status"));
7587
7588        fs::write(
7589            &executable,
7590            "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'icp 1.5.0'; exit 0; fi\necho '{\"not_response_bytes\":true}'\n",
7591        )
7592        .expect("replace unavailable management fixture");
7593        let error = exact_install_canister_status_with(canister, None, None, |_| {
7594            Err(IcpManagementCallError::MissingEnvironment)
7595        })
7596        .expect_err("missing projected and typed version must fail closed");
7597        assert!(matches!(
7598            error,
7599            IcpEnsurePlatformError::InstallVersionProofUnavailable {
7600                canister: observed_canister,
7601                source,
7602            } if observed_canister == canister
7603                && matches!(source.as_ref(), IcpManagementCallError::MissingEnvironment)
7604        ));
7605
7606        fs::remove_dir_all(root).expect("remove version fallback fixture");
7607    }
7608
7609    #[test]
7610    fn root_owned_observation_classifies_bootstrap_and_workload_lifecycle() {
7611        assert_eq!(
7612            root_owned_lifecycle(DesiredCanisterKind::Store, &CanisterPoolAssetStatus::Store),
7613            Some(RootOwnedCanisterLifecycle::Store)
7614        );
7615        assert_eq!(
7616            root_owned_lifecycle(DesiredCanisterKind::Pool, &CanisterPoolAssetStatus::Ready),
7617            Some(RootOwnedCanisterLifecycle::Idle)
7618        );
7619        assert_eq!(
7620            root_owned_lifecycle(
7621                DesiredCanisterKind::Pool,
7622                &CanisterPoolAssetStatus::Claimed {
7623                    claim: canic_core::dto::pool::CanisterPoolClaim {
7624                        component: canic_core::ids::ComponentInstanceId::from_generated_bytes(
7625                            [1; 32]
7626                        ),
7627                        operation_id: [2; 32],
7628                    },
7629                }
7630            ),
7631            Some(RootOwnedCanisterLifecycle::Claimed)
7632        );
7633        assert_eq!(
7634            root_owned_lifecycle(
7635                DesiredCanisterKind::Pool,
7636                &CanisterPoolAssetStatus::Workload {
7637                    claim: canic_core::dto::pool::CanisterPoolClaim {
7638                        component: canic_core::ids::ComponentInstanceId::from_generated_bytes(
7639                            [1; 32]
7640                        ),
7641                        operation_id: [2; 32],
7642                    },
7643                }
7644            ),
7645            Some(RootOwnedCanisterLifecycle::Workload)
7646        );
7647        assert_eq!(
7648            root_owned_lifecycle(
7649                DesiredCanisterKind::Pool,
7650                &CanisterPoolAssetStatus::HandingOff {
7651                    recipient: Principal::anonymous(),
7652                }
7653            ),
7654            None
7655        );
7656        assert_eq!(
7657            root_owned_lifecycle(
7658                DesiredCanisterKind::Store,
7659                &CanisterPoolAssetStatus::PendingReset
7660            ),
7661            None
7662        );
7663    }
7664
7665    #[cfg(unix)]
7666    #[test]
7667    #[expect(
7668        clippy::too_many_lines,
7669        reason = "one platform-to-policy matrix proves lifecycle projection and exclusive funding authority"
7670    )]
7671    fn configured_pool_observation_preserves_one_funding_owner() {
7672        use crate::fleet_ensure::{model::DesiredFleetArtifacts, policy::compile_plan};
7673
7674        let mut fixture = PoolInspectionFixture::fresh(1);
7675        fixture.target = Principal::from_slice(&[10]);
7676        PoolInspectionFixture::reserve_response(
7677            &fixture.owners.root,
7678            fixture.root_id,
7679            fixture.target,
7680            1_000,
7681        );
7682        let root_id = fixture.root_id.to_text();
7683        let target = fixture.target.to_text();
7684        let mut desired = fixture.owners.platform.desired.clone();
7685        desired.cycles_ledger = Principal::from_slice(&[8]).to_text();
7686        desired.treasury = "root".into();
7687        desired.ledger_fee_cycles = "5".into();
7688        desired.management_creation_fee_cycles = "500".into();
7689        desired.material_cycle_threshold = "1".into();
7690        desired.maximum_observation_burn_cycles = "10".into();
7691        desired.maximum_update_burn_cycles = "20".into();
7692        let mut root = desired.canisters[0].clone();
7693        root.principal = Some(root_id.clone());
7694        root.subnet = root_id.clone();
7695        root.controllers = vec![desired.operator.clone()];
7696        root.wasm = None;
7697        root.initial_cycles = "1000".into();
7698        root.minimum_cycles = "1000".into();
7699        let mut configured = root.clone();
7700        configured.name = "pool".into();
7701        configured.kind = DesiredCanisterKind::Pool;
7702        configured.principal = Some(target.clone());
7703        configured.controllers = vec![root_id.clone()];
7704        configured.parent = Some(root.name.clone());
7705        let mut coordinator = root.clone();
7706        coordinator.name = "coordinator".into();
7707        coordinator.kind = DesiredCanisterKind::Coordinator;
7708        coordinator.principal = Some(Principal::from_slice(&[20]).to_text());
7709        root.parent = Some(coordinator.name.clone());
7710        desired.canisters = vec![coordinator.clone(), root, configured.clone()];
7711        let bootstrap = desired.bootstrap.as_mut().unwrap();
7712        bootstrap.fresh_estate = false;
7713        bootstrap.roots = vec![crate::fleet_ensure::model::DesiredFleetBootstrapRoot {
7714            canister_pool_imports: vec![target.clone()],
7715            component_admissions: Vec::new(),
7716            component_topology_digest: canic_core::ids::ComponentTopologyDigest::from_bytes(
7717                [1; 32],
7718            ),
7719            funding: crate::test_support::fleet_subnet_root_funding_authority(),
7720            limits: canic_core::ids::FleetSubnetRootLimits {
7721                maximum_component_instances: 1,
7722                maximum_registry_bytes: 1,
7723                maximum_wasm_store_bytes: 1,
7724                canister_pool: canic_core::ids::FleetSubnetCanisterPoolConfig {
7725                    minimum_size: 0,
7726                    maximum_size: 1,
7727                    canister_cycles: Cycles::new(1_000),
7728                    creation_execution_margin: Cycles::new(100),
7729                },
7730                cycles_funding: canic_core::ids::CyclesFundingBudget {
7731                    window_secs: 1,
7732                    maximum_cycles: Cycles::new(1),
7733                },
7734                maximum_group_placements: 1,
7735            },
7736            placement_subnet: canic_core::ids::SubnetId::from_principal(fixture.root_id),
7737            root: "root".into(),
7738            store: "store".into(),
7739        }];
7740        let claim = canic_core::dto::pool::CanisterPoolClaim {
7741            component: canic_core::ids::ComponentInstanceId::from_generated_bytes([1; 32]),
7742            operation_id: [2; 32],
7743        };
7744        for status in [
7745            CanisterPoolAssetStatus::PendingReset,
7746            CanisterPoolAssetStatus::Failed {
7747                reason: "reset interrupted".into(),
7748            },
7749            CanisterPoolAssetStatus::Ready,
7750            CanisterPoolAssetStatus::Claimed {
7751                claim: claim.clone(),
7752            },
7753            CanisterPoolAssetStatus::Workload { claim },
7754        ] {
7755            for cycles in [900, 1_050, 1_100] {
7756                let lifecycle = estate_pool_lifecycle(&status).unwrap();
7757                let retained = matches!(
7758                    lifecycle,
7759                    EstatePoolAssetLifecycle::PendingReset | EstatePoolAssetLifecycle::Failed
7760                );
7761                let live = IcpEnsurePlatform::observed_root_owned_asset(
7762                    &configured,
7763                    &target,
7764                    &root_id,
7765                    CanisterPoolAsset {
7766                        canister_id: fixture.target,
7767                        creation_receipt: None,
7768                        cycles: Cycles::new(cycles),
7769                        origin: CanisterPoolAssetOrigin::Imported,
7770                        status: status.clone(),
7771                        added_at_ns: 1,
7772                        updated_at_ns: 1,
7773                    },
7774                )
7775                .unwrap();
7776                let mut observation = FleetObservation {
7777                    additional_controlled_cycles: BTreeMap::new(),
7778                    canisters: BTreeMap::from([
7779                        ("pool".into(), live),
7780                        (
7781                            "root".into(),
7782                            Some(LiveCanister {
7783                                canister_version: None,
7784                                controllers: vec![desired.operator.clone()],
7785                                cycles: 10_000,
7786                                module_sha256: None,
7787                                principal: root_id.clone(),
7788                                reinstall_required: false,
7789                                root_owned_lifecycle: None,
7790                                status: CanisterRuntimeStatus::Running,
7791                            }),
7792                        ),
7793                    ]),
7794                    estate_funding_domains: BTreeMap::from([(
7795                        "root".into(),
7796                        EstateFundingDomainObservation {
7797                            balance_cycles: Some(0),
7798                            cycles_ledger: desired.cycles_ledger.clone(),
7799                            root_principal: Some(root_id.clone()),
7800                            pool: Some(EstatePoolInventoryObservation {
7801                                assets: vec![EstatePoolAssetObservation {
7802                                    creation_receipt: None,
7803                                    cycles,
7804                                    lifecycle,
7805                                    origin: EstatePoolAssetOrigin::Imported,
7806                                    principal: target.clone(),
7807                                }],
7808                                maximum_size: 1,
7809                                minimum_size: 0,
7810                                pending_creation: None,
7811                                readiness_floor_cycles: 1_000,
7812                                creation_execution_margin_cycles: 100,
7813                            }),
7814                        },
7815                    )]),
7816                    ledger_fee_cycles: 5,
7817                    operator_cycles: 10_000,
7818                    protocol_ready: BTreeMap::new(),
7819                };
7820                let mut coordinator_live = observation.canisters["root"].clone().unwrap();
7821                coordinator_live.principal = coordinator.principal.clone().unwrap();
7822                observation
7823                    .canisters
7824                    .insert(coordinator.name.clone(), Some(coordinator_live));
7825                let actions = current_protocol::compile_pool_reconciliation(
7826                    &fixture.owners.root,
7827                    &desired,
7828                    &fixture.owners.state,
7829                    &observation.estate_funding_domains,
7830                )
7831                .unwrap();
7832                assert_eq!(actions.len(), usize::from(retained));
7833                let plan = compile_plan(
7834                    &desired,
7835                    &DesiredFleetArtifacts::default(),
7836                    &actions,
7837                    &"11".repeat(32),
7838                    &desired.fleet,
7839                    &observation,
7840                    1,
7841                    &"22".repeat(32),
7842                    None,
7843                )
7844                .expect("configured pool and reconciliation must share one funding owner");
7845                let pool_plan = plan
7846                    .canisters
7847                    .iter()
7848                    .find(|entry| entry.name == "pool")
7849                    .unwrap();
7850                let funds = pool_plan
7851                    .actions
7852                    .iter()
7853                    .filter(|action| matches!(action, EnsureAction::Fund { .. }))
7854                    .collect::<Vec<_>>();
7855                let should_fund = if retained {
7856                    cycles < 1_100
7857                } else {
7858                    cycles < 1_000 && lifecycle == EstatePoolAssetLifecycle::Ready
7859                };
7860                assert_eq!(funds.len(), usize::from(should_fund));
7861                if let Some(EnsureAction::Fund {
7862                    amount,
7863                    pool_funding,
7864                    expected_post_cycles,
7865                    ..
7866                }) = funds.first()
7867                {
7868                    let authority = pool_funding.as_ref().unwrap();
7869                    assert_eq!(
7870                        (&authority.root, authority.lifecycle),
7871                        (&root_id, lifecycle)
7872                    );
7873                    if retained {
7874                        assert_eq!((*amount, *expected_post_cycles), (1_130 - cycles, 1_130));
7875                    }
7876                }
7877                if retained && cycles == 1_100 {
7878                    observation
7879                        .canisters
7880                        .get_mut(&coordinator.name)
7881                        .unwrap()
7882                        .as_mut()
7883                        .unwrap()
7884                        .status = CanisterRuntimeStatus::Stopped;
7885                    let prerequisite = compile_plan(
7886                        &desired,
7887                        &DesiredFleetArtifacts::default(),
7888                        &[],
7889                        &"11".repeat(32),
7890                        &desired.fleet,
7891                        &observation,
7892                        1,
7893                        &"22".repeat(32),
7894                        None,
7895                    )
7896                    .expect("live reset evidence must not block Coordinator readiness");
7897                    assert!(prerequisite.canisters.iter().flat_map(|entry| &entry.actions)
7898                        .any(|action| matches!(action, EnsureAction::Start { name, .. } if name == &coordinator.name)));
7899                }
7900            }
7901        }
7902        std::fs::remove_dir_all(&fixture.owners.root).unwrap();
7903    }
7904
7905    #[test]
7906    fn estate_pool_inventory_reconciles_every_lifecycle_and_declared_total() {
7907        let claim = canic_core::dto::pool::CanisterPoolClaim {
7908            component: canic_core::ids::ComponentInstanceId::from_generated_bytes([1; 32]),
7909            operation_id: [2; 32],
7910        };
7911        let statuses = [
7912            CanisterPoolAssetStatus::Store,
7913            CanisterPoolAssetStatus::StoreDeletionPending {
7914                operation_id: [3; 32],
7915            },
7916            CanisterPoolAssetStatus::PendingReset,
7917            CanisterPoolAssetStatus::Ready,
7918            CanisterPoolAssetStatus::Claimed {
7919                claim: claim.clone(),
7920            },
7921            CanisterPoolAssetStatus::Workload {
7922                claim: claim.clone(),
7923            },
7924            CanisterPoolAssetStatus::Recycling {
7925                claim,
7926                reset: canic_core::dto::pool::CanisterPoolRecycleReset::Pending,
7927            },
7928            CanisterPoolAssetStatus::HandingOff {
7929                recipient: Principal::anonymous(),
7930            },
7931            CanisterPoolAssetStatus::Failed {
7932                reason: "retained failure".to_string(),
7933            },
7934        ];
7935        let mut observed = EstatePoolLifecycleCounts::default();
7936        for status in &statuses {
7937            observed.observe(status).expect("bounded lifecycle count");
7938        }
7939        let authority = EstatePoolPageAuthority {
7940            completed_handoffs: 0,
7941            counts: observed,
7942            pending_handoff: None,
7943            pooled: 4,
7944            surplus: 1,
7945        };
7946
7947        assert!(authority.matches_complete_inventory(observed, 3));
7948        let mut incomplete = observed;
7949        incomplete.failed -= 1;
7950        incomplete.tracked -= 1;
7951        assert!(!authority.matches_complete_inventory(incomplete, 3));
7952
7953        assert_eq!(estate_pool_lifecycle(&statuses[0]), None);
7954        assert_eq!(estate_pool_lifecycle(&statuses[1]), None);
7955        assert_eq!(
7956            statuses[2..]
7957                .iter()
7958                .map(estate_pool_lifecycle)
7959                .collect::<Vec<_>>(),
7960            vec![
7961                Some(EstatePoolAssetLifecycle::PendingReset),
7962                Some(EstatePoolAssetLifecycle::Ready),
7963                Some(EstatePoolAssetLifecycle::Claimed),
7964                Some(EstatePoolAssetLifecycle::Workload),
7965                Some(EstatePoolAssetLifecycle::Recycling),
7966                Some(EstatePoolAssetLifecycle::HandingOff),
7967                Some(EstatePoolAssetLifecycle::Failed),
7968            ]
7969        );
7970    }
7971
7972    #[test]
7973    fn estate_funding_requires_exact_source_debit_and_destination_credit() {
7974        let exact = EstateFundingObservation {
7975            amount: 60,
7976            destination_after: 100,
7977            destination_before: Some(40),
7978            expected_destination_after: 100,
7979            ledger_fee_cycles: 5,
7980            source_after: 135,
7981            source_before: Some(200),
7982        };
7983        assert!(estate_funding_applied(exact));
7984
7985        for drifted in [
7986            EstateFundingObservation {
7987                source_after: 136,
7988                ..exact
7989            },
7990            EstateFundingObservation {
7991                destination_after: 99,
7992                ..exact
7993            },
7994            EstateFundingObservation {
7995                source_before: None,
7996                ..exact
7997            },
7998            EstateFundingObservation {
7999                destination_before: None,
8000                ..exact
8001            },
8002        ] {
8003            assert!(!estate_funding_applied(drifted));
8004        }
8005    }
8006
8007    #[test]
8008    #[expect(
8009        clippy::too_many_lines,
8010        reason = "the binding regression mutates each authority field independently"
8011    )]
8012    fn retained_root_owned_topology_requires_exact_child_root_and_parent_binding() {
8013        let mut state = FleetEnsureStateRecord {
8014            active_registry: None,
8015            completed_reinstall_action_sha256: BTreeMap::new(),
8016            completed_reinstall_operation_id: None,
8017            completed_reinstalls: BTreeMap::new(),
8018            fleet: "fleet".to_string(),
8019            pending_principals: BTreeMap::new(),
8020            principals: BTreeMap::from([
8021                ("root".to_string(), "root-principal".to_string()),
8022                ("store".to_string(), "store-principal".to_string()),
8023            ]),
8024            retained_cycles_by_principal: BTreeMap::new(),
8025            schema_version: crate::fleet_ensure::model::FLEET_ENSURE_SCHEMA_VERSION,
8026            topology: BTreeMap::from([(
8027                "store".to_string(),
8028                crate::fleet_ensure::model::FleetEnsureTopologyRecord {
8029                    kind: DesiredCanisterKind::Store,
8030                    module_hash: Some("11".repeat(32)),
8031                    parent: Some("root".to_string()),
8032                    protocol_binding: None,
8033                    role: None,
8034                },
8035            )]),
8036        };
8037
8038        assert!(
8039            exact_retained_root_owned_topology(
8040                &state,
8041                "store",
8042                DesiredCanisterKind::Store,
8043                "root",
8044                "store-principal",
8045                "root-principal",
8046            )
8047            .is_some()
8048        );
8049        assert!(
8050            exact_retained_root_owned_topology(
8051                &state,
8052                "store",
8053                DesiredCanisterKind::Store,
8054                "root",
8055                "foreign-store",
8056                "root-principal",
8057            )
8058            .is_none()
8059        );
8060        state
8061            .principals
8062            .insert("store".to_string(), "store-principal".to_string());
8063        state
8064            .principals
8065            .insert("root".to_string(), "foreign-root".to_string());
8066        assert!(
8067            exact_retained_root_owned_topology(
8068                &state,
8069                "store",
8070                DesiredCanisterKind::Store,
8071                "root",
8072                "store-principal",
8073                "root-principal",
8074            )
8075            .is_none()
8076        );
8077        state
8078            .principals
8079            .insert("root".to_string(), "root-principal".to_string());
8080        state
8081            .topology
8082            .get_mut("store")
8083            .expect("Store topology")
8084            .kind = DesiredCanisterKind::Pool;
8085        assert!(
8086            exact_retained_root_owned_topology(
8087                &state,
8088                "store",
8089                DesiredCanisterKind::Store,
8090                "root",
8091                "store-principal",
8092                "root-principal",
8093            )
8094            .is_none()
8095        );
8096        state
8097            .topology
8098            .get_mut("store")
8099            .expect("Store topology")
8100            .kind = DesiredCanisterKind::Store;
8101        state
8102            .topology
8103            .get_mut("store")
8104            .expect("Store topology")
8105            .parent = Some("foreign-root".to_string());
8106        assert!(
8107            exact_retained_root_owned_topology(
8108                &state,
8109                "store",
8110                DesiredCanisterKind::Store,
8111                "root",
8112                "store-principal",
8113                "root-principal",
8114            )
8115            .is_none()
8116        );
8117        state
8118            .topology
8119            .get_mut("store")
8120            .expect("Store topology")
8121            .parent = Some("root".to_string());
8122        state.principals.clear();
8123        state.pending_principals = BTreeMap::from([
8124            ("root".to_string(), "root-principal".to_string()),
8125            ("store".to_string(), "store-principal".to_string()),
8126        ]);
8127        assert!(
8128            exact_retained_root_owned_topology(
8129                &state,
8130                "store",
8131                DesiredCanisterKind::Store,
8132                "root",
8133                "store-principal",
8134                "root-principal",
8135            )
8136            .is_some()
8137        );
8138        state
8139            .principals
8140            .insert("store".to_string(), "foreign-store".to_string());
8141        assert!(
8142            exact_retained_root_owned_topology(
8143                &state,
8144                "store",
8145                DesiredCanisterKind::Store,
8146                "root",
8147                "store-principal",
8148                "root-principal",
8149            )
8150            .is_none()
8151        );
8152    }
8153
8154    #[test]
8155    fn completed_reinstall_requires_exact_continuity_when_ordinary_status_has_no_version() {
8156        let desired = "11".repeat(32);
8157        assert_eq!(
8158            completed_reinstall_continuity(7, true, true, Some(&desired), &desired, None,),
8159            Ok(())
8160        );
8161        assert_eq!(
8162            completed_reinstall_continuity(7, true, true, Some(&desired), &desired, Some(8),),
8163            Ok(())
8164        );
8165        for (rejected, field) in [
8166            (
8167                completed_reinstall_continuity(7, false, true, Some(&desired), &desired, None),
8168                "Principal",
8169            ),
8170            (
8171                completed_reinstall_continuity(7, true, false, Some(&desired), &desired, None),
8172                "Root/parent/kind topology",
8173            ),
8174            (
8175                completed_reinstall_continuity(
8176                    7,
8177                    true,
8178                    true,
8179                    Some(&"22".repeat(32)),
8180                    &desired,
8181                    None,
8182                ),
8183                "module SHA-256",
8184            ),
8185            (
8186                completed_reinstall_continuity(7, true, true, Some(&desired), &desired, Some(7)),
8187                "canister version",
8188            ),
8189        ] {
8190            assert_eq!(rejected, Err(field));
8191        }
8192    }
8193
8194    #[test]
8195    fn fresh_fleet_create_responses_retain_the_exact_requested_balance() {
8196        let canister = Principal::from_slice(&[9; 29]);
8197        let canister_text = canister.to_text();
8198        for (response, receipt) in [
8199            (
8200                Ok(CreateCanisterSuccess {
8201                    block_id: Nat::from(41_u8),
8202                    canister_id: canister,
8203                }),
8204                "41",
8205            ),
8206            (
8207                Err(CreateCanisterError::Duplicate {
8208                    duplicate_of: Nat::from(41_u8),
8209                    canister_id: Some(canister),
8210                }),
8211                "41",
8212            ),
8213        ] {
8214            let outcome = create_canister_response_outcome(response, 5_000)
8215                .expect("successful or duplicate-with-Principal Create response");
8216            assert_eq!(
8217                outcome.created_principal.as_deref(),
8218                Some(canister_text.as_str())
8219            );
8220            assert_eq!(outcome.post_cycles, Some(5_000));
8221            assert_eq!(outcome.receipt.as_deref(), Some(receipt));
8222        }
8223    }
8224
8225    #[test]
8226    fn root_funding_inspection_requires_exact_controller_and_pool_shape() {
8227        let root = Principal::from_slice(&[7; 29]);
8228        let root_text = root.to_text();
8229        let cycles = Nat::from(2_898_749_313_788_u128);
8230        assert_eq!(
8231            validate_root_controlled_inspection(
8232                "pool-0",
8233                InspectedModule::Empty,
8234                &root_text,
8235                &[root],
8236                None,
8237                &cycles,
8238            )
8239            .expect("exact Root-authorized pool inspection"),
8240            2_898_749_313_788,
8241        );
8242
8243        assert_eq!(
8244            validate_root_controlled_inspection(
8245                "pending-reset",
8246                InspectedModule::Any,
8247                &root_text,
8248                &[root],
8249                Some(&[1]),
8250                &cycles,
8251            )
8252            .expect("installed controlled asset remains part of conservation"),
8253            2_898_749_313_788,
8254        );
8255        let foreign = Principal::from_slice(&[8; 29]);
8256        for rejected in [
8257            validate_root_controlled_inspection(
8258                "pending-reset",
8259                InspectedModule::Any,
8260                &root_text,
8261                &[foreign],
8262                Some(&[1]),
8263                &cycles,
8264            ),
8265            validate_root_controlled_inspection(
8266                "pool-0",
8267                InspectedModule::Empty,
8268                &root_text,
8269                &[foreign],
8270                None,
8271                &cycles,
8272            ),
8273            validate_root_controlled_inspection(
8274                "pool-0",
8275                InspectedModule::Empty,
8276                &root_text,
8277                &[root],
8278                Some(&[1]),
8279                &cycles,
8280            ),
8281        ] {
8282            assert!(matches!(
8283                rejected,
8284                Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { .. })
8285            ));
8286        }
8287    }
8288
8289    #[test]
8290    fn pending_fresh_pool_observation_accepts_only_its_reviewed_controller_transition() {
8291        let root = Principal::from_slice(&[7; 29]);
8292        let operator = Principal::from_slice(&[8; 29]);
8293        let foreign = Principal::from_slice(&[9; 29]);
8294        for controllers in [vec![root, operator], vec![root]] {
8295            assert_eq!(
8296                validate_pending_fresh_pool_inspection(
8297                    "pool",
8298                    &root.to_text(),
8299                    &operator.to_text(),
8300                    &controllers,
8301                    None,
8302                    &Nat::from(17_u8),
8303                )
8304                .expect("issued fresh controller transition"),
8305                17,
8306            );
8307        }
8308        for controllers in [
8309            vec![root, foreign],
8310            vec![operator],
8311            vec![root, operator, foreign],
8312            vec![root, root],
8313        ] {
8314            assert!(matches!(
8315                validate_pending_fresh_pool_inspection(
8316                    "pool",
8317                    &root.to_text(),
8318                    &operator.to_text(),
8319                    &controllers,
8320                    None,
8321                    &Nat::from(17_u8),
8322                ),
8323                Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { .. })
8324            ));
8325        }
8326    }
8327}