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_authenticated},
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, TerminalFleetInventory,
28            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::{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/// First input-ordered mismatch or read failure from a drained reset verification batch.
537enum ReinstallAssetVerificationError {
538    Mismatch,
539    Read(Box<IcpEnsurePlatformError>),
540}
541
542/// Fresh management evidence validated for one retained reset asset.
543struct ReinstallAssetStatus {
544    response: RootInspectionStatus,
545    controllers: Vec<String>,
546    cycles: u128,
547}
548
549#[derive(CandidType)]
550enum ManagedCanisterStatusRequest {
551    CycleBalance,
552}
553
554#[derive(CandidType, Deserialize)]
555enum ManagedCanisterStatusResponse {
556    CycleBalance(canic_core::dto::role::CycleBalanceStatusResponse),
557}
558
559struct CreateCanisterAuthority<'a> {
560    controller_canisters: &'a [String],
561    controllers: &'a [String],
562    created_at_time: u64,
563    ledger: &'a str,
564    requested_initial_cycles: u128,
565    subnet: &'a str,
566}
567
568#[derive(CandidType)]
569struct WithdrawArgs {
570    amount: Nat,
571    created_at_time: Option<u64>,
572    from_subaccount: Option<[u8; 32]>,
573    to: Principal,
574}
575
576#[derive(CandidType, Deserialize)]
577enum WithdrawError {
578    BadFee {
579        expected_fee: Nat,
580    },
581    CreatedInFuture {
582        ledger_time: u64,
583    },
584    Duplicate {
585        duplicate_of: Nat,
586    },
587    FailedToWithdraw {
588        fee_block: Option<Nat>,
589        rejection_code: RejectionCode,
590        rejection_reason: String,
591    },
592    GenericError {
593        error_code: Nat,
594        message: String,
595    },
596    InsufficientFunds {
597        balance: Nat,
598    },
599    InvalidReceiver {
600        receiver: Principal,
601    },
602    TemporarilyUnavailable,
603    TooOld,
604}
605
606#[derive(CandidType, Deserialize)]
607enum RejectionCode {
608    CanisterError,
609    CanisterReject,
610    DestinationInvalid,
611    NoError,
612    SysFatal,
613    SysTransient,
614    Unknown,
615}
616
617/// Typed failure at the current IC effect boundary.
618
619#[derive(Debug, ThisError)]
620pub enum IcpEnsurePlatformError {
621    #[error("bootstrap uninstall failed: {0}")]
622    BootstrapUninstall(#[source] Box<IcpManagementCallError>),
623    #[error(transparent)]
624    InfrastructureBootstrap(#[from] super::infrastructure_bootstrap::InfrastructureBootstrapError),
625    #[error(transparent)]
626    FundingObservation(
627        #[from] crate::fleet_ensure::model::funding_observation::FundingObservationError,
628    ),
629
630    #[error("mainnet Subnet Catalog observation failed: {0}")]
631    SubnetCatalog(#[source] Box<ic_query::subnet_catalog::SubnetCatalogLoadFailure>),
632
633    #[error("cycle arithmetic overflow while executing {0}")]
634    Arithmetic(&'static str),
635
636    #[error(
637        "management history does not prove the issued reinstall of {canister}; refusing another reset"
638    )]
639    ReinstallHistoryConflict { canister: String },
640
641    #[error("configured Candid file is not a regular file: {}", .0.display())]
642    CandidUnavailable(PathBuf),
643
644    #[error("configured effect artifact is unavailable: {}", .0.display())]
645    ArtifactUnavailable(PathBuf),
646
647    #[error("configured {kind} changed after plan review: expected {expected}, observed {actual}")]
648    ArtifactDigestMismatch {
649        actual: String,
650        expected: String,
651        kind: &'static str,
652    },
653
654    #[error("configured Principal is invalid for {field}: {value}")]
655    InvalidPrincipal { field: &'static str, value: String },
656
657    #[error("Cycles Ledger returned an unsupported fee value: {0}")]
658    InvalidLedgerFee(String),
659
660    #[error("active ICP identity is {actual}, but reviewed Fleet operator is {expected}")]
661    OperatorMismatch { actual: String, expected: String },
662
663    #[error("management status for configured canister {expected} returned identity {actual}")]
664    StatusIdentityMismatch { actual: String, expected: String },
665
666    #[error("retained Root management observation failed: {0}")]
667    RootManagement(String),
668
669    #[error("ICP status has invalid cycle balance for {canister}: {value}")]
670    InvalidStatusCycles { canister: String, value: String },
671
672    #[error("ICP status omitted exact {field} required for controlled canister {canister}")]
673    IncompleteCanisterStatus {
674        canister: String,
675        field: &'static str,
676    },
677
678    #[error(
679        "ICP returned only public non-controller status for {canister}; exact runtime and cycle evidence is unavailable"
680    )]
681    PublicCanisterStatusUnavailable { canister: String },
682
683    #[error(
684        "Root-owned observation for {canister} conflicts with reviewed {field}; no paid effect was repeated"
685    )]
686    RootOwnedObservationAuthorityConflict {
687        canister: String,
688        field: &'static str,
689    },
690
691    #[error(
692        "ICP CLI status JSON omitted canister_version for {canister}, and the exact typed \
693         effective-ID-routed management-canister status call failed: {source}; no install was \
694         issued. Restore the selected controller identity and management-status access, then \
695         resume the same reviewed plan"
696    )]
697    InstallVersionProofUnavailable {
698        canister: String,
699        #[source]
700        source: Box<IcpManagementCallError>,
701    },
702
703    #[error("typed local management-status observation failed: {0}")]
704    LocalManagementStatus(#[source] Box<IcpManagementCallError>),
705
706    #[error(
707        "completed reinstall proof for {canister} conflicts with current {field}; no install was authorized"
708    )]
709    CompletedReinstallAuthorityConflict {
710        canister: String,
711        field: &'static str,
712    },
713
714    #[error(transparent)]
715    Candid(#[from] IcpCandidCallError),
716
717    #[error(transparent)]
718    Icp(#[from] IcpCommandError),
719
720    #[error("Cycles Ledger create_canister failed: {0}")]
721    LedgerCreate(String),
722
723    #[error("Cycles Ledger duplicate does not yet identify its created canister")]
724    LedgerCreatePending,
725
726    #[error(
727        "Root {root} pool policy differs from desired input; explicitly review infrastructure reinstall before resetting it"
728    )]
729    PoolPolicyReinstallReviewRequired { root: String },
730
731    #[error("Cycles Ledger withdraw failed: {0}")]
732    LedgerWithdraw(String),
733
734    #[error("completed withdrawal balance conflicts with reviewed funding bounds: {observation:?}")]
735    NativeFundingBalanceDrift {
736        observation: Box<NativeFundingObservation>,
737    },
738
739    #[error("Cycles Ledger estate funding transfer failed: {0}")]
740    LedgerTransfer(String),
741
742    #[error(
743        "Cycles Ledger transfer fee changed from reviewed {reviewed_fee_cycles} to {expected_fee_cycles}; no transfer was accepted"
744    )]
745    LedgerTransferFeeChanged {
746        expected_fee_cycles: u128,
747        reviewed_fee_cycles: u128,
748    },
749
750    #[error(
751        "Root-authorized funding inspection for {canister} conflicts with reviewed {field}; no Ledger withdrawal was repeated"
752    )]
753    FundingInspectionAuthorityConflict {
754        canister: String,
755        field: &'static str,
756    },
757
758    #[error(
759        "canister {canister} retains {actual} cycles above deletion limit {maximum}; left untouched"
760    )]
761    MaterialCycles {
762        actual: u128,
763        canister: String,
764        maximum: u128,
765    },
766
767    #[error("canister {canister} is not stopped; deletion was not attempted")]
768    NotStopped { canister: String },
769
770    #[error("effect references unresolved created canister {0}")]
771    UnresolvedCreated(String),
772
773    #[error("configured retirement endpoint transferred {actual} cycles, expected {expected}")]
774    WrongTransferAmount { actual: Nat, expected: u128 },
775
776    #[error(
777        "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"
778    )]
779    UnbalancedTransfer {
780        canister: String,
781        destination_credit: u128,
782        expected: u128,
783        maximum_source_debit: u128,
784        source_debit: u128,
785    },
786
787    #[error("retirement transfer for {canister} is missing its durable {side} balance")]
788    MissingTransferBalance {
789        canister: String,
790        side: &'static str,
791    },
792
793    #[error("retirement transfer balance for {canister} moved in an invalid direction")]
794    InvalidTransferBalance { canister: String },
795
796    #[error(transparent)]
797    CurrentProtocol(#[from] current_protocol::CurrentProtocolError),
798
799    #[error(transparent)]
800    CanicInit(#[from] canic_init::CanicInitError),
801
802    #[error(transparent)]
803    Protocol(#[from] protocol::ProtocolEffectError),
804}
805
806/// Read evidence shared only by projections of one Fleet observation.
807#[derive(Default)]
808struct FleetObservationSnapshot {
809    pool_inspections: BTreeMap<PoolInspectionAuthority, RootInspectionStatus>,
810    pool_pages: BTreeMap<(Principal, Option<Principal>), CanisterPoolResponse>,
811    statuses: BTreeMap<String, Option<LiveCanister>>,
812}
813
814/// An inspection response belongs to the exact protected Root and requested asset.
815#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
816struct PoolInspectionAuthority {
817    root: Principal,
818    target: Principal,
819}
820
821impl PoolInspectionAuthority {
822    fn parse(root: &str, target: &str) -> Result<Self, IcpEnsurePlatformError> {
823        Ok(Self {
824            root: parse_principal("Fleet Subnet Root", root)?,
825            target: parse_principal("Root-owned inspection target", target)?,
826        })
827    }
828}
829
830/// Consumer requirements kept separate from the transport result shared by one batch.
831enum PoolInspectionRequirement {
832    PendingFresh,
833    RootControlled(InspectedModule),
834}
835
836/// Prepared only after the caller's Root and operator checks succeed.
837struct PreparedPoolInspection<'a> {
838    authority: PoolInspectionAuthority,
839    name: &'a str,
840    root: &'a str,
841    requirement: PoolInspectionRequirement,
842}
843
844/// A configured pending asset awaiting its independent management inspection.
845struct PendingPoolObservation<'a> {
846    configured: &'a crate::fleet_ensure::model::DesiredCanister,
847    principal: &'a str,
848    asset: CanisterPoolAsset,
849    inspection: PreparedPoolInspection<'a>,
850}
851
852/// Pool-page and Root checks stay ordered before independent inspections overlap.
853enum PreparedRootOwnedObservation<'a> {
854    Observed(Option<LiveCanister>),
855    Pending(Box<PendingPoolObservation<'a>>),
856}
857
858/// Completed transport outcomes local to one drained inspection batch.
859#[derive(Default)]
860struct PoolInspectionBatch {
861    responses:
862        BTreeMap<PoolInspectionAuthority, Result<RootInspectionStatus, IcpEnsurePlatformError>>,
863}
864
865/// Distinguish an unobserved identity from an observed missing canister.
866enum StatusCacheEntry {
867    Miss,
868    Hit(Option<LiveCanister>),
869}
870
871/// Cumulative diagnostic counters sampled at observation boundaries.
872struct ObservationCounters {
873    remote_calls: u64,
874    identity_lookups: u64,
875    identity_lookup_millis: u64,
876    cached_reads: u64,
877}
878
879/// Production ICP adapter for the current desired Fleet.
880pub struct IcpEnsurePlatform {
881    pool_reader: RootPoolReader,
882    infrastructure_bootstrap:
883        Option<crate::fleet_ensure::model::infrastructure_bootstrap::InfrastructureBootstrapRecord>,
884    bootstrap_registration_recovery:
885        Option<crate::fleet_ensure::model::infrastructure_bootstrap::registration_recovery::BootstrapRegistrationReviewRecord>,
886    pub(super) desired: DesiredFleet,
887    pub(super) icp: IcpCli,
888    initial_observation_delay: Duration,
889    maximum_observation_delay: Duration,
890    observation_snapshot: RefCell<Option<FleetObservationSnapshot>>,
891    staging_observations: Option<current_protocol::StoreStagingObservations>,
892    cached_reads: Cell<u64>,
893    observation_stages: Vec<(u64, FleetObservationStage)>,
894    progress_handler: Option<Box<dyn FnMut(FleetEnsureProgress)>>,
895    observation_handler: Option<Box<dyn FnMut(FleetObservationTiming)>>,
896    estate_observations: BTreeMap<String, EstateFundingDomainObservation>,
897    pub(super) root: PathBuf,
898}
899
900/// One typed read boundary; observation caching and authority stay in the platform.
901struct RootPoolReader(
902    fn(
903        &IcpCli,
904        Principal,
905        &RootPoolStatusRequest,
906    ) -> Result<RootPoolStatusResponse, CanisterProtocolError>,
907);
908
909static NEXT_SPAN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
910
911const INITIAL_PROTOCOL_OBSERVATION_DELAY: Duration = Duration::from_millis(250);
912const MAXIMUM_PROTOCOL_OBSERVATION_DELAY: Duration = Duration::from_secs(5);
913
914fn protocol_observation_delay(
915    consecutive_unchanged_observations: u32,
916    initial_observation_delay: Duration,
917    maximum_observation_delay: Duration,
918) -> Duration {
919    let exponent = consecutive_unchanged_observations.saturating_sub(1).min(5);
920    let multiplier = 1_u32.checked_shl(exponent).unwrap_or(u32::MAX);
921    initial_observation_delay
922        .checked_mul(multiplier)
923        .unwrap_or(MAXIMUM_PROTOCOL_OBSERVATION_DELAY)
924        .min(MAXIMUM_PROTOCOL_OBSERVATION_DELAY)
925        .min(maximum_observation_delay)
926}
927
928impl IcpEnsurePlatform {
929    #[must_use]
930    pub fn new(desired: DesiredFleet, icp_executable: &str, root: &Path) -> Self {
931        let icp = IcpCli::new(icp_executable, Some(desired.environment.clone()))
932            .with_cwd(root.to_path_buf());
933        Self {
934            pool_reader: RootPoolReader(|icp, root, request| {
935                query_authenticated(icp, root, canic_protocol::CANIC_ROOT_STATUS, request)
936            }),
937            desired,
938            icp,
939            infrastructure_bootstrap: None,
940            bootstrap_registration_recovery: None,
941            initial_observation_delay: INITIAL_PROTOCOL_OBSERVATION_DELAY,
942            maximum_observation_delay: MAXIMUM_PROTOCOL_OBSERVATION_DELAY,
943            observation_snapshot: RefCell::new(None),
944            staging_observations: None,
945            cached_reads: Cell::new(0),
946            observation_stages: Vec::new(),
947            progress_handler: None,
948            observation_handler: None,
949            estate_observations: BTreeMap::new(),
950            root: root.to_path_buf(),
951        }
952    }
953
954    /// Select the signer before the operation's existing Principal admission check.
955    #[must_use]
956    pub fn with_identity(mut self, identity: Option<&str>) -> Self {
957        self.icp = self.icp.with_identity(identity);
958        self
959    }
960
961    /// Attach an informational progress sink to this operation.
962    #[must_use]
963    pub fn with_progress_handler(
964        mut self,
965        handler: impl FnMut(FleetEnsureProgress) + 'static,
966    ) -> Self {
967        self.progress_handler = Some(Box::new(handler));
968        self
969    }
970
971    /// Attach informational stage timings without retaining observations across boundaries.
972    #[must_use]
973    pub fn with_observation_handler(
974        mut self,
975        handler: impl FnMut(FleetObservationTiming) + 'static,
976    ) -> Self {
977        self.observation_handler = Some(Box::new(handler));
978        self
979    }
980
981    /// Attach request timing to the same transport used by all worker clones.
982    #[must_use]
983    pub fn with_request_timing_handler(
984        mut self,
985        handler: impl Fn(crate::icp::IcpRequestTiming) + Send + Sync + 'static,
986    ) -> Self {
987        self.icp = self.icp.with_timing_handler(handler);
988        self
989    }
990
991    fn timed_observation<T>(
992        &mut self,
993        stage: FleetObservationStage,
994        observe: impl FnOnce(&mut Self) -> Result<T, IcpEnsurePlatformError>,
995    ) -> Result<T, IcpEnsurePlatformError> {
996        self.measure_observation(stage, observe)
997    }
998
999    fn measure_observation<T, E>(
1000        &mut self,
1001        stage: FleetObservationStage,
1002        observe: impl FnOnce(&mut Self) -> Result<T, E>,
1003    ) -> Result<T, E> {
1004        let started = std::time::Instant::now();
1005        let before = ObservationCounters {
1006            remote_calls: self.icp.remote_call_count(),
1007            identity_lookups: self.icp.identity_lookup_count(),
1008            identity_lookup_millis: self.icp.identity_lookup_millis(),
1009            cached_reads: self.cached_reads.get(),
1010        };
1011        let span_id = NEXT_SPAN.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1012        let parent = self.observation_stages.last().copied();
1013        let mut timing = FleetObservationTiming {
1014            span_id,
1015            parent_span_id: parent.map(|entry| entry.0),
1016            stage,
1017            parent_stage: parent.map(|entry| entry.1),
1018            elapsed_millis: 0,
1019            remote_call_attempts: 0,
1020            identity_lookup_attempts: 0,
1021            identity_lookup_millis: 0,
1022            cached_read_hits: 0,
1023            succeeded: None,
1024        };
1025        if let Some(handler) = &mut self.observation_handler {
1026            handler(timing.clone());
1027        }
1028        self.observation_stages.push((span_id, stage));
1029        let result = observe(self);
1030        self.observation_stages.pop();
1031        timing.elapsed_millis = started.elapsed().as_millis();
1032        timing.remote_call_attempts = self
1033            .icp
1034            .remote_call_count()
1035            .saturating_sub(before.remote_calls);
1036        timing.identity_lookup_attempts = self
1037            .icp
1038            .identity_lookup_count()
1039            .saturating_sub(before.identity_lookups);
1040        timing.identity_lookup_millis = self
1041            .icp
1042            .identity_lookup_millis()
1043            .saturating_sub(before.identity_lookup_millis);
1044        timing.cached_read_hits = self.cached_reads.get().saturating_sub(before.cached_reads);
1045        timing.succeeded = Some(result.is_ok());
1046        if let Some(handler) = &mut self.observation_handler {
1047            handler(timing);
1048        }
1049        result
1050    }
1051
1052    /// Return this adapter bound to one explicit local replica endpoint.
1053    ///
1054    /// This keeps every ordinary ICP CLI call and effective-ID management
1055    /// observation on the same replica when no named ICP project network owns
1056    /// the test or operator session.
1057    #[must_use]
1058    pub fn with_local_replica(mut self, target: LocalReplicaTarget) -> Self {
1059        self.icp = self.icp.with_local_replica(Some(target));
1060        self
1061    }
1062
1063    /// Select passive observation pacing without changing retry or stall bounds.
1064    ///
1065    /// Production callers retain the default five-second cap. Deterministic
1066    /// test replicas may select shorter fixed delays while retaining every
1067    /// production observation and terminal predicate.
1068    #[doc(hidden)]
1069    #[must_use]
1070    pub fn with_observation_delay_bounds(mut self, initial: Duration, maximum: Duration) -> Self {
1071        self.initial_observation_delay = initial.min(maximum);
1072        self.maximum_observation_delay = maximum;
1073        self
1074    }
1075
1076    // Nested stages share one explicit read-only scope. The owner expires it on
1077    // every Result path; retries and effects invalidate it independently.
1078    fn with_observation_snapshot<T>(
1079        &mut self,
1080        observe: impl FnOnce(&mut Self) -> Result<T, IcpEnsurePlatformError>,
1081    ) -> Result<T, IcpEnsurePlatformError> {
1082        self.with_readonly_snapshot(observe)
1083    }
1084
1085    fn with_readonly_snapshot<T, E>(
1086        &mut self,
1087        observe: impl FnOnce(&mut Self) -> Result<T, E>,
1088    ) -> Result<T, E> {
1089        if self.observation_snapshot.borrow().is_some() {
1090            return observe(self);
1091        }
1092        self.observation_snapshot
1093            .replace(Some(FleetObservationSnapshot::default()));
1094        let result = observe(self);
1095        self.observation_snapshot.take();
1096        result
1097    }
1098
1099    fn invalidate_observation_snapshot(&self) {
1100        if let Some(snapshot) = self.observation_snapshot.borrow_mut().as_mut() {
1101            *snapshot = FleetObservationSnapshot::default();
1102        }
1103    }
1104
1105    fn record_cached_read(&self) {
1106        self.cached_reads
1107            .set(self.cached_reads.get().saturating_add(1));
1108    }
1109
1110    pub(super) fn require_operator(&self) -> Result<(), IcpEnsurePlatformError> {
1111        self.icp.bind_selected_identity()?;
1112        let actual = self.icp.identity_principal_text()?;
1113        if actual != self.desired.operator {
1114            return Err(IcpEnsurePlatformError::OperatorMismatch {
1115                actual,
1116                expected: self.desired.operator.clone(),
1117            });
1118        }
1119        Ok(())
1120    }
1121
1122    fn current_principal<'a>(
1123        &'a self,
1124        state: &'a FleetEnsureStateRecord,
1125        name: &str,
1126    ) -> Option<&'a str> {
1127        state
1128            .pending_principals
1129            .get(name)
1130            .or_else(|| state.principals.get(name))
1131            .map(String::as_str)
1132            .or_else(|| {
1133                self.desired
1134                    .canisters
1135                    .iter()
1136                    .find(|configured| configured.name == name)
1137                    .and_then(|configured| configured.principal.as_deref())
1138            })
1139    }
1140
1141    fn protocol_principals(&self, state: &FleetEnsureStateRecord) -> BTreeMap<String, String> {
1142        self.desired
1143            .canisters
1144            .iter()
1145            .filter_map(|configured| {
1146                state
1147                    .pending_principals
1148                    .get(&configured.name)
1149                    .or_else(|| state.principals.get(&configured.name))
1150                    .or(configured.principal.as_ref())
1151                    .map(|principal| (configured.name.clone(), principal.clone()))
1152            })
1153            .collect()
1154    }
1155
1156    fn current_protocol_owners_are_ready(
1157        &self,
1158        state: &FleetEnsureStateRecord,
1159    ) -> Result<bool, IcpEnsurePlatformError> {
1160        let configured = self
1161            .desired
1162            .canisters
1163            .iter()
1164            .filter(|configured| {
1165                configured.presence == crate::fleet_ensure::model::DesiredPresence::Present
1166                    && matches!(
1167                        configured.kind,
1168                        DesiredCanisterKind::Coordinator
1169                            | DesiredCanisterKind::Root
1170                            | DesiredCanisterKind::Store
1171                    )
1172            })
1173            .collect::<Vec<_>>();
1174        let mut owners = Vec::with_capacity(configured.len());
1175        for batch in configured.chunks(super::bounded_observations::MAX_IN_FLIGHT) {
1176            let mut statuses = self.read_status_batch(
1177                batch
1178                    .iter()
1179                    .filter_map(|owner| self.current_principal(state, &owner.name)),
1180            );
1181            // Consume in configured order after draining this batch. A stopped owner
1182            // still precedes a later transport error; neither starts another batch.
1183            for configured in batch {
1184                let live = self
1185                    .current_principal(state, &configured.name)
1186                    .map(|principal| self.status_batch_response(&mut statuses, principal))
1187                    .transpose()?
1188                    .flatten();
1189                if live
1190                    .as_ref()
1191                    .is_some_and(|live| live.status != CanisterRuntimeStatus::Running)
1192                {
1193                    return Ok(false);
1194                }
1195                owners.push((*configured, live));
1196            }
1197        }
1198        // Check running state and module identity from the same fresh status read.
1199        // Keep missing-owner/module decisions after the status scan so transport
1200        // failures retain their existing precedence.
1201        for (configured, live) in owners {
1202            let Some(live) = live else {
1203                return Ok(false);
1204            };
1205            let Some(wasm) = configured.wasm.as_deref() else {
1206                return Ok(false);
1207            };
1208            let expected = artifact_hash(&resolve_path(&self.root, wasm))?;
1209            if live.module_sha256.as_deref() != Some(expected.as_str()) {
1210                return Ok(false);
1211            }
1212        }
1213        Ok(true)
1214    }
1215
1216    fn observe_estate_funding_domains(
1217        &self,
1218        state: &FleetEnsureStateRecord,
1219    ) -> Result<BTreeMap<String, EstateFundingDomainObservation>, IcpEnsurePlatformError> {
1220        let Some(bootstrap) = self.desired.bootstrap.as_ref() else {
1221            return Ok(BTreeMap::new());
1222        };
1223        bootstrap
1224            .roots
1225            .iter()
1226            .map(|root| {
1227                let root_principal = self.current_principal(state, &root.root);
1228                let balance_cycles = root_principal
1229                    .map(|principal| self.cycles_ledger_balance(principal))
1230                    .transpose()?;
1231                let pool = root_principal
1232                    .map(|principal| {
1233                        self.observe_estate_pool_inventory(&root.root, principal, state)
1234                    })
1235                    .transpose()?
1236                    .flatten();
1237                Ok((
1238                    root.root.clone(),
1239                    EstateFundingDomainObservation {
1240                        balance_cycles,
1241                        cycles_ledger: self.desired.cycles_ledger.clone(),
1242                        pool,
1243                        root_principal: root_principal.map(str::to_string),
1244                    },
1245                ))
1246            })
1247            .collect()
1248    }
1249
1250    fn cycles_ledger_balance(&self, owner: &str) -> Result<u128, IcpEnsurePlatformError> {
1251        let balance: Nat = self.icp.canister_query_candid(
1252            &self.desired.cycles_ledger,
1253            "icrc1_balance_of",
1254            &CyclesLedgerAccount {
1255                owner: parse_principal("Cycles Ledger account owner", owner)?,
1256                subaccount: None,
1257            },
1258            None,
1259        )?;
1260        ledger_fee_cycles(balance)
1261    }
1262
1263    fn refresh_estate_funding_observation(
1264        &self,
1265        observation: &mut EffectObservation,
1266        state: &FleetEnsureStateRecord,
1267    ) -> Result<(), IcpEnsurePlatformError> {
1268        let Some(required) = observation.estate_funding_required.as_mut() else {
1269            return Ok(());
1270        };
1271        let root = required.root.to_text();
1272        let known_root = self.desired.canisters.iter().any(|canister| {
1273            canister.kind == DesiredCanisterKind::Root
1274                && self.current_principal(state, &canister.name) == Some(root.as_str())
1275        });
1276        if !known_root || required.cycles_ledger.to_text() != self.desired.cycles_ledger {
1277            return Err(current_protocol::CurrentProtocolError::ResponseMismatch.into());
1278        }
1279        // Root retains the balance from its last creation attempt until its next
1280        // retry. A completed host transfer can therefore precede this status.
1281        let available = self.cycles_ledger_balance(&root)?;
1282        let debit = required.required.to_u128();
1283        if available >= debit {
1284            observation.estate_funding_required = None;
1285        } else {
1286            required.available = Cycles::new(available);
1287            required.shortfall = Cycles::new(debit - available);
1288        }
1289        Ok(())
1290    }
1291
1292    fn require_current_pool_policy(
1293        &self,
1294        domains: &BTreeMap<String, EstateFundingDomainObservation>,
1295    ) -> Result<(), IcpEnsurePlatformError> {
1296        let Some(bootstrap) = self.desired.bootstrap.as_ref() else {
1297            return Ok(());
1298        };
1299        for root in &bootstrap.roots {
1300            let Some(pool) = domains
1301                .get(&root.root)
1302                .and_then(|domain| domain.pool.as_ref())
1303            else {
1304                continue;
1305            };
1306            if !pool_policy_is_current(pool, &root.limits.canister_pool) {
1307                return Err(IcpEnsurePlatformError::PoolPolicyReinstallReviewRequired {
1308                    root: root.root.clone(),
1309                });
1310            }
1311        }
1312        Ok(())
1313    }
1314
1315    fn observe_estate_pool_inventory(
1316        &self,
1317        root_name: &str,
1318        root: &str,
1319        state: &FleetEnsureStateRecord,
1320    ) -> Result<Option<EstatePoolInventoryObservation>, IcpEnsurePlatformError> {
1321        let live = self.required_root_canister(root_name, root)?;
1322        if live.status != CanisterRuntimeStatus::Running {
1323            return Ok(None);
1324        }
1325        let fresh_initialization = self
1326            .desired
1327            .bootstrap
1328            .as_ref()
1329            .is_some_and(|bootstrap| bootstrap.fresh_estate)
1330            && state.active_registry.is_none();
1331        let allocated_by_this_operation = self.desired.canisters.iter().any(|configured| {
1332            configured.name == root_name
1333                && configured.kind == DesiredCanisterKind::Root
1334                && configured.principal.is_none()
1335        });
1336        if live.module_sha256.is_none() && fresh_initialization && allocated_by_this_operation {
1337            // A short Create result can require review before installation. No
1338            // protocol exists yet; unavailable inventory must remain None.
1339            return Ok(None);
1340        }
1341        self.root_protocol_candid()?;
1342        let root_principal = parse_principal("Fleet Subnet Root", root)?;
1343        let mut start_after = None;
1344        let mut inventory = EstatePoolInventoryAccumulator::default();
1345        loop {
1346            let page = self.query_estate_pool_page(root_principal, start_after)?;
1347            let next = inventory.observe_page(root_name, page)?;
1348            if next.is_none() {
1349                break;
1350            }
1351            if next == start_after {
1352                return Err(pool_configuration_error(format!(
1353                    "Root {root_name} pool cursor did not advance"
1354                )));
1355            }
1356            start_after = next;
1357        }
1358        inventory.finish(root_name).map(Some)
1359    }
1360
1361    pub(super) fn query_estate_pool_page(
1362        &self,
1363        root: Principal,
1364        start_after: Option<Principal>,
1365    ) -> Result<CanisterPoolResponse, IcpEnsurePlatformError> {
1366        let key = (root, start_after);
1367        if let Some(page) = self
1368            .observation_snapshot
1369            .borrow()
1370            .as_ref()
1371            .and_then(|snapshot| snapshot.pool_pages.get(&key).cloned())
1372        {
1373            self.record_cached_read();
1374            return Ok(page);
1375        }
1376        let response = (self.pool_reader.0)(
1377            &self.icp,
1378            root,
1379            &RootPoolStatusRequest::Pool(CanisterPoolStatusRequest {
1380                start_after,
1381                limit: 256,
1382            }),
1383        )
1384        .map_err(current_protocol::CurrentProtocolError::from)?;
1385        let RootPoolStatusResponse::Pool(page) = response;
1386        if let Some(snapshot) = self.observation_snapshot.borrow_mut().as_mut() {
1387            snapshot.pool_pages.insert(key, (*page).clone());
1388        }
1389        Ok(*page)
1390    }
1391
1392    fn reinstall_assets_match_bound(
1393        icp: &IcpCli,
1394        candid: &Path,
1395        principal: Principal,
1396        assets: &[&crate::fleet_ensure::model::FleetReinstallAssetRecord],
1397        check: super::ReinstallAssetCheck,
1398    ) -> Result<bool, IcpEnsurePlatformError> {
1399        let result =
1400            super::bounded_observations::collect(
1401                assets,
1402                |asset| match Self::reinstall_asset_matches(icp, candid, principal, asset, check) {
1403                    Ok(true) => Ok(()),
1404                    Ok(false) => Err(ReinstallAssetVerificationError::Mismatch),
1405                    Err(error) => Err(ReinstallAssetVerificationError::Read(Box::new(error))),
1406                },
1407            );
1408        match result {
1409            Ok(_) => Ok(true),
1410            Err(ReinstallAssetVerificationError::Mismatch) => Ok(false),
1411            Err(ReinstallAssetVerificationError::Read(error)) => Err(*error),
1412        }
1413    }
1414
1415    fn reinstall_asset_matches(
1416        icp: &IcpCli,
1417        candid: &Path,
1418        principal: Principal,
1419        asset: &crate::fleet_ensure::model::FleetReinstallAssetRecord,
1420        check: super::ReinstallAssetCheck,
1421    ) -> Result<bool, IcpEnsurePlatformError> {
1422        let target = parse_principal("reset asset", &asset.principal)?;
1423        icp.measure_canister_inspection(principal, target, || {
1424            crate::canister_protocol::inspection::preflight_inspection(
1425                icp, candid, principal, target,
1426            )
1427            .map_err(current_protocol::CurrentProtocolError::from)?;
1428            let response: RootInspectionResponse = call_with_candid(
1429                icp,
1430                candid,
1431                principal,
1432                canic_protocol::CANIC_ROOT_COMMAND,
1433                &RootInspectionCommand::InspectCanister(CanisterInspectionRequest {
1434                    canister_id: target,
1435                }),
1436            )
1437            .map_err(current_protocol::CurrentProtocolError::from)?;
1438            let status = response
1439                .into_status(principal, target)
1440                .map_err(current_protocol::CurrentProtocolError::from)?;
1441            let mut controllers = status
1442                .settings
1443                .controllers
1444                .iter()
1445                .map(Principal::to_text)
1446                .collect::<Vec<_>>();
1447            controllers.sort();
1448            let module = status
1449                .module_hash
1450                .as_ref()
1451                .map(canic_core::cdk::utils::hash::hex_bytes);
1452            let module_matches =
1453                check == super::ReinstallAssetCheck::Terminal || module == asset.module_sha256;
1454            Ok(controllers == asset.controllers && module_matches)
1455        })
1456    }
1457
1458    #[expect(
1459        clippy::too_many_lines,
1460        reason = "one inspection binds each source asset to exact management authority and cycles"
1461    )]
1462    fn inspect_reinstall_assets(
1463        &self,
1464        state: &FleetEnsureStateRecord,
1465        authorities: &BTreeMap<String, RootManagementCanisterObservation>,
1466        observation: &mut FleetObservation,
1467        candid_by_root: &BTreeMap<String, PathBuf>,
1468        allow_pending_reset: bool,
1469    ) -> Result<Vec<crate::fleet_ensure::model::FleetReinstallAssetRecord>, IcpEnsurePlatformError>
1470    {
1471        let mut assets = Vec::new();
1472        let mut seen = BTreeSet::new();
1473        for (root_name, domain) in &observation.estate_funding_domains {
1474            let root = domain.root_principal.as_deref().ok_or_else(|| {
1475                IcpEnsurePlatformError::RootManagement("missing Root principal".to_string())
1476            })?;
1477            let pool = domain.pool.as_ref().ok_or_else(|| {
1478                IcpEnsurePlatformError::RootManagement("missing complete pool".to_string())
1479            })?;
1480            if pool.pending_creation.is_some() {
1481                return Err(IcpEnsurePlatformError::RootManagement(
1482                    "pool creation is still in flight".to_string(),
1483                ));
1484            }
1485            let root_authority = authorities.get(root_name).ok_or_else(|| {
1486                IcpEnsurePlatformError::RootManagement("missing Root binding".to_string())
1487            })?;
1488            let recovery_controllers = self
1489                .desired
1490                .bootstrap
1491                .as_ref()
1492                .map_or(&[][..], |bootstrap| {
1493                    bootstrap.recovery_controllers.as_slice()
1494                });
1495            for batch in pool
1496                .assets
1497                .chunks(super::bounded_observations::MAX_IN_FLIGHT)
1498            {
1499                // Record uniqueness in inventory order, then drain each issued batch
1500                // before mutating the observation or scheduling another inspection.
1501                let requests = batch
1502                    .iter()
1503                    .map(|asset| (asset, seen.insert(asset.principal.clone())))
1504                    .collect::<Vec<_>>();
1505                let icp = &self.icp;
1506                let responses =
1507                    super::bounded_observations::collect(&requests, |(asset, unique)| {
1508                        Self::inspect_reinstall_asset(
1509                            icp,
1510                            candid_by_root.get(root_name).map(PathBuf::as_path),
1511                            root,
1512                            asset,
1513                            *unique,
1514                            allow_pending_reset,
1515                            recovery_controllers,
1516                        )
1517                    })?;
1518                for (asset, inspected) in batch.iter().zip(responses) {
1519                    let ReinstallAssetStatus {
1520                        response,
1521                        controllers,
1522                        cycles,
1523                    } = inspected;
1524
1525                    let module_sha256 = response
1526                        .module_hash
1527                        .as_ref()
1528                        .map(canic_core::cdk::utils::hash::hex_bytes);
1529                    let configured_name = self
1530                        .desired
1531                        .canisters
1532                        .iter()
1533                        .find(|c| {
1534                            c.principal.as_deref() == Some(&asset.principal)
1535                                || self.current_principal(state, &c.name) == Some(&asset.principal)
1536                        })
1537                        .map(|c| c.name.clone());
1538                    let name = configured_name
1539                        .unwrap_or_else(|| format!("reinstall-pool-{}", asset.principal));
1540                    observation.canisters.insert(
1541                        name,
1542                        Some(LiveCanister {
1543                            canister_version: None,
1544                            controllers: controllers.clone(),
1545                            cycles,
1546                            module_sha256: module_sha256.clone(),
1547                            principal: asset.principal.clone(),
1548                            reinstall_required: false,
1549                            root_owned_lifecycle: Some(match asset.lifecycle {
1550                                EstatePoolAssetLifecycle::Ready => RootOwnedCanisterLifecycle::Idle,
1551                                EstatePoolAssetLifecycle::Workload => {
1552                                    RootOwnedCanisterLifecycle::Workload
1553                                }
1554                                _ => RootOwnedCanisterLifecycle::Retained,
1555                            }),
1556                            status: match response.status {
1557                                canic_core::dto::canister::CanisterStatusType::Running => {
1558                                    CanisterRuntimeStatus::Running
1559                                }
1560                                canic_core::dto::canister::CanisterStatusType::Stopped => {
1561                                    CanisterRuntimeStatus::Stopped
1562                                }
1563                                canic_core::dto::canister::CanisterStatusType::Stopping => {
1564                                    CanisterRuntimeStatus::Stopping
1565                                }
1566                            },
1567                        }),
1568                    );
1569                    observation
1570                        .additional_controlled_cycles
1571                        .remove(&asset.principal);
1572                    assets.push(crate::fleet_ensure::model::FleetReinstallAssetRecord {
1573                        controllers,
1574                        module_sha256,
1575                        principal: asset.principal.clone(),
1576                        root: root_name.clone(),
1577                        subnet: root_authority.subnet.clone(),
1578                    });
1579                }
1580            }
1581        }
1582        assets.sort_by(|a, b| a.principal.cmp(&b.principal));
1583        Ok(assets)
1584    }
1585
1586    fn inspect_reinstall_asset(
1587        icp: &IcpCli,
1588        candid: Option<&Path>,
1589        root: &str,
1590        asset: &EstatePoolAssetObservation,
1591        unique: bool,
1592        allow_pending_reset: bool,
1593        recovery_controllers: &[Principal],
1594    ) -> Result<ReinstallAssetStatus, IcpEnsurePlatformError> {
1595        let terminal = matches!(
1596            asset.lifecycle,
1597            EstatePoolAssetLifecycle::Ready | EstatePoolAssetLifecycle::Workload
1598        );
1599        let recoverable = allow_pending_reset
1600            && matches!(
1601                asset.lifecycle,
1602                EstatePoolAssetLifecycle::PendingReset | EstatePoolAssetLifecycle::Failed
1603            );
1604        if !unique || !(terminal || recoverable) {
1605            return Err(IcpEnsurePlatformError::RootManagement(
1606                "pool asset is duplicated or not terminal".to_string(),
1607            ));
1608        }
1609        let authority = PoolInspectionAuthority {
1610            target: parse_principal("reset pool asset", &asset.principal)?,
1611            root: parse_principal("reset Root", root)?,
1612        };
1613        let candid = candid
1614            .ok_or_else(|| pool_configuration_error("missing reset read contract".to_string()))?;
1615        // Reset review always fetches fresh evidence; it does not consume the
1616        // earlier observation's balance or inspection cache.
1617        let response = Self::fetch_pool_inspection(icp, candid, authority)?;
1618        let mut controllers = response
1619            .settings
1620            .controllers
1621            .iter()
1622            .map(Principal::to_text)
1623            .collect::<Vec<_>>();
1624        if !controllers.iter().any(|controller| controller == root) {
1625            return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1626                canister: asset.principal.clone(),
1627                field: "Root controller",
1628            });
1629        }
1630        let cycles = validate_inspected_cycles(
1631            &asset.principal,
1632            InspectedModule::Any,
1633            response.module_hash.as_deref(),
1634            &response.cycles,
1635        )?;
1636        controllers.sort();
1637        let mut expected = recovery_controllers
1638            .iter()
1639            .map(Principal::to_text)
1640            .collect::<Vec<_>>();
1641        expected.push(root.to_string());
1642        expected.sort();
1643        expected.dedup();
1644        if controllers != expected {
1645            return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1646                canister: asset.principal.clone(),
1647                field: "exact Root controllers",
1648            });
1649        }
1650        Ok(ReinstallAssetStatus {
1651            response,
1652            controllers,
1653            cycles,
1654        })
1655    }
1656
1657    fn resolved_controllers(
1658        &self,
1659        state: &FleetEnsureStateRecord,
1660        controllers: &[String],
1661        controller_canisters: &[String],
1662    ) -> Result<Vec<String>, IcpEnsurePlatformError> {
1663        let mut resolved = controllers.to_vec();
1664        for name in controller_canisters {
1665            resolved.push(
1666                self.current_principal(state, name)
1667                    .ok_or_else(|| IcpEnsurePlatformError::UnresolvedCreated(name.clone()))?
1668                    .to_string(),
1669            );
1670        }
1671        resolved.sort();
1672        resolved.dedup();
1673        Ok(resolved)
1674    }
1675
1676    fn observed_protocol_action(
1677        &self,
1678        step: &crate::fleet_ensure::model::DesiredProtocolStep,
1679        principal: String,
1680    ) -> Result<EnsureAction, IcpEnsurePlatformError> {
1681        Ok(EnsureAction::Protocol {
1682            candid: step.candid.clone(),
1683            candid_sha256: artifact_hash(&resolve_path(&self.root, &step.candid))?,
1684            command_args: step.command_args.clone(),
1685            command_args_sha256: artifact_hash(&resolve_path(&self.root, &step.command_args))?,
1686            command_method: step.command_method.clone(),
1687            expected_status: step.expected_status.clone(),
1688            expected_status_sha256: artifact_hash(&resolve_path(
1689                &self.root,
1690                &step.expected_status,
1691            ))?,
1692            maximum_execution_burn_cycles: step
1693                .maximum_execution_burn_cycles
1694                .parse()
1695                .map_err(|_| IcpEnsurePlatformError::Arithmetic("protocol burn"))?,
1696            name: step.name.clone(),
1697            principal,
1698            status_args: step.status_args.clone(),
1699            status_args_sha256: artifact_hash(&resolve_path(&self.root, &step.status_args))?,
1700            status_method: step.status_method.clone(),
1701        })
1702    }
1703
1704    fn action_principal<'a>(
1705        state: &'a FleetEnsureStateRecord,
1706        principal: &'a str,
1707    ) -> Result<&'a str, IcpEnsurePlatformError> {
1708        if let Some(name) = principal.strip_prefix("created:") {
1709            return state
1710                .pending_principals
1711                .get(name)
1712                .map(String::as_str)
1713                .ok_or_else(|| IcpEnsurePlatformError::UnresolvedCreated(name.to_string()));
1714        }
1715        Ok(principal)
1716    }
1717
1718    fn status_optional(
1719        &self,
1720        principal: &str,
1721    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1722        if let Some(cached) = self
1723            .observation_snapshot
1724            .borrow()
1725            .as_ref()
1726            .and_then(|snapshot| snapshot.statuses.get(principal).cloned())
1727        {
1728            self.record_cached_read();
1729            return Ok(cached);
1730        }
1731        let observed = self.read_status_optional(principal)?;
1732        if let Some(snapshot) = self.observation_snapshot.borrow_mut().as_mut() {
1733            snapshot
1734                .statuses
1735                .insert(principal.to_string(), observed.clone());
1736        }
1737        Ok(observed)
1738    }
1739
1740    pub(super) fn read_status_optional(
1741        &self,
1742        principal: &str,
1743    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1744        Self::read_status_with(&self.icp, principal)
1745    }
1746
1747    fn read_status_with(
1748        icp: &IcpCli,
1749        principal: &str,
1750    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1751        if icp.uses_direct_local_replica() {
1752            return Self::direct_local_status_optional(icp, principal);
1753        }
1754        let report = match icp.canister_status_report(principal) {
1755            Ok(report) => report,
1756            Err(error)
1757                if matches!(
1758                    error.diagnostic(),
1759                    Some(IcpDiagnostic::CanisterNotFound { .. })
1760                ) =>
1761            {
1762                return Ok(None);
1763            }
1764            Err(error) => return Err(error.into()),
1765        };
1766        if report.id != principal {
1767            return Err(IcpEnsurePlatformError::StatusIdentityMismatch {
1768                actual: report.id,
1769                expected: principal.to_string(),
1770            });
1771        }
1772        reject_public_status_projection(&report, principal)?;
1773        let status_text =
1774            report
1775                .status
1776                .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
1777                    canister: principal.to_string(),
1778                    field: "status",
1779                })?;
1780        let cycles_text =
1781            report
1782                .cycles
1783                .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
1784                    canister: principal.to_string(),
1785                    field: "cycles",
1786                })?;
1787        let cycles = parse_status_cycles(&cycles_text).ok_or_else(|| {
1788            IcpEnsurePlatformError::InvalidStatusCycles {
1789                canister: principal.to_string(),
1790                value: cycles_text,
1791            }
1792        })?;
1793        let mut controllers = report
1794            .settings
1795            .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
1796                canister: principal.to_string(),
1797                field: "controllers",
1798            })?
1799            .controllers;
1800        controllers.sort();
1801        let status = match status_text.to_ascii_lowercase().as_str() {
1802            "stopped" => CanisterRuntimeStatus::Stopped,
1803            "stopping" => CanisterRuntimeStatus::Stopping,
1804            _ => CanisterRuntimeStatus::Running,
1805        };
1806        Ok(Some(LiveCanister {
1807            canister_version: report.canister_version,
1808            controllers,
1809            cycles,
1810            module_sha256: report.module_hash.map(|hash| normalize_hash(&hash)),
1811            principal: principal.to_string(),
1812            reinstall_required: false,
1813            root_owned_lifecycle: None,
1814            status,
1815        }))
1816    }
1817
1818    fn direct_local_status_optional(
1819        icp: &IcpCli,
1820        principal: &str,
1821    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1822        let canister_id = parse_principal("local canister status target", principal)?;
1823        let response = icp
1824            .management_canister_status_candid::<_, ManagementCanisterObservationResponse>(
1825                canister_id,
1826                &ManagementCanisterStatusRequest { canister_id },
1827            );
1828        let response = match response {
1829            Ok(response) => response,
1830            Err(error)
1831                if matches!(
1832                    crate::icp::classify_icp_diagnostic(&error.to_string()),
1833                    Some(IcpDiagnostic::CanisterNotFound { .. })
1834                ) =>
1835            {
1836                return Ok(None);
1837            }
1838            Err(error) => {
1839                return Err(IcpEnsurePlatformError::LocalManagementStatus(Box::new(
1840                    error,
1841                )));
1842            }
1843        };
1844        let cycles_text = response.cycles.to_string();
1845        let cycles = u128::try_from(response.cycles.0).map_err(|_| {
1846            IcpEnsurePlatformError::InvalidStatusCycles {
1847                canister: principal.to_string(),
1848                value: cycles_text,
1849            }
1850        })?;
1851        let mut controllers = response
1852            .settings
1853            .controllers
1854            .into_iter()
1855            .map(|controller| controller.to_text())
1856            .collect::<Vec<_>>();
1857        controllers.sort();
1858        controllers.dedup();
1859        let status = match response.status {
1860            ManagementCanisterRuntimeStatus::Running => CanisterRuntimeStatus::Running,
1861            ManagementCanisterRuntimeStatus::Stopping => CanisterRuntimeStatus::Stopping,
1862            ManagementCanisterRuntimeStatus::Stopped => CanisterRuntimeStatus::Stopped,
1863        };
1864        Ok(Some(LiveCanister {
1865            canister_version: Some(response.version),
1866            controllers,
1867            cycles,
1868            module_sha256: response
1869                .module_hash
1870                .map(|hash| canic_core::cdk::utils::hash::hex_bytes(&hash)),
1871            principal: principal.to_string(),
1872            reinstall_required: false,
1873            root_owned_lifecycle: None,
1874            status,
1875        }))
1876    }
1877
1878    fn inspect_root_owned_canister(
1879        &self,
1880        configured: &crate::fleet_ensure::model::DesiredCanister,
1881        principal: &str,
1882        state: &FleetEnsureStateRecord,
1883    ) -> Result<Option<RootInspectionStatus>, IcpEnsurePlatformError> {
1884        self.root_owned_inspection_authority(configured, principal, state)?
1885            .map(|authority| self.read_pool_inspection(authority))
1886            .transpose()
1887    }
1888
1889    fn root_owned_inspection_authority(
1890        &self,
1891        configured: &crate::fleet_ensure::model::DesiredCanister,
1892        principal: &str,
1893        state: &FleetEnsureStateRecord,
1894    ) -> Result<Option<PoolInspectionAuthority>, IcpEnsurePlatformError> {
1895        let parent = configured.parent.as_deref().ok_or_else(|| {
1896            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1897                canister: configured.name.clone(),
1898                field: "Root parent",
1899            }
1900        })?;
1901        let root = self.current_principal(state, parent).ok_or_else(|| {
1902            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1903                canister: configured.name.clone(),
1904                field: "Root Principal",
1905            }
1906        })?;
1907        let root_configured = self
1908            .desired
1909            .canisters
1910            .iter()
1911            .find(|candidate| {
1912                candidate.name == parent && candidate.kind == DesiredCanisterKind::Root
1913            })
1914            .ok_or_else(
1915                || IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1916                    canister: configured.name.clone(),
1917                    field: "Root desired authority",
1918                },
1919            )?;
1920        let root_wasm = root_configured.wasm.as_deref().ok_or_else(|| {
1921            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1922                canister: configured.name.clone(),
1923                field: "Root artifact",
1924            }
1925        })?;
1926        let expected_root_module = artifact_hash(&resolve_path(&self.root, root_wasm))?;
1927        let root_live = self.status_optional(root)?.ok_or_else(|| {
1928            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1929                canister: configured.name.clone(),
1930                field: "live Root",
1931            }
1932        })?;
1933        let expected_root_controllers = self.resolved_controllers(
1934            state,
1935            &root_configured.controllers,
1936            &root_configured.controller_canisters,
1937        )?;
1938        if root_live.controllers != expected_root_controllers {
1939            return Err(
1940                IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1941                    canister: configured.name.clone(),
1942                    field: "Root controllers",
1943                },
1944            );
1945        }
1946        let Some(root_module) = root_live.module_sha256.as_deref() else {
1947            return Ok(None);
1948        };
1949        if root_module != expected_root_module {
1950            return Err(
1951                IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1952                    canister: configured.name.clone(),
1953                    field: "Root module SHA-256",
1954                },
1955            );
1956        }
1957        if root_live.status != CanisterRuntimeStatus::Running {
1958            return Ok(None);
1959        }
1960        self.require_operator()?;
1961        PoolInspectionAuthority::parse(root, principal).map(Some)
1962    }
1963
1964    fn created_canister_cycles(
1965        &self,
1966        name: &str,
1967        principal: &str,
1968        state: &FleetEnsureStateRecord,
1969    ) -> Result<(Option<u128>, bool), IcpEnsurePlatformError> {
1970        match self.status_optional(principal) {
1971            Ok(live) => return Ok((live.map(|live| live.cycles), false)),
1972            Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable { .. }) => {}
1973            Err(error) => return Err(error),
1974        }
1975        let configured = self
1976            .desired
1977            .canisters
1978            .iter()
1979            .find(|configured| configured.name == name)
1980            .ok_or_else(
1981                || IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1982                    canister: name.to_string(),
1983                    field: "desired canister",
1984                },
1985            )?;
1986        if configured.kind != DesiredCanisterKind::Pool {
1987            return Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable {
1988                canister: principal.to_string(),
1989            });
1990        }
1991        let Some(response) = self.inspect_root_owned_canister(configured, principal, state)? else {
1992            return Ok((None, true));
1993        };
1994        let parent = configured.parent.as_deref().ok_or_else(|| {
1995            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
1996                canister: name.to_string(),
1997                field: "Root parent",
1998            }
1999        })?;
2000        let root = self.current_principal(state, parent).ok_or_else(|| {
2001            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
2002                canister: name.to_string(),
2003                field: "Root Principal",
2004            }
2005        })?;
2006        let cycles = validate_root_controlled_inspection(
2007            name,
2008            InspectedModule::Empty,
2009            root,
2010            &response.settings.controllers,
2011            response.module_hash.as_deref(),
2012            &response.cycles,
2013            self.desired
2014                .bootstrap
2015                .as_ref()
2016                .map_or(&[], |bootstrap| &bootstrap.recovery_controllers),
2017        )?;
2018        Ok((Some(cycles), false))
2019    }
2020
2021    fn install_status_optional(
2022        &self,
2023        principal: &str,
2024    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
2025        let Some(live) = self.status_optional(principal)? else {
2026            return Ok(None);
2027        };
2028        self.complete_install_status(principal, live).map(Some)
2029    }
2030
2031    fn complete_install_status(
2032        &self,
2033        principal: &str,
2034        mut live: LiveCanister,
2035    ) -> Result<LiveCanister, IcpEnsurePlatformError> {
2036        let exact = exact_install_canister_status(
2037            &self.icp,
2038            principal,
2039            live.canister_version,
2040            live.module_sha256.clone(),
2041        )?;
2042        live.canister_version = Some(exact.canister_version);
2043        live.module_sha256 = exact.module_sha256;
2044        Ok(live)
2045    }
2046
2047    // A direct response can also supply the reconciliation balance, except where
2048    // action_cycles must establish the configured Pool's protected Root authority.
2049    fn direct_reconciliation_cycles(
2050        &self,
2051        name: &str,
2052        live: Option<&LiveCanister>,
2053    ) -> Option<u128> {
2054        let configured = self.desired.canisters.iter().find(|c| c.name == name);
2055        if configured.is_some_and(|c| c.kind == DesiredCanisterKind::Pool) {
2056            return None;
2057        }
2058        live.map(|live| live.cycles)
2059    }
2060
2061    fn observe_configured_canister(
2062        &self,
2063        configured: &crate::fleet_ensure::model::DesiredCanister,
2064        principal: &str,
2065        state: &FleetEnsureStateRecord,
2066    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
2067        if configured.kind == DesiredCanisterKind::Pool {
2068            return self.observe_root_owned_canister(configured, principal, state);
2069        }
2070        let status = self.status_optional(principal);
2071        self.configured_status_or_root(configured, principal, state, status)
2072    }
2073
2074    fn configured_status_or_root(
2075        &self,
2076        configured: &crate::fleet_ensure::model::DesiredCanister,
2077        principal: &str,
2078        state: &FleetEnsureStateRecord,
2079        status: Result<Option<LiveCanister>, IcpEnsurePlatformError>,
2080    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
2081        match status {
2082            Ok(observed) => Ok(observed),
2083            Err(error)
2084                if matches!(
2085                    configured.kind,
2086                    DesiredCanisterKind::Store | DesiredCanisterKind::Pool
2087                ) && !matches!(
2088                    error,
2089                    IcpEnsurePlatformError::Icp(ref source)
2090                        if matches!(
2091                            source.diagnostic(),
2092                            Some(IcpDiagnostic::CanisterNotFound { .. })
2093                        )
2094                ) =>
2095            {
2096                self.observe_root_owned_canister(configured, principal, state)
2097            }
2098            Err(error) => Err(error),
2099        }
2100    }
2101
2102    fn observe_root_owned_canister(
2103        &self,
2104        configured: &crate::fleet_ensure::model::DesiredCanister,
2105        principal: &str,
2106        state: &FleetEnsureStateRecord,
2107    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
2108        match self.prepare_root_owned_observation(configured, principal, state)? {
2109            PreparedRootOwnedObservation::Observed(live) => Ok(live),
2110            PreparedRootOwnedObservation::Pending(pending) => {
2111                let response = self.read_pool_inspection(pending.inspection.authority)?;
2112                self.complete_pending_pool_observation(*pending, response)
2113            }
2114        }
2115    }
2116
2117    fn prepare_root_owned_observation<'a>(
2118        &'a self,
2119        configured: &'a crate::fleet_ensure::model::DesiredCanister,
2120        principal: &'a str,
2121        state: &'a FleetEnsureStateRecord,
2122    ) -> Result<PreparedRootOwnedObservation<'a>, IcpEnsurePlatformError> {
2123        self.root_protocol_candid()?;
2124        let parent = configured.parent.as_deref().ok_or_else(|| {
2125            current_protocol::CurrentProtocolError::Configuration(format!(
2126                "Root-owned canister {} has no Root parent",
2127                configured.name
2128            ))
2129        })?;
2130        let root = self.current_principal(state, parent).ok_or_else(|| {
2131            current_protocol::CurrentProtocolError::Configuration(format!(
2132                "Root-owned canister {} has no resolved Root",
2133                configured.name
2134            ))
2135        })?;
2136        if self.required_root_status(&configured.name, root)? == CanisterRuntimeStatus::Stopped {
2137            return self
2138                .retained_root_owned_observation(configured, principal, parent, root, state)
2139                .map(PreparedRootOwnedObservation::Observed);
2140        }
2141        let target = parse_principal("Root-owned canister", principal)?;
2142        let mut start_after = None;
2143        loop {
2144            let page = self
2145                .query_estate_pool_page(parse_principal("Fleet Subnet Root", root)?, start_after)?;
2146            if let Some(asset) = page
2147                .entries
2148                .into_iter()
2149                .find(|asset| asset.canister_id == target)
2150            {
2151                if matches!(
2152                    asset.status,
2153                    canic_core::dto::pool::CanisterPoolAssetStatus::PendingReset
2154                ) && asset.cycles.to_u128() == 0
2155                {
2156                    let inspection =
2157                        self.prepare_pending_pool_inspection(configured, principal, state)?;
2158                    return Ok(PreparedRootOwnedObservation::Pending(Box::new(
2159                        PendingPoolObservation {
2160                            configured,
2161                            principal,
2162                            asset,
2163                            inspection,
2164                        },
2165                    )));
2166                }
2167                return Self::observed_root_owned_asset(configured, principal, root, asset)
2168                    .map(PreparedRootOwnedObservation::Observed);
2169            }
2170            let next = page.next_start_after;
2171            if next.is_none() {
2172                return Ok(PreparedRootOwnedObservation::Observed(None));
2173            }
2174            if next == start_after {
2175                return Err(
2176                    current_protocol::CurrentProtocolError::Configuration(format!(
2177                        "Root {parent} pool cursor did not advance"
2178                    ))
2179                    .into(),
2180                );
2181            }
2182            start_after = next;
2183        }
2184    }
2185
2186    fn complete_pending_pool_observation(
2187        &self,
2188        mut pending: PendingPoolObservation<'_>,
2189        response: RootInspectionStatus,
2190    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
2191        let root = pending.inspection.root;
2192        let cycles = self.complete_pool_inspection(pending.inspection, response)?;
2193        pending.asset.cycles = Cycles::new(cycles);
2194        Self::observed_root_owned_asset(pending.configured, pending.principal, root, pending.asset)
2195    }
2196
2197    fn prepare_pending_pool_inspection<'a>(
2198        &'a self,
2199        configured: &'a crate::fleet_ensure::model::DesiredCanister,
2200        principal: &str,
2201        state: &'a FleetEnsureStateRecord,
2202    ) -> Result<PreparedPoolInspection<'a>, IcpEnsurePlatformError> {
2203        let parent = configured.parent.as_deref().ok_or_else(|| {
2204            IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
2205                canister: configured.name.clone(),
2206                field: "Root parent",
2207            }
2208        })?;
2209        let root = self.current_principal(state, parent).ok_or_else(|| {
2210            IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
2211                canister: configured.name.clone(),
2212                field: "Root Principal",
2213            }
2214        })?;
2215        let pending_fresh_pool = self
2216            .desired
2217            .bootstrap
2218            .as_ref()
2219            .is_some_and(|bootstrap| bootstrap.fresh_estate)
2220            && configured.principal.is_none()
2221            && state
2222                .pending_principals
2223                .get(&configured.name)
2224                .map(String::as_str)
2225                == Some(principal);
2226        if pending_fresh_pool {
2227            let authority = self
2228                .root_owned_inspection_authority(configured, principal, state)?
2229                .ok_or_else(
2230                    || IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
2231                        canister: configured.name.clone(),
2232                        field: "installed Root",
2233                    },
2234                )?;
2235            return Ok(PreparedPoolInspection {
2236                authority,
2237                name: &configured.name,
2238                root,
2239                requirement: PoolInspectionRequirement::PendingFresh,
2240            });
2241        }
2242        self.prepare_pool_balance_inspection(
2243            &configured.name,
2244            root,
2245            principal,
2246            InspectedModule::Any,
2247        )
2248    }
2249
2250    fn inspect_pool_balance(
2251        &self,
2252        name: &str,
2253        root: &str,
2254        principal: &str,
2255        module: InspectedModule,
2256    ) -> Result<u128, IcpEnsurePlatformError> {
2257        let inspection = self.prepare_pool_balance_inspection(name, root, principal, module)?;
2258        let response = self.read_pool_inspection(inspection.authority)?;
2259        self.complete_pool_inspection(inspection, response)
2260    }
2261
2262    fn refresh_pool_balances(
2263        &self,
2264        root: &str,
2265        assets: &mut [EstatePoolAssetObservation],
2266    ) -> Result<(), IcpEnsurePlatformError> {
2267        let indices = assets
2268            .iter()
2269            .enumerate()
2270            .filter_map(|(index, asset)| {
2271                matches!(
2272                    asset.lifecycle,
2273                    EstatePoolAssetLifecycle::PendingReset | EstatePoolAssetLifecycle::Failed
2274                )
2275                .then_some(index)
2276            })
2277            .collect::<Vec<_>>();
2278        for batch in indices.chunks(super::bounded_observations::MAX_IN_FLIGHT) {
2279            // One shared authority check precedes this batch's independent reads.
2280            // The next batch, observation and effect all establish authority again.
2281            self.require_pool_balance_authority(&assets[batch[0]].principal, root)?;
2282            let prepared = batch
2283                .iter()
2284                .map(|index| {
2285                    let target = &assets[*index].principal;
2286                    Ok::<_, IcpEnsurePlatformError>(PreparedPoolInspection {
2287                        authority: PoolInspectionAuthority::parse(root, target)?,
2288                        name: target,
2289                        root,
2290                        requirement: PoolInspectionRequirement::RootControlled(
2291                            InspectedModule::Any,
2292                        ),
2293                    })
2294                })
2295                .collect::<Vec<_>>();
2296            let authorities = prepared
2297                .iter()
2298                .filter_map(|result| result.as_ref().ok().map(|inspection| inspection.authority))
2299                .collect::<Vec<_>>();
2300            let mut responses = self.read_pool_inspection_batch(&authorities)?;
2301            // Drain issued reads, then validate in inventory order before publishing
2302            // this batch's balances. A failed batch never starts the next group.
2303            let balances = prepared
2304                .into_iter()
2305                .map(|inspection| {
2306                    let inspection = inspection?;
2307                    let response =
2308                        self.pool_inspection_batch_response(&mut responses, inspection.authority)?;
2309                    self.complete_pool_inspection(inspection, response)
2310                })
2311                .collect::<Result<Vec<_>, IcpEnsurePlatformError>>()?;
2312            for (index, cycles) in batch.iter().zip(balances) {
2313                assets[*index].cycles = cycles;
2314            }
2315        }
2316        Ok(())
2317    }
2318
2319    fn prepare_pool_balance_inspection<'a>(
2320        &self,
2321        name: &'a str,
2322        root: &'a str,
2323        principal: &str,
2324        module: InspectedModule,
2325    ) -> Result<PreparedPoolInspection<'a>, IcpEnsurePlatformError> {
2326        self.require_pool_balance_authority(name, root)?;
2327        Ok(PreparedPoolInspection {
2328            authority: PoolInspectionAuthority::parse(root, principal)?,
2329            name,
2330            root,
2331            requirement: PoolInspectionRequirement::RootControlled(module),
2332        })
2333    }
2334
2335    fn require_pool_balance_authority(
2336        &self,
2337        name: &str,
2338        root: &str,
2339    ) -> Result<(), IcpEnsurePlatformError> {
2340        if self.required_root_status(name, root)? != CanisterRuntimeStatus::Running {
2341            return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
2342                canister: name.to_string(),
2343                field: "running Root",
2344            });
2345        }
2346        self.require_operator()
2347    }
2348
2349    fn complete_pool_inspection(
2350        &self,
2351        inspection: PreparedPoolInspection<'_>,
2352        response: RootInspectionStatus,
2353    ) -> Result<u128, IcpEnsurePlatformError> {
2354        let cycles = match inspection.requirement {
2355            PoolInspectionRequirement::PendingFresh => validate_pending_fresh_pool_inspection(
2356                inspection.name,
2357                inspection.root,
2358                &self.desired.operator,
2359                &response.settings.controllers,
2360                response.module_hash.as_deref(),
2361                &response.cycles,
2362                self.desired
2363                    .bootstrap
2364                    .as_ref()
2365                    .map_or(&[], |bootstrap| &bootstrap.recovery_controllers),
2366            ),
2367            PoolInspectionRequirement::RootControlled(module) => {
2368                validate_root_controlled_inspection(
2369                    inspection.name,
2370                    module,
2371                    inspection.root,
2372                    &response.settings.controllers,
2373                    response.module_hash.as_deref(),
2374                    &response.cycles,
2375                    self.desired
2376                        .bootstrap
2377                        .as_ref()
2378                        .map_or(&[], |bootstrap| &bootstrap.recovery_controllers),
2379                )
2380            }
2381        }?;
2382        self.retain_pool_inspection(inspection.authority, response);
2383        Ok(cycles)
2384    }
2385
2386    // Read raw evidence only after the caller has established its Root/operator authority.
2387    fn read_pool_inspection(
2388        &self,
2389        authority: PoolInspectionAuthority,
2390    ) -> Result<RootInspectionStatus, IcpEnsurePlatformError> {
2391        if let Some(response) = self
2392            .observation_snapshot
2393            .borrow()
2394            .as_ref()
2395            .and_then(|snapshot| snapshot.pool_inspections.get(&authority).cloned())
2396        {
2397            self.record_cached_read();
2398            return Ok(response);
2399        }
2400        Self::fetch_pool_inspection(&self.icp, &self.root_protocol_candid()?, authority)
2401    }
2402
2403    fn fetch_pool_inspection(
2404        icp: &IcpCli,
2405        candid: &Path,
2406        authority: PoolInspectionAuthority,
2407    ) -> Result<RootInspectionStatus, IcpEnsurePlatformError> {
2408        icp.measure_canister_inspection(authority.root, authority.target, || {
2409            crate::canister_protocol::inspection::preflight_inspection(
2410                icp,
2411                candid,
2412                authority.root,
2413                authority.target,
2414            )
2415            .map_err(current_protocol::CurrentProtocolError::from)?;
2416            let response: RootInspectionResponse = call_with_candid(
2417                icp,
2418                candid,
2419                authority.root,
2420                canic_protocol::CANIC_ROOT_COMMAND,
2421                &RootInspectionCommand::InspectCanister(CanisterInspectionRequest {
2422                    canister_id: authority.target,
2423                }),
2424            )
2425            .map_err(current_protocol::CurrentProtocolError::from)?;
2426            response
2427                .into_status(authority.root, authority.target)
2428                .map_err(current_protocol::CurrentProtocolError::from)
2429                .map_err(Into::into)
2430        })
2431    }
2432
2433    // Only a successfully validated consumer may retain a response in this observation.
2434    fn retain_pool_inspection(
2435        &self,
2436        authority: PoolInspectionAuthority,
2437        response: RootInspectionStatus,
2438    ) {
2439        if let Some(snapshot) = self.observation_snapshot.borrow_mut().as_mut() {
2440            snapshot.pool_inspections.insert(authority, response);
2441        }
2442    }
2443
2444    fn pool_funding_module(
2445        &self,
2446        name: &str,
2447        authority: &crate::fleet_ensure::model::PoolFundingAuthority,
2448        principal: &str,
2449    ) -> Result<InspectedModule, IcpEnsurePlatformError> {
2450        self.root_protocol_candid()?;
2451        let root = parse_principal("Fleet Subnet Root", &authority.root)?;
2452        let target = parse_principal("pool funding target", principal)?;
2453        let mut start_after = None;
2454        loop {
2455            let page = self.query_estate_pool_page(root, start_after)?;
2456            if let Some(asset) = page
2457                .entries
2458                .iter()
2459                .find(|asset| asset.canister_id == target)
2460            {
2461                return pool_funding_inspected_module(name, authority.lifecycle, &asset.status);
2462            }
2463            if page.next_start_after.is_none() || page.next_start_after == start_after {
2464                return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
2465                    canister: name.to_string(),
2466                    field: "reviewed pool membership",
2467                });
2468            }
2469            start_after = page.next_start_after;
2470        }
2471    }
2472
2473    fn observed_root_owned_asset(
2474        configured: &crate::fleet_ensure::model::DesiredCanister,
2475        principal: &str,
2476        root: &str,
2477        asset: CanisterPoolAsset,
2478    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
2479        let Some(root_owned_lifecycle) = root_owned_lifecycle(configured.kind, &asset.status)
2480        else {
2481            return Err(
2482                current_protocol::CurrentProtocolError::Configuration(format!(
2483                    "Root-owned canister {} has a live pool role incompatible with desired {:?}",
2484                    configured.name, configured.kind
2485                ))
2486                .into(),
2487            );
2488        };
2489        let status = match root_owned_lifecycle {
2490            RootOwnedCanisterLifecycle::Store | RootOwnedCanisterLifecycle::Workload => {
2491                CanisterRuntimeStatus::Running
2492            }
2493            RootOwnedCanisterLifecycle::Claimed
2494            | RootOwnedCanisterLifecycle::Idle
2495            | RootOwnedCanisterLifecycle::Reconciling
2496            | RootOwnedCanisterLifecycle::Retained => CanisterRuntimeStatus::Stopped,
2497        };
2498        Ok(Some(LiveCanister {
2499            canister_version: None,
2500            controllers: {
2501                let mut controllers = configured.controllers.clone();
2502                controllers.push(root.to_string());
2503                controllers.sort();
2504                controllers.dedup();
2505                controllers
2506            },
2507            cycles: asset.cycles.to_u128(),
2508            module_sha256: None,
2509            principal: principal.to_string(),
2510            reinstall_required: false,
2511            root_owned_lifecycle: Some(root_owned_lifecycle),
2512            status,
2513        }))
2514    }
2515
2516    fn root_protocol_candid(&self) -> Result<PathBuf, IcpEnsurePlatformError> {
2517        let protocol = self.desired.protocol.as_ref().ok_or_else(|| {
2518            current_protocol::CurrentProtocolError::Configuration(
2519                "Root-owned observation requires typed Fleet protocol".to_string(),
2520            )
2521        })?;
2522        Ok(resolve_path(&self.root, &protocol.root_candid))
2523    }
2524
2525    fn required_root_status(
2526        &self,
2527        configured_name: &str,
2528        root: &str,
2529    ) -> Result<CanisterRuntimeStatus, IcpEnsurePlatformError> {
2530        self.required_root_canister(configured_name, root)
2531            .map(|live| live.status)
2532    }
2533
2534    fn required_root_canister(
2535        &self,
2536        configured_name: &str,
2537        root: &str,
2538    ) -> Result<LiveCanister, IcpEnsurePlatformError> {
2539        self.status_optional(root)?.ok_or_else(|| {
2540            current_protocol::CurrentProtocolError::Configuration(format!(
2541                "Root-owned canister {configured_name} has no live Root"
2542            ))
2543            .into()
2544        })
2545    }
2546
2547    fn retained_root_owned_observation(
2548        &self,
2549        configured: &crate::fleet_ensure::model::DesiredCanister,
2550        principal: &str,
2551        parent: &str,
2552        root: &str,
2553        state: &FleetEnsureStateRecord,
2554    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
2555        let Some(retained_topology) = exact_retained_root_owned_topology(
2556            state,
2557            &configured.name,
2558            configured.kind,
2559            parent,
2560            principal,
2561            root,
2562        ) else {
2563            return Err(
2564                current_protocol::CurrentProtocolError::Configuration(format!(
2565                    "Root-owned canister {} has no exact retained topology authority",
2566                    configured.name
2567                ))
2568                .into(),
2569            );
2570        };
2571        let cycles = self
2572            .controller_cycle_balance(principal)
2573            .or_else(|| state.retained_cycles_by_principal.get(principal).copied())
2574            .ok_or_else(|| {
2575                current_protocol::CurrentProtocolError::Configuration(format!(
2576                    "Root-owned canister {} has no current or retained exact native balance",
2577                    configured.name
2578                ))
2579            })?;
2580        if !matches!(
2581            configured.kind,
2582            DesiredCanisterKind::Pool | DesiredCanisterKind::Store
2583        ) || configured.presence != crate::fleet_ensure::model::DesiredPresence::Present
2584            || configured.replace
2585            || configured.drain.is_some()
2586            || (configured.kind == DesiredCanisterKind::Pool && configured.wasm.is_some())
2587            || (configured.kind == DesiredCanisterKind::Store && configured.wasm.is_none())
2588        {
2589            return Err(
2590                current_protocol::CurrentProtocolError::Configuration(format!(
2591                    "Root-owned recovery evidence for {} cannot authorize a mutation",
2592                    configured.name
2593                ))
2594                .into(),
2595            );
2596        }
2597        let root_config = self
2598            .desired
2599            .canisters
2600            .iter()
2601            .find(|canister| canister.name == parent && canister.kind == DesiredCanisterKind::Root)
2602            .ok_or_else(|| {
2603                current_protocol::CurrentProtocolError::Configuration(format!(
2604                    "Root-owned canister {} has no exact configured Root",
2605                    configured.name
2606                ))
2607            })?;
2608        let live_root = self.status_optional(root)?.ok_or_else(|| {
2609            current_protocol::CurrentProtocolError::Configuration(format!(
2610                "Root-owned canister {} has no live Root",
2611                configured.name
2612            ))
2613        })?;
2614        let mut expected_controllers = root_config.controllers.clone();
2615        expected_controllers.sort();
2616        if live_root.controllers != expected_controllers {
2617            return Err(
2618                current_protocol::CurrentProtocolError::Configuration(format!(
2619                    "Root-owned canister {} Root controllers drifted",
2620                    configured.name
2621                ))
2622                .into(),
2623            );
2624        }
2625        Ok(Some(LiveCanister {
2626            canister_version: None,
2627            controllers: {
2628                let mut controllers = configured.controllers.clone();
2629                controllers.push(root.to_string());
2630                controllers.sort();
2631                controllers.dedup();
2632                controllers
2633            },
2634            cycles,
2635            module_sha256: retained_topology.module_hash.clone(),
2636            principal: principal.to_string(),
2637            reinstall_required: false,
2638            root_owned_lifecycle: Some(RootOwnedCanisterLifecycle::Retained),
2639            status: if configured.kind == DesiredCanisterKind::Store {
2640                CanisterRuntimeStatus::Running
2641            } else {
2642                CanisterRuntimeStatus::Stopped
2643            },
2644        }))
2645    }
2646
2647    fn completed_reinstall_is_current(
2648        &self,
2649        state: &FleetEnsureStateRecord,
2650        name: &str,
2651        live: &LiveCanister,
2652    ) -> Result<bool, IcpEnsurePlatformError> {
2653        if state.completed_reinstall_operation_id.is_none()
2654            || !state.completed_reinstall_action_sha256.contains_key(name)
2655        {
2656            return Ok(false);
2657        }
2658        let Some(pre_canister_version) = state.completed_reinstalls.get(name) else {
2659            return Ok(false);
2660        };
2661        let Some(configured) = self
2662            .desired
2663            .canisters
2664            .iter()
2665            .find(|configured| configured.name == name)
2666        else {
2667            return Ok(false);
2668        };
2669        let Some(wasm) = configured.wasm.as_deref() else {
2670            return Ok(false);
2671        };
2672        let principal_matches = state
2673            .principals
2674            .get(name)
2675            .is_some_and(|principal| principal == &live.principal);
2676        let retained_topology = state.topology.get(name);
2677        let topology_matches = retained_topology.is_some_and(|topology| {
2678            topology.kind == configured.kind && topology.parent == configured.parent
2679        });
2680        let desired_module_sha256 = artifact_hash(&resolve_path(&self.root, wasm))?;
2681        let root_owned_store_module = (live.module_sha256.is_none()
2682            && live.root_owned_lifecycle == Some(RootOwnedCanisterLifecycle::Store))
2683        .then(|| {
2684            configured
2685                .parent
2686                .as_deref()
2687                .and_then(|parent| state.principals.get(parent))
2688                .filter(|root| {
2689                    let mut expected = configured.controllers.clone();
2690                    expected.push((*root).clone());
2691                    expected.sort();
2692                    expected.dedup();
2693                    live.controllers == expected
2694                })
2695                .and(retained_topology)
2696                .and_then(|topology| topology.module_hash.as_deref())
2697        })
2698        .flatten();
2699        completed_reinstall_continuity(
2700            *pre_canister_version,
2701            principal_matches,
2702            topology_matches,
2703            live.module_sha256.as_deref().or(root_owned_store_module),
2704            &desired_module_sha256,
2705            live.canister_version,
2706        )
2707        .map_err(
2708            |field| IcpEnsurePlatformError::CompletedReinstallAuthorityConflict {
2709                canister: name.to_string(),
2710                field,
2711            },
2712        )?;
2713        Ok(true)
2714    }
2715
2716    fn controller_cycle_balance(&self, principal: &str) -> Option<u128> {
2717        let response: Result<ManagedCanisterStatusResponse, canic_core::dto::error::Error> = self
2718            .icp
2719            .canister_query_candid(
2720                principal,
2721                canic_protocol::CANIC_OBSERVABILITY,
2722                &ManagedCanisterStatusRequest::CycleBalance,
2723                None,
2724            )
2725            .ok()?;
2726        let ManagedCanisterStatusResponse::CycleBalance(balance) = response.ok()?;
2727        Some(balance.cycles)
2728    }
2729
2730    fn apply_create(
2731        &self,
2732        authority: CreateCanisterAuthority<'_>,
2733        state: &FleetEnsureStateRecord,
2734    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2735        let creation_fee = self
2736            .desired
2737            .management_creation_fee_cycles
2738            .parse::<Cycles>()
2739            .map(|cycles| cycles.to_u128())
2740            .map_err(|_| IcpEnsurePlatformError::Arithmetic("management creation fee"))?;
2741        let amount = authority
2742            .requested_initial_cycles
2743            .checked_add(creation_fee)
2744            .ok_or(IcpEnsurePlatformError::Arithmetic("creation amount"))?;
2745        let controllers = self
2746            .resolved_controllers(state, authority.controllers, authority.controller_canisters)?
2747            .iter()
2748            .map(|value| parse_principal("controller", value))
2749            .collect::<Result<Vec<_>, _>>()?;
2750        let request = CreateCanisterArgs {
2751            amount: Nat::from(amount),
2752            created_at_time: Some(authority.created_at_time),
2753            creation_args: Some(CmcCreateCanisterArgs {
2754                settings: Some(CanisterSettings {
2755                    compute_allocation: None,
2756                    controllers: Some(controllers),
2757                    freezing_threshold: None,
2758                    memory_allocation: None,
2759                    reserved_cycles_limit: None,
2760                }),
2761                subnet_selection: Some(SubnetSelection::Subnet {
2762                    subnet: parse_principal("subnet", authority.subnet)?,
2763                }),
2764            }),
2765            from_subaccount: None,
2766        };
2767        let response: Result<CreateCanisterSuccess, CreateCanisterError> = self
2768            .icp
2769            .canister_call_candid(authority.ledger, "create_canister", &request, None)?;
2770        create_canister_response_outcome(response, authority.requested_initial_cycles)
2771    }
2772
2773    fn apply_fund(
2774        &self,
2775        amount: u128,
2776        created_at_time: u64,
2777        ledger: &str,
2778        principal: &str,
2779    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2780        let request = WithdrawArgs {
2781            amount: Nat::from(amount),
2782            created_at_time: Some(created_at_time),
2783            from_subaccount: None,
2784            to: parse_principal("funding target", principal)?,
2785        };
2786        let response: Result<Nat, WithdrawError> = self
2787            .icp
2788            .canister_call_candid(ledger, "withdraw", &request, None)?;
2789        match response {
2790            Ok(block) => Ok(EffectOutcome {
2791                created_principal: None,
2792                post_cycles: None,
2793                receipt: Some(block.to_string()),
2794            }),
2795            Err(WithdrawError::Duplicate { duplicate_of }) => Ok(EffectOutcome {
2796                created_principal: None,
2797                post_cycles: None,
2798                receipt: Some(duplicate_of.to_string()),
2799            }),
2800            Err(error) => Err(IcpEnsurePlatformError::LedgerWithdraw(
2801                render_withdraw_error(error),
2802            )),
2803        }
2804    }
2805
2806    fn apply_estate_fund(
2807        &self,
2808        amount: u128,
2809        created_at_time: u64,
2810        ledger: &str,
2811        principal: &str,
2812        reviewed_fee_cycles: u128,
2813    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2814        let request = CyclesLedgerTransferArgs {
2815            amount: Nat::from(amount),
2816            created_at_time: Some(created_at_time),
2817            fee: Some(Nat::from(reviewed_fee_cycles)),
2818            from_subaccount: None,
2819            memo: None,
2820            to: CyclesLedgerAccount {
2821                owner: parse_principal("estate funding target", principal)?,
2822                subaccount: None,
2823            },
2824        };
2825        let response: Result<Nat, CyclesLedgerTransferError> =
2826            self.icp
2827                .canister_call_candid(ledger, "icrc1_transfer", &request, None)?;
2828        match response {
2829            Ok(block) => Ok(EffectOutcome {
2830                created_principal: None,
2831                post_cycles: None,
2832                receipt: Some(block.to_string()),
2833            }),
2834            Err(CyclesLedgerTransferError::Duplicate { duplicate_of }) => Ok(EffectOutcome {
2835                created_principal: None,
2836                post_cycles: None,
2837                receipt: Some(duplicate_of.to_string()),
2838            }),
2839            Err(CyclesLedgerTransferError::BadFee { expected_fee }) => {
2840                Err(IcpEnsurePlatformError::LedgerTransferFeeChanged {
2841                    expected_fee_cycles: ledger_fee_cycles(expected_fee)?,
2842                    reviewed_fee_cycles,
2843                })
2844            }
2845            Err(error) => Err(IcpEnsurePlatformError::LedgerTransfer(
2846                render_ledger_transfer_error(error),
2847            )),
2848        }
2849    }
2850
2851    #[expect(
2852        clippy::too_many_arguments,
2853        reason = "the install boundary verifies one complete immutable action tuple"
2854    )]
2855    fn apply_install(
2856        &self,
2857        operation_id: &str,
2858        principals: &BTreeMap<String, String>,
2859        canic_init: Option<&crate::fleet_ensure::model::DesiredCanisterInit>,
2860        init_arg: Option<&str>,
2861        init_arg_sha256: Option<&str>,
2862        init_candid: Option<&str>,
2863        init_candid_sha256: Option<&str>,
2864        mode: InstallMode,
2865        principal: &str,
2866        wasm: &str,
2867        wasm_sha256: &str,
2868    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2869        verify_artifact(&resolve_path(&self.root, wasm), wasm_sha256, "Wasm")?;
2870        let mut command = self.icp.canister_command();
2871        command.args(["install", principal, "--mode"]);
2872        command.arg(match mode {
2873            InstallMode::Install => "install",
2874            InstallMode::Reinstall => "reinstall",
2875        });
2876        command.args(["--yes", "--wasm"]);
2877        command.arg(resolve_path(&self.root, wasm));
2878        let generated_init = if let Some(canic_init) = canic_init {
2879            let path = canic_init::write_arguments(canic_init::CanicInitRequest {
2880                desired: &self.desired,
2881                init: canic_init,
2882                operation_id,
2883                principals,
2884                root: &self.root,
2885                wasm,
2886                wasm_sha256,
2887            })?;
2888            command.arg("--args-file");
2889            command.arg(&path);
2890            command.args(["--args-format", "bin"]);
2891            Some(path)
2892        } else if let Some(init_arg) = init_arg {
2893            let expected =
2894                init_arg_sha256.ok_or_else(|| IcpEnsurePlatformError::ArtifactDigestMismatch {
2895                    actual: "missing".to_string(),
2896                    expected: "reviewed init argument identity".to_string(),
2897                    kind: "init argument",
2898                })?;
2899            let init_candid =
2900                init_candid.ok_or_else(|| IcpEnsurePlatformError::ArtifactDigestMismatch {
2901                    actual: "missing".to_string(),
2902                    expected: "reviewed init Candid".to_string(),
2903                    kind: "init Candid",
2904                })?;
2905            let init_candid_sha256 = init_candid_sha256.ok_or_else(|| {
2906                IcpEnsurePlatformError::ArtifactDigestMismatch {
2907                    actual: "missing".to_string(),
2908                    expected: "reviewed init Candid identity".to_string(),
2909                    kind: "init Candid",
2910                }
2911            })?;
2912            let path = protocol::write_init_arguments(
2913                &self.root,
2914                operation_id,
2915                principals,
2916                init_candid,
2917                init_candid_sha256,
2918                init_arg,
2919                expected,
2920            )?;
2921            command.arg("--args-file");
2922            command.arg(&path);
2923            command.args(["--args-format", "bin"]);
2924            Some(path)
2925        } else {
2926            command.args(["--args", "()"]);
2927            None
2928        };
2929        self.icp.add_target_args(&mut command);
2930        self.icp.record_remote_call();
2931        let result = self
2932            .icp
2933            .measure_request(
2934                crate::icp::IcpRequestKind::Install,
2935                Some(principal),
2936                None,
2937                || run_status(&mut command),
2938            )
2939            .map_err(IcpEnsurePlatformError::from);
2940        if let Some(path) = generated_init {
2941            std::fs::remove_file(&path)
2942                .map_err(|_| IcpEnsurePlatformError::ArtifactUnavailable(path))?;
2943        }
2944        result?;
2945        Ok(empty_outcome())
2946    }
2947
2948    fn apply_controllers(
2949        &self,
2950        principal: &str,
2951        controllers: &[String],
2952    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2953        let mut command = self.icp.canister_command();
2954        command.args([
2955            "settings",
2956            "update",
2957            principal,
2958            "--force",
2959            "--remove-all-controllers",
2960        ]);
2961        for controller in controllers {
2962            parse_principal("controller", controller)?;
2963            command.args(["--add-controller", controller]);
2964        }
2965        self.icp.add_target_args(&mut command);
2966        self.icp.record_remote_call();
2967        self.icp.measure_request(
2968            crate::icp::IcpRequestKind::SetControllers,
2969            Some(principal),
2970            None,
2971            || run_status(&mut command),
2972        )?;
2973        Ok(empty_outcome())
2974    }
2975
2976    fn observe_fleet_snapshot(
2977        &mut self,
2978        operation_id: &str,
2979        state: &FleetEnsureStateRecord,
2980    ) -> Result<FleetObservation, IcpEnsurePlatformError> {
2981        self.require_operator()?;
2982        let mut canisters = self
2983            .timed_observation(FleetObservationStage::ConfiguredCanisters, |platform| {
2984                platform.observe_configured_canisters(state)
2985            })?;
2986        self.reconcile_completed_reinstalls(state, &mut canisters)?;
2987        let mut estate_funding_domains = self
2988            .timed_observation(FleetObservationStage::EstateFunding, |platform| {
2989                platform.observe_estate_funding_domains(state)
2990            })?;
2991        self.timed_observation(FleetObservationStage::PoolBalances, |platform| {
2992            for domain in estate_funding_domains.values_mut() {
2993                if let (Some(root), Some(pool)) = (&domain.root_principal, &mut domain.pool) {
2994                    platform.refresh_pool_balances(root, &mut pool.assets)?;
2995                    for asset in &mut pool.assets {
2996                        if matches!(
2997                            asset.lifecycle,
2998                            EstatePoolAssetLifecycle::PendingReset
2999                                | EstatePoolAssetLifecycle::Failed
3000                        ) {
3001                            for live in canisters.values_mut().filter_map(Option::as_mut) {
3002                                if live.principal == asset.principal {
3003                                    live.cycles = asset.cycles;
3004                                }
3005                            }
3006                        }
3007                    }
3008                }
3009            }
3010            Ok(())
3011        })?;
3012        self.estate_observations.clone_from(&estate_funding_domains);
3013        self.require_current_pool_policy(&estate_funding_domains)?;
3014        self.reconcile_completed_reinstalls(state, &mut canisters)?;
3015        let additional_controlled_cycles =
3016            Self::additional_pool_cycles(&canisters, &estate_funding_domains)?;
3017        let protocol_ready = self
3018            .timed_observation(FleetObservationStage::ProtocolReadiness, |platform| {
3019                platform.observe_protocol_readiness(operation_id, state, &canisters)
3020            })?;
3021        let ledger_fee_cycles =
3022            self.timed_observation(FleetObservationStage::LedgerFee, |platform| {
3023                ledger_fee_cycles(platform.icp.canister_query_candid(
3024                    &platform.desired.cycles_ledger,
3025                    "icrc1_fee",
3026                    &(),
3027                    None,
3028                )?)
3029            })?;
3030        let operator_cycles =
3031            self.timed_observation(FleetObservationStage::OperatorBalance, |platform| {
3032                platform
3033                    .icp
3034                    .identity_cycles_balance()
3035                    .map_err(|error| IcpEnsurePlatformError::LedgerWithdraw(error.to_string()))
3036            })?;
3037        Ok(FleetObservation {
3038            additional_controlled_cycles,
3039            canisters,
3040            estate_funding_domains,
3041            ledger_fee_cycles,
3042            operator_cycles,
3043            protocol_ready,
3044        })
3045    }
3046
3047    fn observe_configured_canisters(
3048        &self,
3049        state: &FleetEnsureStateRecord,
3050    ) -> Result<BTreeMap<String, Option<LiveCanister>>, IcpEnsurePlatformError> {
3051        let configured = &self.desired.canisters;
3052        let mut observed = BTreeMap::new();
3053        let mut cursor = 0;
3054        while cursor < configured.len() {
3055            let pool_batch = configured[cursor].kind == DesiredCanisterKind::Pool;
3056            let end = configured[cursor..]
3057                .iter()
3058                .take(super::bounded_observations::MAX_IN_FLIGHT)
3059                .take_while(|canister| (canister.kind == DesiredCanisterKind::Pool) == pool_batch)
3060                .count()
3061                + cursor;
3062            if pool_batch {
3063                self.observe_configured_pool_batch(&configured[cursor..end], state, &mut observed)?;
3064                cursor = end;
3065                continue;
3066            }
3067            self.observe_configured_status_batch(&configured[cursor..end], state, &mut observed)?;
3068            cursor = end;
3069        }
3070        Ok(observed)
3071    }
3072
3073    fn observe_configured_pool_batch(
3074        &self,
3075        configured: &[crate::fleet_ensure::model::DesiredCanister],
3076        state: &FleetEnsureStateRecord,
3077        observed: &mut BTreeMap<String, Option<LiveCanister>>,
3078    ) -> Result<(), IcpEnsurePlatformError> {
3079        // Preparation and result consumption follow desired order. An earlier transport
3080        // failure must still precede a later preparation failure, regardless of arrival.
3081        let prepared = configured
3082            .iter()
3083            .map(|canister| {
3084                self.current_principal(state, &canister.name).map_or(
3085                    Ok(PreparedRootOwnedObservation::Observed(None)),
3086                    |principal| self.prepare_root_owned_observation(canister, principal, state),
3087                )
3088            })
3089            .collect::<Vec<_>>();
3090        let authorities = prepared
3091            .iter()
3092            .filter_map(|result| match result {
3093                Ok(PreparedRootOwnedObservation::Pending(pending)) => {
3094                    Some(pending.inspection.authority)
3095                }
3096                _ => None,
3097            })
3098            .collect::<Vec<_>>();
3099        let mut responses = self.read_pool_inspection_batch(&authorities)?;
3100        for (canister, prepared) in configured.iter().zip(prepared) {
3101            let live = match prepared? {
3102                PreparedRootOwnedObservation::Observed(live) => live,
3103                PreparedRootOwnedObservation::Pending(pending) => {
3104                    let response = self.pool_inspection_batch_response(
3105                        &mut responses,
3106                        pending.inspection.authority,
3107                    )?;
3108                    self.complete_pending_pool_observation(*pending, response)?
3109                }
3110            };
3111            observed.insert(canister.name.clone(), live);
3112        }
3113        Ok(())
3114    }
3115
3116    fn read_pool_inspection_batch(
3117        &self,
3118        authorities: &[PoolInspectionAuthority],
3119    ) -> Result<PoolInspectionBatch, IcpEnsurePlatformError> {
3120        let mut seen = BTreeSet::new();
3121        let requests = authorities
3122            .iter()
3123            .copied()
3124            .filter(|authority| {
3125                seen.insert(*authority)
3126                    && !self
3127                        .observation_snapshot
3128                        .borrow()
3129                        .as_ref()
3130                        .is_some_and(|snapshot| snapshot.pool_inspections.contains_key(authority))
3131            })
3132            .collect::<Vec<_>>();
3133        if requests.is_empty() {
3134            return Ok(PoolInspectionBatch::default());
3135        }
3136        let candid = self.root_protocol_candid()?;
3137        let icp = &self.icp;
3138        Ok(PoolInspectionBatch {
3139            responses: super::bounded_observations::collect(&requests, |authority| {
3140                Ok::<_, std::convert::Infallible>((
3141                    *authority,
3142                    Self::fetch_pool_inspection(icp, &candid, *authority),
3143                ))
3144            })
3145            .expect("inspection results are retained for ordered authority decisions")
3146            .into_iter()
3147            .collect(),
3148        })
3149    }
3150
3151    fn pool_inspection_batch_response(
3152        &self,
3153        batch: &mut PoolInspectionBatch,
3154        authority: PoolInspectionAuthority,
3155    ) -> Result<RootInspectionStatus, IcpEnsurePlatformError> {
3156        match batch.responses.get(&authority) {
3157            Some(Ok(response)) => Ok(response.clone()),
3158            Some(Err(_)) => batch
3159                .responses
3160                .remove(&authority)
3161                .expect("observed batch failure"),
3162            None => self.read_pool_inspection(authority),
3163        }
3164    }
3165
3166    fn observe_configured_status_batch(
3167        &self,
3168        configured: &[crate::fleet_ensure::model::DesiredCanister],
3169        state: &FleetEnsureStateRecord,
3170        observed: &mut BTreeMap<String, Option<LiveCanister>>,
3171    ) -> Result<(), IcpEnsurePlatformError> {
3172        let mut results = self.read_status_batch(
3173            configured
3174                .iter()
3175                .filter_map(|canister| self.current_principal(state, &canister.name)),
3176        );
3177        for canister in configured {
3178            let Some(principal) = self.current_principal(state, &canister.name) else {
3179                observed.insert(canister.name.clone(), None);
3180                continue;
3181            };
3182            let status = self.status_batch_response(&mut results, principal);
3183            let live = self.configured_status_or_root(canister, principal, state, status)?;
3184            observed.insert(canister.name.clone(), live);
3185        }
3186        Ok(())
3187    }
3188
3189    fn read_status_batch<'a>(
3190        &self,
3191        principals: impl Iterator<Item = &'a str>,
3192    ) -> BTreeMap<&'a str, Result<Option<LiveCanister>, IcpEnsurePlatformError>> {
3193        let mut seen = BTreeSet::new();
3194        let requests = principals
3195            .filter(|principal| seen.insert(*principal))
3196            .filter(|principal| matches!(self.cached_status(principal), StatusCacheEntry::Miss))
3197            .collect::<Vec<_>>();
3198        let icp = &self.icp;
3199        super::bounded_observations::collect(&requests, |principal| {
3200            Ok::<_, std::convert::Infallible>((*principal, Self::read_status_with(icp, principal)))
3201        })
3202        .expect("read outcomes are retained for ordered authority decisions")
3203        .into_iter()
3204        .collect()
3205    }
3206
3207    fn status_batch_response(
3208        &self,
3209        results: &mut BTreeMap<&str, Result<Option<LiveCanister>, IcpEnsurePlatformError>>,
3210        principal: &str,
3211    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
3212        let status = if let StatusCacheEntry::Hit(cached) = self.cached_status(principal) {
3213            self.record_cached_read();
3214            Ok(cached)
3215        } else {
3216            results
3217                .remove(principal)
3218                .unwrap_or_else(|| self.status_optional(principal))
3219        };
3220        if let Ok(live) = &status
3221            && let Some(snapshot) = self.observation_snapshot.borrow_mut().as_mut()
3222        {
3223            snapshot
3224                .statuses
3225                .insert(principal.to_string(), live.clone());
3226        }
3227        status
3228    }
3229
3230    fn cached_status(&self, principal: &str) -> StatusCacheEntry {
3231        self.observation_snapshot
3232            .borrow()
3233            .as_ref()
3234            .and_then(|snapshot| snapshot.statuses.get(principal).cloned())
3235            .map_or(StatusCacheEntry::Miss, StatusCacheEntry::Hit)
3236    }
3237
3238    fn reconcile_completed_reinstalls(
3239        &self,
3240        state: &FleetEnsureStateRecord,
3241        canisters: &mut BTreeMap<String, Option<LiveCanister>>,
3242    ) -> Result<(), IcpEnsurePlatformError> {
3243        for (name, live) in canisters.iter_mut() {
3244            let Some(live) = live.as_mut() else {
3245                continue;
3246            };
3247            let completed = self.completed_reinstall_is_current(state, name, live)?;
3248            let store_module_unobserved = live.module_sha256.is_none()
3249                && live.root_owned_lifecycle == Some(RootOwnedCanisterLifecycle::Store);
3250            if completed && store_module_unobserved {
3251                live.module_sha256 = state
3252                    .topology
3253                    .get(name)
3254                    .and_then(|topology| topology.module_hash.clone());
3255            }
3256        }
3257        Ok(())
3258    }
3259
3260    fn additional_pool_cycles(
3261        canisters: &BTreeMap<String, Option<LiveCanister>>,
3262        domains: &BTreeMap<String, EstateFundingDomainObservation>,
3263    ) -> Result<BTreeMap<String, u128>, IcpEnsurePlatformError> {
3264        let configured = canisters
3265            .values()
3266            .filter_map(|live| live.as_ref().map(|live| live.principal.as_str()))
3267            .collect::<BTreeSet<_>>();
3268        let mut additional = BTreeMap::new();
3269        for asset in domains
3270            .values()
3271            .filter_map(|domain| domain.pool.as_ref())
3272            .flat_map(|pool| &pool.assets)
3273        {
3274            if configured.contains(asset.principal.as_str()) {
3275                continue;
3276            }
3277            if additional
3278                .insert(asset.principal.clone(), asset.cycles)
3279                .is_some()
3280            {
3281                return Err(pool_configuration_error(format!(
3282                    "controlled pool canister {} is retained by more than one Root",
3283                    asset.principal
3284                )));
3285            }
3286        }
3287        Ok(additional)
3288    }
3289
3290    fn management_roots_are_current(
3291        &self,
3292        roots: &[(&crate::fleet_ensure::model::DesiredCanister, LiveCanister)],
3293        reviewed_targets: &BTreeSet<String>,
3294    ) -> Result<bool, IcpEnsurePlatformError> {
3295        let mut all_current = true;
3296        for (configured, live) in roots {
3297            if live.status != CanisterRuntimeStatus::Running || !reviewed_targets.is_empty() {
3298                all_current = false;
3299                continue;
3300            }
3301            if live.module_sha256.is_none()
3302                && is_unallocated_fresh_root(&self.desired, configured, reviewed_targets)
3303            {
3304                continue;
3305            }
3306            if let Some(wasm) = &configured.wasm {
3307                let expected = artifact_hash(&resolve_path(&self.root, wasm))?;
3308                if live.module_sha256.as_deref() != Some(expected.as_str()) {
3309                    all_current = false;
3310                }
3311            }
3312        }
3313        Ok(all_current)
3314    }
3315
3316    fn observe_protocol_readiness(
3317        &self,
3318        operation_id: &str,
3319        state: &FleetEnsureStateRecord,
3320        canisters: &BTreeMap<String, Option<LiveCanister>>,
3321    ) -> Result<BTreeMap<String, bool>, IcpEnsurePlatformError> {
3322        let principals = self.protocol_principals(state);
3323        self.desired
3324            .protocol_steps
3325            .iter()
3326            .map(|step| {
3327                let Some(principal) = principals.get(&step.canister) else {
3328                    return Ok((step.name.clone(), false));
3329                };
3330                let Some(live) = canisters.get(&step.canister).and_then(Option::as_ref) else {
3331                    return Ok((step.name.clone(), false));
3332                };
3333                let configured = self
3334                    .desired
3335                    .canisters
3336                    .iter()
3337                    .find(|configured| configured.name == step.canister)
3338                    .expect("protocol target was validated against desired canisters");
3339                if live.reinstall_required {
3340                    return Ok((step.name.clone(), false));
3341                }
3342                if let Some(wasm) = &configured.wasm {
3343                    let desired_hash = artifact_hash(&resolve_path(&self.root, wasm))?;
3344                    if live.module_sha256.as_deref() != Some(desired_hash.as_str()) {
3345                        return Ok((step.name.clone(), false));
3346                    }
3347                }
3348                let action = self.observed_protocol_action(step, principal.clone())?;
3349                protocol::observe(&self.icp, &self.root, operation_id, &principals, &action)
3350                    .map(|observation| (step.name.clone(), observation.applied))
3351                    .map_err(IcpEnsurePlatformError::from)
3352            })
3353            .collect()
3354    }
3355
3356    #[expect(
3357        clippy::too_many_arguments,
3358        reason = "one typed transfer effect carries the complete reviewed authority tuple"
3359    )]
3360    fn apply_transfer(
3361        &self,
3362        amount: u128,
3363        candid: &str,
3364        candid_sha256: &str,
3365        destination: &str,
3366        method: &str,
3367        operation_id: &str,
3368        principal: &str,
3369    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
3370        let candid = resolve_path(&self.root, candid);
3371        if !candid.is_file() {
3372            return Err(IcpEnsurePlatformError::CandidUnavailable(candid));
3373        }
3374        verify_artifact(&candid, candid_sha256, "drain Candid")?;
3375        let response: DrainResponse = self.icp.canister_call_candid(
3376            principal,
3377            method,
3378            &DrainRequest {
3379                operation_id: operation_id.to_string(),
3380                destination: parse_principal("treasury", destination)?,
3381                cycles: Nat::from(amount),
3382            },
3383            Some(&candid),
3384        )?;
3385        let transferred = match response {
3386            DrainResponse::Accepted { transferred_cycles }
3387            | DrainResponse::Replayed { transferred_cycles } => transferred_cycles,
3388        };
3389        if transferred != amount {
3390            return Err(IcpEnsurePlatformError::WrongTransferAmount {
3391                actual: transferred,
3392                expected: amount,
3393            });
3394        }
3395        Ok(EffectOutcome {
3396            created_principal: None,
3397            post_cycles: None,
3398            receipt: Some(operation_id.to_string()),
3399        })
3400    }
3401}
3402
3403impl EnsurePlatform for IcpEnsurePlatform {
3404    type Error = IcpEnsurePlatformError;
3405
3406    fn infrastructure_bootstrap_observation(
3407        &mut self,
3408        record: &crate::fleet_ensure::model::infrastructure_bootstrap::InfrastructureBootstrapRecord,
3409        state: &FleetEnsureStateRecord,
3410    ) -> Result<
3411        Option<
3412            crate::fleet_ensure::view::infrastructure_bootstrap::InfrastructureBootstrapObservation,
3413        >,
3414        Self::Error,
3415    > {
3416        super::infrastructure_bootstrap::observe(&self.icp, &self.desired, record, state)
3417            .map(Some)
3418            .map_err(Into::into)
3419    }
3420
3421    fn with_planning_observations<T, E>(
3422        &mut self,
3423        observe: impl FnOnce(&mut Self) -> Result<T, E>,
3424    ) -> Result<T, E> {
3425        self.measure_observation(FleetObservationStage::Planning, |platform| {
3426            platform.with_readonly_snapshot(observe)
3427        })
3428    }
3429
3430    fn with_preparation_observations<T>(
3431        &mut self,
3432        action: &EnsureAction,
3433        observe: impl FnOnce(&mut Self) -> Result<T, Self::Error>,
3434    ) -> Result<T, Self::Error> {
3435        if matches!(action, EnsureAction::Install { .. }) {
3436            // A single fresh status supplies balance and version before intent. Even
3437            // an enclosing scope must not carry older authority into preparation.
3438            self.observation_snapshot.take();
3439            self.with_observation_snapshot(observe)
3440        } else {
3441            observe(self)
3442        }
3443    }
3444
3445    fn with_independent_observations<T, E>(
3446        &mut self,
3447        observe: impl FnOnce(&mut Self) -> Result<T, E>,
3448    ) -> Result<T, E> {
3449        // Every pass starts fresh, including retries and nested invocations.
3450        self.staging_observations = Some(current_protocol::StoreStagingObservations::default());
3451        let result = observe(self);
3452        self.staging_observations = None;
3453        result
3454    }
3455
3456    fn report_progress(&mut self, progress: FleetEnsureProgress) {
3457        if let Some(handler) = self.progress_handler.as_mut() {
3458            handler(progress);
3459        }
3460    }
3461
3462    fn require_retained_root_activation(
3463        &mut self,
3464        operation_id: &str,
3465        root: &str,
3466        state: &FleetEnsureStateRecord,
3467    ) -> Result<(), Self::Error> {
3468        let authorities = canic_init::compile_root_authorities(
3469            &self.root,
3470            &self.desired,
3471            &self.protocol_principals(state),
3472        )?;
3473        let (_, authority) = authorities
3474            .iter()
3475            .find(|(name, _)| name == root)
3476            .ok_or_else(|| canic_init::CanicInitError::MissingRoot(root.to_string()))?;
3477        current_protocol::require_store_installation_binding(
3478            &self.icp,
3479            &self.root_protocol_candid()?,
3480            root,
3481            authority,
3482            operation_id,
3483        )?;
3484        Ok(())
3485    }
3486
3487    fn bind_reviewed_desired(&mut self, desired: &DesiredFleet) -> Result<(), Self::Error> {
3488        self.infrastructure_bootstrap = None;
3489        self.bootstrap_registration_recovery = None;
3490        self.invalidate_observation_snapshot();
3491        self.desired = desired.clone();
3492        Ok(())
3493    }
3494
3495    fn bind_infrastructure_bootstrap(
3496        &mut self,
3497        source: &crate::fleet_ensure::model::infrastructure_bootstrap::InfrastructureBootstrapRecord,
3498    ) -> Result<(), Self::Error> {
3499        self.infrastructure_bootstrap = Some(source.clone());
3500        Ok(())
3501    }
3502
3503    fn bind_bootstrap_registration_recovery(
3504        &mut self,
3505        review: &crate::fleet_ensure::model::infrastructure_bootstrap::registration_recovery::BootstrapRegistrationReviewRecord,
3506    ) -> Result<bool, Self::Error> {
3507        self.bootstrap_registration_recovery = Some(review.clone());
3508        Ok(true)
3509    }
3510
3511    fn verify_bootstrap_coordinator(
3512        &mut self,
3513        state: &FleetEnsureStateRecord,
3514    ) -> Result<bool, Self::Error> {
3515        let source = self
3516            .infrastructure_bootstrap
3517            .as_ref()
3518            .ok_or(super::infrastructure_bootstrap::InfrastructureBootstrapError::Integrity)?;
3519        super::infrastructure_bootstrap::verify_coordinator(
3520            &self.icp,
3521            &self.root,
3522            &self.desired,
3523            source,
3524            state,
3525        )?;
3526        Ok(true)
3527    }
3528
3529    fn verify_bootstrap_registry(
3530        &mut self,
3531        expected: &canic_core::dto::fleet_registry::FleetRegistry,
3532    ) -> Result<bool, Self::Error> {
3533        let source = self
3534            .infrastructure_bootstrap
3535            .as_ref()
3536            .ok_or(super::infrastructure_bootstrap::InfrastructureBootstrapError::Integrity)?;
3537        super::infrastructure_bootstrap::verify_registry(
3538            &self.icp,
3539            &self.desired,
3540            source,
3541            expected,
3542        )?;
3543        Ok(true)
3544    }
3545
3546    fn with_activity<T, E>(
3547        &mut self,
3548        stage: FleetObservationStage,
3549        activity: impl FnOnce(&mut Self) -> Result<T, E>,
3550    ) -> Result<T, E> {
3551        self.measure_observation(stage, activity)
3552    }
3553
3554    fn pace_effect_observation(
3555        &mut self,
3556        _action: &EnsureAction,
3557        consecutive_unchanged_observations: u32,
3558    ) {
3559        self.invalidate_observation_snapshot();
3560        let _: Result<(), std::convert::Infallible> =
3561            self.measure_observation(FleetObservationStage::Backoff, |platform| {
3562                thread::sleep(protocol_observation_delay(
3563                    consecutive_unchanged_observations,
3564                    platform.initial_observation_delay,
3565                    platform.maximum_observation_delay,
3566                ));
3567                Ok(())
3568            });
3569    }
3570
3571    fn pace_root_owned_observation(
3572        &mut self,
3573        _target: &str,
3574        consecutive_retained_observations: u32,
3575    ) {
3576        self.invalidate_observation_snapshot();
3577        let _: Result<(), std::convert::Infallible> =
3578            self.measure_observation(FleetObservationStage::Backoff, |platform| {
3579                thread::sleep(protocol_observation_delay(
3580                    consecutive_retained_observations,
3581                    platform.initial_observation_delay,
3582                    platform.maximum_observation_delay,
3583                ));
3584                Ok(())
3585            });
3586    }
3587
3588    fn reinstall_assets_match(
3589        &mut self,
3590        intent: &crate::fleet_ensure::model::FleetReinstallRecord,
3591        root_name: &str,
3592        check: super::ReinstallAssetCheck,
3593    ) -> Result<bool, Self::Error> {
3594        self.timed_observation(FleetObservationStage::RootManagement, |platform| {
3595            let binding = intent
3596                .authorities
3597                .iter()
3598                .find(|binding| binding.name == root_name)
3599                .ok_or_else(|| {
3600                    IcpEnsurePlatformError::RootManagement("missing reset Root".to_string())
3601                })?;
3602            let principal = parse_principal("reset Root", &binding.principal)?;
3603            let candid = platform.root_protocol_candid()?;
3604            let assets = intent
3605                .assets
3606                .iter()
3607                .filter(|asset| asset.root == root_name)
3608                .collect::<Vec<_>>();
3609            Self::reinstall_assets_match_bound(&platform.icp, &candid, principal, &assets, check)
3610        })
3611    }
3612
3613    fn reinstall_authorities(
3614        &mut self,
3615        state: &FleetEnsureStateRecord,
3616    ) -> Result<Option<BTreeMap<String, RootManagementCanisterObservation>>, Self::Error> {
3617        self.timed_observation(FleetObservationStage::RootManagement, |platform| {
3618            platform.require_operator()?;
3619            let network =
3620                resolve_icp_build_network_from_root(&platform.root, &platform.desired.environment)
3621                    .map_err(|error| IcpEnsurePlatformError::RootManagement(error.to_string()))?;
3622            let catalog = if network == BuildNetwork::Ic {
3623                let now = SystemTime::now()
3624                    .duration_since(UNIX_EPOCH)
3625                    .map_err(|_| {
3626                        IcpEnsurePlatformError::RootManagement(
3627                            "system clock is before the Unix epoch".to_string(),
3628                        )
3629                    })?
3630                    .as_secs();
3631                Some(
3632                    load_cached_mainnet_subnet_catalog(&platform.root, now)
3633                        .map_err(IcpEnsurePlatformError::SubnetCatalog)?,
3634                )
3635            } else {
3636                None
3637            };
3638            let mut authorities = BTreeMap::new();
3639            let root_targets = platform
3640                .desired
3641                .canisters
3642                .iter()
3643                .filter(|c| c.kind == DesiredCanisterKind::Root)
3644                .map(|c| c.name.clone())
3645                .collect();
3646            let roots = platform
3647                .observe_root_management(state, &root_targets)?
3648                .ok_or_else(|| {
3649                    IcpEnsurePlatformError::RootManagement("missing Root authority".to_string())
3650                })?;
3651            let configured = platform
3652                .desired
3653                .canisters
3654                .iter()
3655                .filter(|c| c.kind != DesiredCanisterKind::Pool)
3656                .collect::<Vec<_>>();
3657            for batch in configured.chunks(super::bounded_observations::MAX_IN_FLIGHT) {
3658                // Root prerequisites finish first. Drain this batch, then consume its
3659                // outcomes in configured order before issuing any later batch.
3660                let mut statuses = platform.read_status_batch(
3661                    batch
3662                        .iter()
3663                        .filter(|c| !roots.roots.contains_key(&c.name))
3664                        .filter_map(|c| platform.current_principal(state, &c.name)),
3665                );
3666                for configured in batch {
3667                    let principal = platform
3668                        .current_principal(state, &configured.name)
3669                        .ok_or_else(|| {
3670                            IcpEnsurePlatformError::UnresolvedCreated(configured.name.clone())
3671                        })?;
3672                    // Reuse only this pass's Root observation; a subsequent guard starts fresh.
3673                    let live = if let Some(root) = roots.roots.get(&configured.name) {
3674                        platform.complete_install_status(principal, root.live.clone())?
3675                    } else {
3676                        let live = platform
3677                            .status_batch_response(&mut statuses, principal)?
3678                            .ok_or_else(|| {
3679                                IcpEnsurePlatformError::RootManagement(
3680                                    "missing infrastructure".to_string(),
3681                                )
3682                            })?;
3683                        platform.complete_install_status(principal, live)?
3684                    };
3685                    let subnet = if let Some(root) = roots.roots.get(&configured.name) {
3686                        root.subnet.clone()
3687                    } else if let Some(catalog) = &catalog {
3688                        catalog
3689                            .catalog
3690                            .resolve_canister_route(principal)
3691                            .map(|route| route.subnet.to_text())
3692                            .map_err(|error| {
3693                                IcpEnsurePlatformError::RootManagement(error.to_string())
3694                            })?
3695                    } else {
3696                        configured.subnet.clone()
3697                    };
3698                    authorities.insert(
3699                        configured.name.clone(),
3700                        RootManagementCanisterObservation {
3701                            live,
3702                            name: configured.name.clone(),
3703                            subnet,
3704                        },
3705                    );
3706                }
3707            }
3708            Ok(Some(authorities))
3709        })
3710    }
3711
3712    #[expect(
3713        clippy::too_many_lines,
3714        reason = "one source observation binds old protocol receipts, full pagination and physical management inspection"
3715    )]
3716    fn activation_reset_inventory(
3717        &mut self,
3718        source: &crate::fleet_ensure::model::FleetActivationSourceRecord,
3719        state: &FleetEnsureStateRecord,
3720    ) -> Result<Option<super::FleetActivationResetObservation>, Self::Error> {
3721        if source.operator != self.desired.operator
3722            || source.cycles_ledger != self.desired.cycles_ledger
3723        {
3724            return Err(pool_configuration_error(
3725                "source operator or Cycles Ledger differs from the requested reset".to_string(),
3726            ));
3727        }
3728        let authorities = self
3729            .reinstall_authorities(state)?
3730            .ok_or_else(|| pool_configuration_error("missing source infrastructure".to_string()))?;
3731        let mut observation = FleetObservation {
3732            additional_controlled_cycles: BTreeMap::new(),
3733            canisters: authorities
3734                .iter()
3735                .map(|(name, observed)| (name.clone(), Some(observed.live.clone())))
3736                .collect(),
3737            estate_funding_domains: BTreeMap::new(),
3738            ledger_fee_cycles: self
3739                .desired
3740                .ledger_fee_cycles
3741                .parse::<Cycles>()
3742                .map_err(|_| pool_configuration_error("invalid source fee bound".to_string()))?
3743                .to_u128(),
3744            operator_cycles: 0,
3745            protocol_ready: BTreeMap::new(),
3746        };
3747        let mut roots = Vec::new();
3748        let mut candid_by_root = BTreeMap::new();
3749        for configured in &self.desired.canisters {
3750            if configured.kind != DesiredCanisterKind::Root {
3751                continue;
3752            }
3753            let live = &authorities
3754                .get(&configured.name)
3755                .ok_or_else(|| pool_configuration_error("missing source Root".to_string()))?
3756                .live;
3757            let root = parse_principal("source Root", &live.principal)?;
3758            let evidence = current_protocol::observe_inactive_activation(
3759                &self.icp,
3760                &self.root,
3761                source,
3762                &configured.name,
3763                root,
3764            )?;
3765            let candid = source
3766                .registry_preparations
3767                .iter()
3768                .find_map(|action| match action {
3769                    EnsureAction::FleetProtocol {
3770                        principal, candid, ..
3771                    } if principal == &live.principal => Some(self.root.join(candid)),
3772                    _ => None,
3773                })
3774                .ok_or_else(|| {
3775                    pool_configuration_error("missing source Root read contract".to_string())
3776                })?;
3777            let mut inventory = EstatePoolInventoryAccumulator::default();
3778            let mut cursor = None;
3779            loop {
3780                let page = self.query_estate_pool_page(root, cursor)?;
3781                if !inactive_source_pool_has_full_inventory(&page, &evidence) {
3782                    return Err(pool_configuration_error(
3783                        "source pool has unsettled work or spare creation capacity".to_string(),
3784                    ));
3785                }
3786                let next = inventory.observe_page(&configured.name, page)?;
3787                if next.is_none() {
3788                    break;
3789                }
3790                if next == cursor {
3791                    return Err(pool_configuration_error(
3792                        "source pool cursor did not advance".to_string(),
3793                    ));
3794                }
3795                cursor = next;
3796            }
3797            observation.estate_funding_domains.insert(
3798                configured.name.clone(),
3799                EstateFundingDomainObservation {
3800                    balance_cycles: Some(self.cycles_ledger_balance(&live.principal)?),
3801                    cycles_ledger: self.desired.cycles_ledger.clone(),
3802                    pool: Some(inventory.finish(&configured.name)?),
3803                    root_principal: Some(live.principal.clone()),
3804                },
3805            );
3806            candid_by_root.insert(configured.name.clone(), candid);
3807            roots.push(evidence);
3808        }
3809        if roots.is_empty() || roots.len() != source.registry_preparations.len() {
3810            return Err(pool_configuration_error(
3811                "source Root inventory is incomplete".to_string(),
3812            ));
3813        }
3814        let assets = self.inspect_reinstall_assets(
3815            state,
3816            &authorities,
3817            &mut observation,
3818            &candid_by_root,
3819            false,
3820        )?;
3821        Ok(Some(super::FleetActivationResetObservation {
3822            inventory: super::FleetReinstallObservation {
3823                authorities,
3824                assets,
3825                observation,
3826            },
3827            roots,
3828        }))
3829    }
3830
3831    fn activation_reset_inventory_after_reset(
3832        &mut self,
3833        intent: &crate::fleet_ensure::model::FleetReinstallRecord,
3834        state: &FleetEnsureStateRecord,
3835    ) -> Result<Option<super::FleetReinstallObservation>, Self::Error> {
3836        let authorities = self
3837            .reinstall_authorities(state)?
3838            .ok_or_else(|| pool_configuration_error("missing reset infrastructure".to_string()))?;
3839        let mut observation = FleetObservation {
3840            canisters: authorities
3841                .iter()
3842                .map(|(name, entry)| (name.clone(), Some(entry.live.clone())))
3843                .collect(),
3844            additional_controlled_cycles: BTreeMap::new(),
3845            estate_funding_domains: BTreeMap::new(),
3846            ledger_fee_cycles: self
3847                .desired
3848                .ledger_fee_cycles
3849                .parse::<Cycles>()
3850                .map_err(|_| pool_configuration_error("invalid reset Ledger fee".to_string()))?
3851                .to_u128(),
3852            operator_cycles: 0,
3853            protocol_ready: BTreeMap::new(),
3854        };
3855        let candid = self.root_protocol_candid()?;
3856        let mut candid_by_root = BTreeMap::new();
3857        for configured in &self.desired.canisters {
3858            if configured.kind != DesiredCanisterKind::Root {
3859                continue;
3860            }
3861            let binding = authorities
3862                .get(&configured.name)
3863                .ok_or_else(|| pool_configuration_error("missing reset Root".to_string()))?;
3864            let root = parse_principal("reset Root", &binding.live.principal)?;
3865            let mut inventory = EstatePoolInventoryAccumulator::default();
3866            let mut cursor = None;
3867            loop {
3868                let page = self.query_estate_pool_page(root, cursor)?;
3869                let next = inventory.observe_page(&configured.name, page)?;
3870                if next.is_none() {
3871                    break;
3872                }
3873                if next == cursor {
3874                    return Err(pool_configuration_error(
3875                        "reset inventory cursor did not advance".to_string(),
3876                    ));
3877                }
3878                cursor = next;
3879            }
3880            let pool = inventory.finish(&configured.name)?;
3881            let expected = intent
3882                .assets
3883                .iter()
3884                .filter(|asset| asset.root == configured.name)
3885                .map(|asset| asset.principal.as_str())
3886                .collect::<BTreeSet<_>>();
3887            let observed = pool
3888                .assets
3889                .iter()
3890                .map(|asset| asset.principal.as_str())
3891                .collect::<BTreeSet<_>>();
3892            if observed != expected || pool.pending_creation.is_some() {
3893                return Err(pool_configuration_error(
3894                    "reset changed the complete physical estate".to_string(),
3895                ));
3896            }
3897            observation.estate_funding_domains.insert(
3898                configured.name.clone(),
3899                EstateFundingDomainObservation {
3900                    balance_cycles: Some(self.cycles_ledger_balance(&binding.live.principal)?),
3901                    cycles_ledger: self.desired.cycles_ledger.clone(),
3902                    pool: Some(pool),
3903                    root_principal: Some(binding.live.principal.clone()),
3904                },
3905            );
3906            candid_by_root.insert(configured.name.clone(), candid.clone());
3907        }
3908        let assets = self.inspect_reinstall_assets(
3909            state,
3910            &authorities,
3911            &mut observation,
3912            &candid_by_root,
3913            true,
3914        )?;
3915        Ok(Some(super::FleetReinstallObservation {
3916            authorities,
3917            assets,
3918            observation,
3919        }))
3920    }
3921
3922    fn observe_root_management(
3923        &mut self,
3924        state: &FleetEnsureStateRecord,
3925        reviewed_targets: &BTreeSet<String>,
3926    ) -> Result<Option<RootManagementObservation>, Self::Error> {
3927        self.timed_observation(FleetObservationStage::RootManagement, |platform| {
3928            let configured_roots = platform
3929                .desired
3930                .canisters
3931                .iter()
3932                .filter(|configured| {
3933                    configured.kind == DesiredCanisterKind::Root
3934                        && configured.presence
3935                            == crate::fleet_ensure::model::DesiredPresence::Present
3936                })
3937                .collect::<Vec<_>>();
3938            if configured_roots.is_empty() {
3939                return Ok(None);
3940            }
3941            platform.require_operator()?;
3942            let mut observed_roots = Vec::new();
3943            for configured in configured_roots {
3944                let Some(principal) = platform.current_principal(state, &configured.name) else {
3945                    if is_unallocated_fresh_root(&platform.desired, configured, reviewed_targets) {
3946                        continue;
3947                    }
3948                    return Err(IcpEnsurePlatformError::RootManagement(format!(
3949                        "configured Root {} has no exact Principal",
3950                        configured.name
3951                    )));
3952                };
3953                let live = platform.status_optional(principal)?.ok_or_else(|| {
3954                    IcpEnsurePlatformError::RootManagement(format!(
3955                        "configured Root {} is unavailable",
3956                        configured.name
3957                    ))
3958                })?;
3959                observed_roots.push((configured, live));
3960            }
3961            let all_current =
3962                platform.management_roots_are_current(&observed_roots, reviewed_targets)?;
3963            if reviewed_targets.is_empty() && all_current {
3964                return Ok(None);
3965            }
3966            let network =
3967                resolve_icp_build_network_from_root(&platform.root, &platform.desired.environment)
3968                    .map_err(|error| IcpEnsurePlatformError::RootManagement(error.to_string()))?;
3969            let catalog = if network == BuildNetwork::Ic {
3970                let now = SystemTime::now()
3971                    .duration_since(UNIX_EPOCH)
3972                    .map_err(|_| {
3973                        IcpEnsurePlatformError::RootManagement(
3974                            "system clock is before the Unix epoch".to_string(),
3975                        )
3976                    })?
3977                    .as_secs();
3978                Some(
3979                    load_cached_mainnet_subnet_catalog(&platform.root, now)
3980                        .map_err(IcpEnsurePlatformError::SubnetCatalog)?,
3981                )
3982            } else {
3983                None
3984            };
3985            let mut roots = BTreeMap::new();
3986            for (configured, live) in observed_roots {
3987                let principal = live.principal.clone();
3988                let subnet = catalog.as_ref().map_or_else(
3989                    || Ok(configured.subnet.clone()),
3990                    |catalog| {
3991                        catalog
3992                            .catalog
3993                            .resolve_canister_route(&principal)
3994                            .map(|route| route.subnet.to_text())
3995                            .map_err(|error| {
3996                                IcpEnsurePlatformError::RootManagement(error.to_string())
3997                            })
3998                    },
3999                )?;
4000                let name = configured.name.clone();
4001                if roots
4002                    .insert(
4003                        name.clone(),
4004                        RootManagementCanisterObservation { live, name, subnet },
4005                    )
4006                    .is_some()
4007                {
4008                    return Err(IcpEnsurePlatformError::RootManagement(
4009                        "configured Root names are not unique".to_string(),
4010                    ));
4011                }
4012            }
4013            let operator_cycles = platform
4014                .icp
4015                .identity_cycles_balance()
4016                .map_err(|error| IcpEnsurePlatformError::LedgerWithdraw(error.to_string()))?;
4017            Ok(Some(RootManagementObservation {
4018                operator_cycles,
4019                roots,
4020            }))
4021        })
4022    }
4023
4024    fn observe_operator_funding(
4025        &mut self,
4026    ) -> Result<Option<crate::fleet_ensure::view::OperatorFundingObservation>, Self::Error> {
4027        self.require_operator()?;
4028        let ledger_fee_cycles = ledger_fee_cycles(self.icp.canister_query_candid(
4029            &self.desired.cycles_ledger,
4030            "icrc1_fee",
4031            &(),
4032            None,
4033        )?)?;
4034        Ok(Some(
4035            crate::fleet_ensure::view::OperatorFundingObservation {
4036                cycles_ledger: self.desired.cycles_ledger.clone(),
4037                ledger_fee_cycles,
4038                operator_cycles: self.cycles_ledger_balance(&self.desired.operator)?,
4039            },
4040        ))
4041    }
4042
4043    fn observe_native_funding(
4044        &mut self,
4045        root: &str,
4046        state: &FleetEnsureStateRecord,
4047    ) -> Result<Option<crate::fleet_ensure::model::NativeFundingObservation>, Self::Error> {
4048        self.require_operator()?;
4049        let configured = self
4050            .desired
4051            .canisters
4052            .iter()
4053            .find(|configured| {
4054                configured.name == root
4055                    && configured.kind == DesiredCanisterKind::Root
4056                    && configured.presence == crate::fleet_ensure::model::DesiredPresence::Present
4057            })
4058            .ok_or_else(|| {
4059                IcpEnsurePlatformError::RootManagement(
4060                    "native funding requires a configured Root".into(),
4061                )
4062            })?;
4063        let principal = self
4064            .current_principal(state, &configured.name)
4065            .ok_or_else(|| {
4066                IcpEnsurePlatformError::RootManagement(
4067                    "native funding requires an allocated Root".into(),
4068                )
4069            })?;
4070        let live = self.status_optional(principal)?.ok_or_else(|| {
4071            IcpEnsurePlatformError::RootManagement("native funding Root is unavailable".into())
4072        })?;
4073        if let Some(wasm) = &configured.wasm {
4074            let expected = artifact_hash(&resolve_path(&self.root, wasm))?;
4075            if live.module_sha256.as_deref() != Some(&expected) {
4076                return Err(IcpEnsurePlatformError::RootManagement(
4077                    "native funding Root module differs from the reviewed artifact".into(),
4078                ));
4079            }
4080        }
4081        let fee = ledger_fee_cycles(self.icp.canister_query_candid(
4082            &self.desired.cycles_ledger,
4083            "icrc1_fee",
4084            &(),
4085            None,
4086        )?)?;
4087        let operator_cycles = self.cycles_ledger_balance(&self.desired.operator)?;
4088        Ok(Some(crate::fleet_ensure::model::NativeFundingObservation {
4089            cycles_ledger: self.desired.cycles_ledger.clone(),
4090            ledger_fee_cycles: fee,
4091            live,
4092            operator_cycles,
4093        }))
4094    }
4095
4096    fn observe(
4097        &mut self,
4098        operation_id: &str,
4099        state: &FleetEnsureStateRecord,
4100    ) -> Result<FleetObservation, Self::Error> {
4101        self.timed_observation(FleetObservationStage::FleetSnapshot, |platform| {
4102            platform.with_observation_snapshot(|platform| {
4103                platform.observe_fleet_snapshot(operation_id, state)
4104            })
4105        })
4106    }
4107
4108    fn protocol_actions(
4109        &mut self,
4110        operation_id: &str,
4111        state: &FleetEnsureStateRecord,
4112    ) -> Result<Vec<EnsureAction>, Self::Error> {
4113        self.timed_observation(FleetObservationStage::ProtocolActions, |platform| {
4114            if platform.desired.protocol.is_none() {
4115                return Ok(Vec::new());
4116            }
4117            current_protocol::validate_component_pool_capacity(&platform.root, &platform.desired)?;
4118            if !platform.current_protocol_owners_are_ready(state)? {
4119                return Ok(Vec::new());
4120            }
4121            let reconciliation = current_protocol::compile_pool_reconciliation(
4122                &platform.root,
4123                &platform.desired,
4124                state,
4125                &platform.estate_observations,
4126            )?;
4127            if !reconciliation.is_empty() {
4128                return Ok(reconciliation);
4129            }
4130            current_protocol::compile(
4131                &platform.icp,
4132                &platform.root,
4133                &platform.desired,
4134                operation_id,
4135                state,
4136            )
4137            .map_err(Into::into)
4138        })
4139    }
4140
4141    fn infrastructure_protocol_actions(
4142        &mut self,
4143        operation_id: &str,
4144        state: &FleetEnsureStateRecord,
4145    ) -> Result<Vec<EnsureAction>, Self::Error> {
4146        current_protocol::compile_infrastructure_protocol(
4147            &self.root,
4148            &self.desired,
4149            state,
4150            operation_id,
4151        )
4152        .map_err(Into::into)
4153    }
4154
4155    fn fresh_protocol_actions(
4156        &mut self,
4157        operation_id: &str,
4158        state: &FleetEnsureStateRecord,
4159    ) -> Result<Vec<EnsureAction>, Self::Error> {
4160        current_protocol::compile_fresh_protocol(&self.root, &self.desired, state, operation_id)
4161            .map_err(Into::into)
4162    }
4163
4164    fn terminal_inventory(
4165        &mut self,
4166        operation_id: &str,
4167        state: &FleetEnsureStateRecord,
4168    ) -> Result<TerminalFleetInventory, Self::Error> {
4169        self.timed_observation(FleetObservationStage::TerminalInventory, |platform| {
4170            if platform.desired.protocol.is_none() {
4171                if state.active_registry.is_some() {
4172                    return Err(current_protocol::CurrentProtocolError::Configuration(
4173                        "managed terminal Fleet cannot drop its typed protocol intent".to_string(),
4174                    )
4175                    .into());
4176                }
4177                return Ok(TerminalFleetInventory::default());
4178            }
4179            super::current_inventory::terminal_inventory(
4180                &platform.icp,
4181                &platform.root,
4182                &platform.desired,
4183                operation_id,
4184                state,
4185            )
4186            .map_err(Into::into)
4187        })
4188    }
4189
4190    #[expect(
4191        clippy::too_many_lines,
4192        reason = "the platform keeps every effect's exact live terminal predicate in one exhaustive match"
4193    )]
4194    fn observe_effect(
4195        &mut self,
4196        operation_id: &str,
4197        action: &EnsureAction,
4198        record: &EffectRecord,
4199        state: &FleetEnsureStateRecord,
4200    ) -> Result<EffectObservation, Self::Error> {
4201        self.measure_observation(FleetObservationStage::EffectReconciliation, |platform| {
4202        let mut retry = EffectRetry::None;
4203        let mut post_cycles = None;
4204        let (applied, progress_identity) = match action {
4205            EnsureAction::Create {
4206                name,
4207                requested_initial_cycles,
4208                ..
4209            } => {
4210                let (live_cycles, deferred) =
4211                    if record.created_principal.is_some() && record.receipt.is_some() {
4212                        platform.created_canister_cycles(
4213                            name,
4214                            record
4215                                .created_principal
4216                                .as_deref()
4217                                .ok_or(IcpEnsurePlatformError::LedgerCreatePending)?,
4218                            state,
4219                        )?
4220                    } else {
4221                        (None, false)
4222                    };
4223                post_cycles = live_cycles;
4224                let maximum_observation_burn_cycles =
4225                    super::maximum_creation_observation_burn(&platform.desired).ok_or(
4226                        IcpEnsurePlatformError::Arithmetic("Create execution and observation burn"),
4227                    )?;
4228                let applied = create_balance_is_terminal(
4229                    live_cycles,
4230                    *requested_initial_cycles,
4231                    maximum_observation_burn_cycles,
4232                );
4233                if live_cycles.is_some() && !applied {
4234                    retry = EffectRetry::ReplanRequiredAfterCreateBalanceDrift;
4235                } else if deferred {
4236                    retry = EffectRetry::DeferUntilControllerObservation;
4237                }
4238                (
4239                    applied,
4240                    format!(
4241                        "created:{:?}:actual:{live_cycles:?}:requested:{requested_initial_cycles}",
4242                        record.created_principal,
4243                    ),
4244                )
4245            }
4246            EnsureAction::Delete { principal, .. } => {
4247                let live = platform.status_optional(Self::action_principal(state, principal)?)?;
4248                (live.is_none(), format!("delete:{live:?}"))
4249            }
4250            EnsureAction::Fund {
4251                amount,
4252                expected_post_cycles,
4253                funding_deficit_cycles,
4254                funding_margin_cycles,
4255                ..
4256            } => {
4257                let live_cycles = platform.action_cycles(action, state)?;
4258                post_cycles = live_cycles;
4259                let observation = NativeFundingObservation {
4260                    amount: *amount,
4261                    expected_post_cycles: *expected_post_cycles,
4262                    funding_deficit_cycles: *funding_deficit_cycles,
4263                    funding_margin_cycles: *funding_margin_cycles,
4264                    live_cycles,
4265                    pre_cycles: record.pre_cycles,
4266                };
4267                (
4268                    native_funding_completion(observation, record.receipt.is_some())?,
4269                    format!(
4270                        "native-topup:ledger-withdraw:{}:actual:{live_cycles:?}:expected:{expected_post_cycles}:margin:{funding_margin_cycles}",
4271                        record.receipt.as_deref().unwrap_or("pending"),
4272                    ),
4273                )
4274            }
4275            EnsureAction::FundEstate {
4276                amount,
4277                expected_post_cycles,
4278                ledger_fee_cycles,
4279                principal,
4280                ..
4281            } => {
4282                let source_cycles = platform
4283                    .icp
4284                    .identity_cycles_balance()
4285                    .map_err(|error| IcpEnsurePlatformError::LedgerTransfer(error.to_string()))?;
4286                let target = Self::action_principal(state, principal)?;
4287                let destination_cycles = platform.cycles_ledger_balance(target)?;
4288                post_cycles = Some(source_cycles);
4289                (
4290                    record.receipt.is_some()
4291                        && estate_funding_applied(EstateFundingObservation {
4292                            amount: *amount,
4293                            destination_after: destination_cycles,
4294                            destination_before: record.destination_pre_cycles,
4295                            expected_destination_after: *expected_post_cycles,
4296                            ledger_fee_cycles: *ledger_fee_cycles,
4297                            source_after: source_cycles,
4298                            source_before: record.pre_cycles,
4299                        }),
4300                    format!(
4301                        "estate-funding:ledger-transfer:{}:source:{source_cycles}:destination:{destination_cycles}:expected:{expected_post_cycles}",
4302                        record.receipt.as_deref().unwrap_or("pending"),
4303                    ),
4304                )
4305            }
4306            EnsureAction::Install {
4307                mode,
4308                reinstall_witness,
4309                principal,
4310                wasm_sha256,
4311                ..
4312            } => {
4313                let live =
4314                    platform.install_status_optional(Self::action_principal(state, principal)?)?;
4315                post_cycles = platform.direct_reconciliation_cycles(action.name(), live.as_ref());
4316                let applied = if let Some(witness) = reinstall_witness {
4317                    let current = platform.reinstall_authorities(state)?;
4318                    let authority = current
4319                        .as_ref()
4320                        .and_then(|a| a.get(&witness.authority.name))
4321                        .and_then(super::reinstall::authority_binding);
4322                    let history_authority = authority
4323                        .as_ref()
4324                        .and_then(|current| {
4325                            crate::fleet_ensure::policy::reinstall::history_authority(
4326                                witness,
4327                                current,
4328                                principal,
4329                                wasm_sha256,
4330                                &record.state,
4331                            )
4332                        })
4333                        .ok_or_else(|| IcpEnsurePlatformError::ReinstallHistoryConflict {
4334                            canister: witness.authority.principal.clone(),
4335                        })?;
4336                    let live = live.as_ref().ok_or_else(|| {
4337                        IcpEnsurePlatformError::ReinstallHistoryConflict {
4338                            canister: principal.clone(),
4339                        }
4340                    })?;
4341                    let before = record.pre_canister_version.ok_or_else(|| {
4342                        IcpEnsurePlatformError::ReinstallHistoryConflict {
4343                            canister: principal.clone(),
4344                        }
4345                    })?;
4346                    let history = super::install_history::observe(
4347                        &platform.icp,
4348                        &platform.root,
4349                        &history_authority,
4350                        parse_principal("operator", &platform.desired.operator)?,
4351                        before,
4352                        live,
4353                    )?;
4354                    match super::install_history::reconcile(
4355                        history,
4356                        wasm_sha256,
4357                        &witness.prior_module_sha256,
4358                        before,
4359                        live,
4360                    ) {
4361                        super::install_history::HistoryEffect::Applied => true,
4362                        super::install_history::HistoryEffect::NotApplied => false,
4363                        super::install_history::HistoryEffect::Conflict => {
4364                            return Err(IcpEnsurePlatformError::ReinstallHistoryConflict {
4365                                canister: live.principal.clone(),
4366                            });
4367                        }
4368                    }
4369
4370                } else {
4371                    live.as_ref().is_some_and(|live| {
4372                        install_effect_applied(
4373                            *mode,
4374                            wasm_sha256,
4375                            live.module_sha256.as_deref(),
4376                            record.pre_canister_version,
4377                            live.canister_version,
4378                        )
4379                    })
4380                };
4381                let progress_identity = live.as_ref().map_or_else(
4382                    || "install:missing".to_string(),
4383                    |live| {
4384                        format!(
4385                            "install:{:?}:version:{:?}",
4386                            live.module_sha256, live.canister_version
4387                        )
4388                    },
4389                );
4390                (applied, progress_identity)
4391            }
4392            EnsureAction::FleetProtocol {
4393                action: current_action,
4394                ..
4395            } => {
4396                let observed = match platform.staging_observations.as_mut() {
4397                    Some(staging) => {
4398                        let before = staging.cached_reads();
4399                        let result = current_protocol::observe_with_staging(
4400                            &platform.icp, &platform.root, action, staging,
4401                        );
4402                        if staging.cached_reads() > before {
4403                            platform.record_cached_read();
4404                        }
4405                        result
4406                    }
4407                    None => current_protocol::observe(&platform.icp, &platform.root, action),
4408                };
4409                let mut observation = match observed
4410                {
4411                    Ok(observation) => observation,
4412                    Err(error)
4413                        if matches!(
4414                            current_action.as_ref(),
4415                            crate::fleet_ensure::model::CurrentFleetProtocolAction::AdoptStore { .. }
4416                        ) && recoverable_current_protocol_error(&error) =>
4417                    {
4418                        EffectObservation {
4419                            provisioning_progress: None,
4420                            provisioning_failure: None,
4421                            applied: false,
4422                            estate_funding_required: None,
4423                            post_cycles: None,
4424                            progress_identity: "store-adoption:protected-status-unavailable"
4425                                .to_string(),
4426                            retry: EffectRetry::None,
4427                        }
4428                    }
4429                    Err(error) => return Err(error.into()),
4430                };
4431                platform.refresh_estate_funding_observation(&mut observation, state)?;
4432                return Ok(observation);
4433            }
4434            EnsureAction::Protocol { .. } => {
4435                let observation = protocol::observe(
4436                    &platform.icp,
4437                    &platform.root,
4438                    operation_id,
4439                    &platform.protocol_principals(state),
4440                    &resolved_protocol_action(action, state)?,
4441                )?;
4442                (observation.applied, observation.progress_identity)
4443            }
4444            EnsureAction::SetControllers {
4445                controller_canisters,
4446                controllers,
4447                name,
4448                principal,
4449                ..
4450            } => {
4451                let expected =
4452                    platform.resolved_controllers(state, controllers, controller_canisters)?;
4453                let principal = Self::action_principal(state, principal)?;
4454                let configured = platform
4455                    .desired
4456                    .canisters
4457                    .iter()
4458                    .find(|configured| configured.name == *name);
4459                let mut observed_controllers = if let Some(configured) =
4460                    configured.filter(|configured| configured.kind == DesiredCanisterKind::Pool)
4461                {
4462                    platform.inspect_root_owned_canister(configured, principal, state)?
4463                        .map(|response| {
4464                            response
4465                                .settings
4466                                .controllers
4467                                .into_iter()
4468                                .map(|controller| controller.to_text())
4469                                .collect::<Vec<_>>()
4470                        })
4471                } else {
4472                    platform.status_optional(principal)?
4473                        .map(|live| {
4474                            post_cycles = platform.direct_reconciliation_cycles(name, Some(&live));
4475                            live.controllers
4476                        })
4477                };
4478                if let Some(controllers) = &mut observed_controllers {
4479                    controllers.sort();
4480                    controllers.dedup();
4481                }
4482                (
4483                    observed_controllers.as_ref() == Some(&expected),
4484                    format!("controllers:{observed_controllers:?}"),
4485                )
4486            }
4487            EnsureAction::Start { principal, .. } => {
4488                let live = platform.status_optional(Self::action_principal(state, principal)?)?;
4489                post_cycles = platform.direct_reconciliation_cycles(action.name(), live.as_ref());
4490                (
4491                    live.as_ref()
4492                        .is_some_and(|live| live.status == CanisterRuntimeStatus::Running),
4493                    format!("start:{:?}", live.map(|live| live.status)),
4494                )
4495            }
4496            EnsureAction::Uninstall { principal, .. } => {
4497                let live = platform.install_status_optional(Self::action_principal(state, principal)?)?;
4498                post_cycles = platform.direct_reconciliation_cycles(action.name(), live.as_ref());
4499                (
4500                    live.as_ref().is_some_and(|live| live.module_sha256.is_none()),
4501                    format!("uninstall:{:?}", live.map(|live| live.module_sha256)),
4502                )
4503            }
4504            EnsureAction::Stop { principal, .. } => {
4505                let live = platform.status_optional(Self::action_principal(state, principal)?)?;
4506                post_cycles = platform.direct_reconciliation_cycles(action.name(), live.as_ref());
4507                (
4508                    live.as_ref()
4509                        .is_some_and(|live| live.status == CanisterRuntimeStatus::Stopped),
4510                    format!("stop:{:?}", live.map(|live| live.status)),
4511                )
4512            }
4513            EnsureAction::Transfer {
4514                amount,
4515                maximum_execution_burn_cycles,
4516                name,
4517                principal,
4518                destination,
4519                ..
4520            } => {
4521                let source = platform
4522                    .status_optional(Self::action_principal(state, principal)?)?
4523                    .map(|live| live.cycles)
4524                    .ok_or_else(|| IcpEnsurePlatformError::MissingTransferBalance {
4525                        canister: name.clone(),
4526                        side: "live source",
4527                    })?;
4528                let destination = platform
4529                    .status_optional(platform.current_principal(state, destination).ok_or_else(
4530                        || IcpEnsurePlatformError::UnresolvedCreated(destination.clone()),
4531                    )?)?
4532                    .map(|live| live.cycles)
4533                    .ok_or_else(|| IcpEnsurePlatformError::MissingTransferBalance {
4534                        canister: name.clone(),
4535                        side: "live treasury",
4536                    })?;
4537                let source_pre = record.pre_cycles.ok_or_else(|| {
4538                    IcpEnsurePlatformError::MissingTransferBalance {
4539                        canister: name.clone(),
4540                        side: "retained source",
4541                    }
4542                })?;
4543                let destination_pre = record.destination_pre_cycles.ok_or_else(|| {
4544                    IcpEnsurePlatformError::MissingTransferBalance {
4545                        canister: name.clone(),
4546                        side: "retained treasury",
4547                    }
4548                })?;
4549                let reconciliation = reconcile_retirement_transfer(RetirementTransferBalances {
4550                    destination_after: destination,
4551                    destination_before: destination_pre,
4552                    maximum_execution_burn: *maximum_execution_burn_cycles,
4553                    source_after: source,
4554                    source_before: source_pre,
4555                    transfer_amount: *amount,
4556                });
4557                let (applied, source_debit, destination_credit) = match reconciliation {
4558                    Ok(RetirementTransferReconciliation::Pending) => (false, 0, 0),
4559                    Ok(RetirementTransferReconciliation::Conserved {
4560                        destination_credit,
4561                        source_debit,
4562                    }) => (true, source_debit, destination_credit),
4563                    Err(RetirementTransferInvariantError::ArithmeticOverflow) => {
4564                        return Err(IcpEnsurePlatformError::Arithmetic(
4565                            "retirement transfer debit",
4566                        ));
4567                    }
4568                    Err(RetirementTransferInvariantError::BalanceMovedInInvalidDirection) => {
4569                        return Err(IcpEnsurePlatformError::InvalidTransferBalance {
4570                            canister: name.clone(),
4571                        });
4572                    }
4573                    Err(RetirementTransferInvariantError::Unbalanced {
4574                        destination_credit,
4575                        maximum_source_debit,
4576                        source_debit,
4577                    }) => {
4578                        return Err(IcpEnsurePlatformError::UnbalancedTransfer {
4579                            canister: name.clone(),
4580                            destination_credit,
4581                            expected: *amount,
4582                            maximum_source_debit,
4583                            source_debit,
4584                        });
4585                    }
4586                };
4587                (
4588                    applied,
4589                    format!("transfer:{source_debit}:{destination_credit}"),
4590                )
4591            }
4592        };
4593        Ok(EffectObservation {
4594            provisioning_progress: None,
4595            provisioning_failure: None,
4596            applied,
4597            estate_funding_required: None,
4598            post_cycles,
4599            progress_identity,
4600            retry,
4601        })
4602            })
4603    }
4604
4605    fn apply_independent_effects(
4606        &mut self,
4607        _operation_id: &str,
4608        uploads: &[super::independent_effects::IndependentEffect<'_>],
4609        _state: &FleetEnsureStateRecord,
4610    ) -> Result<Vec<Result<EffectOutcome, Self::Error>>, Self::Error> {
4611        self.measure_observation(FleetObservationStage::IndependentSubmission, |platform| {
4612            platform.observation_snapshot.take();
4613            platform.staging_observations = None;
4614            platform.require_operator()?;
4615            Ok(
4616                super::independent_effects::apply(&platform.icp, &platform.root, uploads)?
4617                    .into_iter()
4618                    .map(|result| result.map_err(Into::into))
4619                    .collect(),
4620            )
4621        })
4622    }
4623
4624    #[expect(
4625        clippy::too_many_lines,
4626        reason = "the platform keeps every approved single-step effect in one exhaustive match"
4627    )]
4628    fn apply(
4629        &mut self,
4630        operation_id: &str,
4631        action: &EnsureAction,
4632        _record: &EffectRecord,
4633        state: &FleetEnsureStateRecord,
4634    ) -> Result<EffectOutcome, Self::Error> {
4635        self.measure_observation(FleetObservationStage::EffectSubmission, |platform| {
4636            platform.observation_snapshot.take();
4637            platform.staging_observations = None;
4638            platform.require_operator()?;
4639            match action {
4640                EnsureAction::Create {
4641                    controller_canisters,
4642                    controllers,
4643                    created_at_time,
4644                    ledger,
4645                    requested_initial_cycles,
4646                    subnet,
4647                    ..
4648                } => platform.apply_create(
4649                    CreateCanisterAuthority {
4650                        controller_canisters,
4651                        controllers,
4652                        created_at_time: *created_at_time,
4653                        ledger,
4654                        requested_initial_cycles: *requested_initial_cycles,
4655                        subnet,
4656                    },
4657                    state,
4658                ),
4659                EnsureAction::Delete {
4660                    maximum_remaining_cycles,
4661                    principal,
4662                    ..
4663                } => {
4664                    let principal = Self::action_principal(state, principal)?;
4665                    if let Some(live) = platform.status_optional(principal)? {
4666                        if live.cycles > *maximum_remaining_cycles {
4667                            return Err(IcpEnsurePlatformError::MaterialCycles {
4668                                actual: live.cycles,
4669                                canister: principal.to_string(),
4670                                maximum: *maximum_remaining_cycles,
4671                            });
4672                        }
4673                        if live.status != CanisterRuntimeStatus::Stopped {
4674                            return Err(IcpEnsurePlatformError::NotStopped {
4675                                canister: principal.to_string(),
4676                            });
4677                        }
4678                        platform
4679                            .icp
4680                            .delete_canister_without_cycle_recovery(principal)?;
4681                    }
4682                    Ok(empty_outcome())
4683                }
4684                EnsureAction::Fund {
4685                    amount,
4686                    created_at_time,
4687                    ledger,
4688                    principal,
4689                    ..
4690                } => platform.apply_fund(
4691                    *amount,
4692                    *created_at_time,
4693                    ledger,
4694                    Self::action_principal(state, principal)?,
4695                ),
4696                EnsureAction::FundEstate {
4697                    amount,
4698                    created_at_time,
4699                    ledger,
4700                    ledger_fee_cycles,
4701                    principal,
4702                    ..
4703                } => platform.apply_estate_fund(
4704                    *amount,
4705                    *created_at_time,
4706                    ledger,
4707                    Self::action_principal(state, principal)?,
4708                    *ledger_fee_cycles,
4709                ),
4710                EnsureAction::Install {
4711                    canic_init,
4712                    init_arg,
4713                    init_arg_sha256,
4714                    init_candid,
4715                    init_candid_sha256,
4716                    mode,
4717                    principal,
4718                    wasm,
4719                    wasm_sha256,
4720                    ..
4721                } => platform.apply_install(
4722                    operation_id,
4723                    &platform.protocol_principals(state),
4724                    canic_init.as_ref(),
4725                    init_arg.as_deref(),
4726                    init_arg_sha256.as_deref(),
4727                    init_candid.as_deref(),
4728                    init_candid_sha256.as_deref(),
4729                    *mode,
4730                    Self::action_principal(state, principal)?,
4731                    wasm,
4732                    wasm_sha256,
4733                ),
4734                EnsureAction::FleetProtocol { .. } => {
4735                    current_protocol::apply(&platform.icp, &platform.root, action)
4736                        .map_err(Into::into)
4737                }
4738                EnsureAction::Protocol { .. } => {
4739                    let action = resolved_protocol_action(action, state)?;
4740                    protocol::apply(
4741                        &platform.icp,
4742                        &platform.root,
4743                        operation_id,
4744                        &platform.protocol_principals(state),
4745                        &action,
4746                    )?;
4747                    Ok(EffectOutcome {
4748                        created_principal: None,
4749                        post_cycles: None,
4750                        receipt: Some(operation_id.to_string()),
4751                    })
4752                }
4753                EnsureAction::SetControllers {
4754                    controller_canisters,
4755                    controllers,
4756                    principal,
4757                    ..
4758                } => platform.apply_controllers(
4759                    Self::action_principal(state, principal)?,
4760                    &platform.resolved_controllers(state, controllers, controller_canisters)?,
4761                ),
4762                EnsureAction::Start { principal, .. } => {
4763                    platform
4764                        .icp
4765                        .start_canister(Self::action_principal(state, principal)?)?;
4766                    Ok(empty_outcome())
4767                }
4768                EnsureAction::Uninstall { principal, .. } => {
4769                    let principal = parse_principal(
4770                        "bootstrap uninstall",
4771                        Self::action_principal(state, principal)?,
4772                    )?;
4773                    platform
4774                        .icp
4775                        .uninstall_canister(principal)
4776                        .map_err(|error| {
4777                            IcpEnsurePlatformError::BootstrapUninstall(Box::new(error))
4778                        })?;
4779                    Ok(empty_outcome())
4780                }
4781                EnsureAction::Stop { principal, .. } => {
4782                    platform
4783                        .icp
4784                        .stop_canister(Self::action_principal(state, principal)?)?;
4785                    Ok(empty_outcome())
4786                }
4787                EnsureAction::Transfer {
4788                    amount,
4789                    candid,
4790                    candid_sha256,
4791                    destination,
4792                    method,
4793                    principal,
4794                    ..
4795                } => platform.apply_transfer(
4796                    *amount,
4797                    candid,
4798                    candid_sha256,
4799                    platform
4800                        .current_principal(state, destination)
4801                        .ok_or_else(|| {
4802                            IcpEnsurePlatformError::UnresolvedCreated(destination.clone())
4803                        })?,
4804                    method,
4805                    operation_id,
4806                    Self::action_principal(state, principal)?,
4807                ),
4808            }
4809        })
4810    }
4811
4812    fn action_cycles(
4813        &mut self,
4814        action: &EnsureAction,
4815        state: &FleetEnsureStateRecord,
4816    ) -> Result<Option<u128>, Self::Error> {
4817        if matches!(action, EnsureAction::FundEstate { .. }) {
4818            return self
4819                .icp
4820                .identity_cycles_balance()
4821                .map(Some)
4822                .map_err(|error| IcpEnsurePlatformError::LedgerTransfer(error.to_string()));
4823        }
4824        if let EnsureAction::Fund {
4825            pool_funding: Some(authority),
4826            name,
4827            principal,
4828            ..
4829        } = action
4830        {
4831            let principal = Self::action_principal(state, principal)?;
4832            let module = self.pool_funding_module(name, authority, principal)?;
4833            return self
4834                .inspect_pool_balance(name, &authority.root, principal, module)
4835                .map(Some);
4836        }
4837        let (name, principal) = match action {
4838            EnsureAction::Create { .. } => return Ok(None),
4839            EnsureAction::Delete {
4840                name, principal, ..
4841            }
4842            | EnsureAction::FleetProtocol {
4843                name, principal, ..
4844            }
4845            | EnsureAction::Fund {
4846                name, principal, ..
4847            }
4848            | EnsureAction::FundEstate {
4849                name, principal, ..
4850            }
4851            | EnsureAction::Install {
4852                name, principal, ..
4853            }
4854            | EnsureAction::Protocol {
4855                name, principal, ..
4856            }
4857            | EnsureAction::SetControllers {
4858                name, principal, ..
4859            }
4860            | EnsureAction::Start {
4861                name, principal, ..
4862            }
4863            | EnsureAction::Stop {
4864                name, principal, ..
4865            }
4866            | EnsureAction::Uninstall { name, principal }
4867            | EnsureAction::Transfer {
4868                name, principal, ..
4869            } => (name, Self::action_principal(state, principal)?),
4870        };
4871        let observed = self
4872            .desired
4873            .canisters
4874            .iter()
4875            .find(|configured| configured.name == *name)
4876            .map_or_else(
4877                || self.status_optional(principal),
4878                |configured| self.observe_configured_canister(configured, principal, state),
4879            )?;
4880        if let Some(review) = &self.bootstrap_registration_recovery {
4881            super::infrastructure_bootstrap::registration_recovery::verify_funding_target(
4882                review,
4883                action,
4884                observed.as_ref(),
4885            )?;
4886        }
4887        Ok(observed.map(|live| live.cycles))
4888    }
4889
4890    fn action_destination_cycles(
4891        &mut self,
4892        action: &EnsureAction,
4893        state: &FleetEnsureStateRecord,
4894    ) -> Result<Option<u128>, Self::Error> {
4895        if let EnsureAction::FundEstate { principal, .. } = action {
4896            let owner = Self::action_principal(state, principal)?;
4897            return self.cycles_ledger_balance(owner).map(Some);
4898        }
4899        let EnsureAction::Transfer { destination, .. } = action else {
4900            return Ok(None);
4901        };
4902        let destination = self
4903            .current_principal(state, destination)
4904            .ok_or_else(|| IcpEnsurePlatformError::UnresolvedCreated(destination.clone()))?;
4905        Ok(self.status_optional(destination)?.map(|live| live.cycles))
4906    }
4907
4908    fn action_canister_version(
4909        &mut self,
4910        action: &EnsureAction,
4911        state: &FleetEnsureStateRecord,
4912    ) -> Result<Option<u64>, Self::Error> {
4913        let EnsureAction::Install { principal, .. } = action else {
4914            return Ok(None);
4915        };
4916        let principal = Self::action_principal(state, principal)?;
4917        let version = self
4918            .install_status_optional(principal)?
4919            .and_then(|live| live.canister_version)
4920            .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
4921                canister: principal.to_string(),
4922                field: "canister_version",
4923            })?;
4924        Ok(Some(version))
4925    }
4926}
4927
4928fn exact_install_canister_status(
4929    icp: &IcpCli,
4930    canister: &str,
4931    projected_canister_version: Option<u64>,
4932    projected_module_sha256: Option<String>,
4933) -> Result<ExactInstallCanisterStatus, IcpEnsurePlatformError> {
4934    exact_install_canister_status_with(
4935        canister,
4936        projected_canister_version,
4937        projected_module_sha256,
4938        |canister_id| {
4939            icp.management_canister_status_candid::<_, ManagementCanisterStatusResponse>(
4940                canister_id,
4941                &ManagementCanisterStatusRequest { canister_id },
4942            )
4943        },
4944    )
4945}
4946
4947fn exact_install_canister_status_with(
4948    canister: &str,
4949    projected_canister_version: Option<u64>,
4950    projected_module_sha256: Option<String>,
4951    management_status: impl FnOnce(
4952        Principal,
4953    )
4954        -> Result<ManagementCanisterStatusResponse, IcpManagementCallError>,
4955) -> Result<ExactInstallCanisterStatus, IcpEnsurePlatformError> {
4956    if let Some(canister_version) = projected_canister_version {
4957        return Ok(ExactInstallCanisterStatus {
4958            canister_version,
4959            module_sha256: projected_module_sha256,
4960        });
4961    }
4962    let canister_id = parse_principal("install target", canister)?;
4963    let response = management_status(canister_id).map_err(|source| {
4964        IcpEnsurePlatformError::InstallVersionProofUnavailable {
4965            canister: canister.to_string(),
4966            source: Box::new(source),
4967        }
4968    })?;
4969    Ok(ExactInstallCanisterStatus {
4970        canister_version: response.version,
4971        module_sha256: response
4972            .module_hash
4973            .map(|hash| canic_core::cdk::utils::hash::hex_bytes(&hash)),
4974    })
4975}
4976
4977fn exact_retained_root_owned_topology<'a>(
4978    state: &'a FleetEnsureStateRecord,
4979    name: &str,
4980    kind: DesiredCanisterKind,
4981    parent: &str,
4982    principal: &str,
4983    root: &str,
4984) -> Option<&'a crate::fleet_ensure::model::FleetEnsureTopologyRecord> {
4985    let child_identity_matches = retained_state_principal_matches(state, name, principal);
4986    let root_identity_matches = retained_state_principal_matches(state, parent, root);
4987    let topology = state
4988        .topology
4989        .get(name)
4990        .filter(|topology| topology.kind == kind && topology.parent.as_deref() == Some(parent));
4991    (child_identity_matches && root_identity_matches)
4992        .then_some(topology)
4993        .flatten()
4994}
4995
4996fn retained_state_principal_matches(
4997    state: &FleetEnsureStateRecord,
4998    name: &str,
4999    principal: &str,
5000) -> bool {
5001    let pending = state.pending_principals.get(name).map(String::as_str);
5002    let terminal = state.principals.get(name).map(String::as_str);
5003    let retained = pending.is_some() || terminal.is_some();
5004    retained
5005        && pending.is_none_or(|retained| retained == principal)
5006        && terminal.is_none_or(|retained| retained == principal)
5007}
5008
5009fn completed_reinstall_continuity(
5010    pre_canister_version: u64,
5011    principal_matches: bool,
5012    topology_matches: bool,
5013    live_module_sha256: Option<&str>,
5014    desired_module_sha256: &str,
5015    live_canister_version: Option<u64>,
5016) -> Result<(), &'static str> {
5017    if !principal_matches {
5018        return Err("Principal");
5019    }
5020    if !topology_matches {
5021        return Err("Root/parent/kind topology");
5022    }
5023    if live_module_sha256 != Some(desired_module_sha256) {
5024        return Err("module SHA-256");
5025    }
5026    if live_canister_version.is_some_and(|version| version <= pre_canister_version) {
5027        return Err("canister version");
5028    }
5029    Ok(())
5030}
5031
5032fn recoverable_root_status_error(error: &CanisterProtocolError) -> bool {
5033    error.is_rejected_with(canic_core::diagnostics::codes::STATE_CONFLICT)
5034        || error.is_rejected_with(canic_core::diagnostics::codes::STATE_UNAVAILABLE)
5035}
5036
5037fn recoverable_current_protocol_error(error: &current_protocol::CurrentProtocolError) -> bool {
5038    matches!(
5039        error,
5040        current_protocol::CurrentProtocolError::Transport(source)
5041            if recoverable_root_status_error(source)
5042    )
5043}
5044
5045pub fn install_effect_applied(
5046    mode: InstallMode,
5047    expected_hash: &str,
5048    live_hash: Option<&str>,
5049    pre_canister_version: Option<u64>,
5050    live_canister_version: Option<u64>,
5051) -> bool {
5052    if live_hash != Some(expected_hash) {
5053        return false;
5054    }
5055    match mode {
5056        InstallMode::Install => true,
5057        InstallMode::Reinstall => pre_canister_version
5058            .zip(live_canister_version)
5059            .is_some_and(|(before, after)| after > before),
5060    }
5061}
5062
5063/// Exact retained and live evidence for one Cycles Ledger withdrawal.
5064#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5065pub struct NativeFundingObservation {
5066    pub amount: u128,
5067    pub expected_post_cycles: u128,
5068    pub funding_deficit_cycles: u128,
5069    pub funding_margin_cycles: u128,
5070    pub live_cycles: Option<u128>,
5071    pub pre_cycles: Option<u128>,
5072}
5073
5074/// Require the reviewed floor after all planning-to-completion burn within its margin.
5075pub const fn native_funding_applied(observation: NativeFundingObservation) -> bool {
5076    let Some(pre_cycles) = observation.pre_cycles else {
5077        return false;
5078    };
5079    let Some(live_cycles) = observation.live_cycles else {
5080        return false;
5081    };
5082    let Some(_) = pre_cycles.checked_add(observation.amount) else {
5083        return false;
5084    };
5085    let Some(reviewed_pre_cycles) = observation
5086        .expected_post_cycles
5087        .checked_sub(observation.amount)
5088    else {
5089        return false;
5090    };
5091    let Some(minimum_live_cycles) = observation
5092        .expected_post_cycles
5093        .checked_sub(observation.funding_margin_cycles)
5094    else {
5095        return false;
5096    };
5097    let Some(minimum_from_deficit) =
5098        reviewed_pre_cycles.checked_add(observation.funding_deficit_cycles)
5099    else {
5100        return false;
5101    };
5102    let reviewed_amount_matches =
5103        observation.funding_deficit_cycles > 0 && minimum_live_cycles == minimum_from_deficit;
5104    // A donation cannot stand in for the separate Ledger receipt, but it may
5105    // increase either native balance observation without invalidating that receipt.
5106    let observation_is_bounded = live_cycles >= minimum_live_cycles;
5107    reviewed_amount_matches && observation_is_bounded
5108}
5109
5110fn native_funding_completion(
5111    observation: NativeFundingObservation,
5112    has_receipt: bool,
5113) -> Result<bool, IcpEnsurePlatformError> {
5114    if !has_receipt || observation.live_cycles.is_none() {
5115        return Ok(false);
5116    }
5117    if !native_funding_applied(observation) {
5118        return Err(IcpEnsurePlatformError::NativeFundingBalanceDrift {
5119            observation: Box::new(observation),
5120        });
5121    }
5122    Ok(true)
5123}
5124
5125/// Exact retained and live evidence for one operator-to-Root Ledger transfer.
5126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5127pub struct EstateFundingObservation {
5128    pub amount: u128,
5129    pub destination_after: u128,
5130    pub destination_before: Option<u128>,
5131    pub expected_destination_after: u128,
5132    pub ledger_fee_cycles: u128,
5133    pub source_after: u128,
5134    pub source_before: Option<u128>,
5135}
5136
5137/// Require both sides of an estate-funding transfer to match its reviewed debit.
5138#[must_use]
5139pub const fn estate_funding_applied(observation: EstateFundingObservation) -> bool {
5140    let Some(source_before) = observation.source_before else {
5141        return false;
5142    };
5143    let Some(destination_before) = observation.destination_before else {
5144        return false;
5145    };
5146    let Some(expected_source_debit) = observation
5147        .amount
5148        .checked_add(observation.ledger_fee_cycles)
5149    else {
5150        return false;
5151    };
5152    let Some(source_debit) = source_before.checked_sub(observation.source_after) else {
5153        return false;
5154    };
5155    let Some(destination_credit) = observation
5156        .destination_after
5157        .checked_sub(destination_before)
5158    else {
5159        return false;
5160    };
5161    source_debit == expected_source_debit
5162        && destination_credit == observation.amount
5163        && observation.destination_after == observation.expected_destination_after
5164}
5165
5166/// Creation and Ready funding require empty modules; reviewed reset funding accepts installed assets.
5167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5168enum InspectedModule {
5169    Any,
5170    Empty,
5171}
5172
5173fn pool_funding_inspected_module(
5174    canister: &str,
5175    reviewed: EstatePoolAssetLifecycle,
5176    observed: &CanisterPoolAssetStatus,
5177) -> Result<InspectedModule, IcpEnsurePlatformError> {
5178    match (reviewed, observed) {
5179        (EstatePoolAssetLifecycle::Ready, CanisterPoolAssetStatus::Ready) => {
5180            Ok(InspectedModule::Empty)
5181        }
5182        (EstatePoolAssetLifecycle::PendingReset, CanisterPoolAssetStatus::PendingReset)
5183        | (EstatePoolAssetLifecycle::Failed, CanisterPoolAssetStatus::Failed { .. }) => {
5184            Ok(InspectedModule::Any)
5185        }
5186        _ => Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
5187            canister: canister.to_string(),
5188            field: "reviewed pool lifecycle",
5189        }),
5190    }
5191}
5192
5193fn validate_pending_fresh_pool_inspection(
5194    canister: &str,
5195    root: &str,
5196    operator: &str,
5197    controllers: &[Principal],
5198    module_hash: Option<&[u8]>,
5199    cycles: &Nat,
5200    recovery_controllers: &[Principal],
5201) -> Result<u128, IcpEnsurePlatformError> {
5202    let mut actual = controllers
5203        .iter()
5204        .map(Principal::to_text)
5205        .collect::<Vec<_>>();
5206    actual.sort();
5207    let mut ready = recovery_controllers
5208        .iter()
5209        .map(Principal::to_text)
5210        .collect::<Vec<_>>();
5211    ready.push(root.to_string());
5212    ready.sort();
5213    let mut temporary = ready.clone();
5214    temporary.push(operator.to_string());
5215    temporary.sort();
5216    if actual != ready && actual != temporary {
5217        return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
5218            canister: canister.to_string(),
5219            field: "reviewed fresh pool controllers",
5220        });
5221    }
5222    // Controller finalization is itself journalled. Until it completes, the exact
5223    // temporary operator remains a controller of this issued fresh Create only.
5224    validate_inspected_cycles(canister, InspectedModule::Empty, module_hash, cycles)
5225}
5226
5227fn validate_root_controlled_inspection(
5228    canister: &str,
5229    module: InspectedModule,
5230    root: &str,
5231    controllers: &[Principal],
5232    module_hash: Option<&[u8]>,
5233    cycles: &Nat,
5234    recovery_controllers: &[Principal],
5235) -> Result<u128, IcpEnsurePlatformError> {
5236    let mut expected = recovery_controllers.to_vec();
5237    expected.push(Principal::from_text(root).map_err(|_| {
5238        IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
5239            canister: canister.to_string(),
5240            field: "Root Principal",
5241        }
5242    })?);
5243    expected.sort_unstable();
5244    let mut actual = controllers.to_vec();
5245    actual.sort_unstable();
5246    if actual != expected {
5247        return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
5248            canister: canister.to_string(),
5249            field: "exact Root controllers",
5250        });
5251    }
5252    validate_inspected_cycles(canister, module, module_hash, cycles)
5253}
5254
5255fn validate_inspected_cycles(
5256    canister: &str,
5257    module: InspectedModule,
5258    module_hash: Option<&[u8]>,
5259    cycles: &Nat,
5260) -> Result<u128, IcpEnsurePlatformError> {
5261    if matches!(module, InspectedModule::Empty) && module_hash.is_some() {
5262        return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
5263            canister: canister.to_string(),
5264            field: "module-free pool asset",
5265        });
5266    }
5267    u128::try_from(cycles.0.clone()).map_err(|_| IcpEnsurePlatformError::InvalidStatusCycles {
5268        canister: canister.to_string(),
5269        value: cycles.to_string(),
5270    })
5271}
5272
5273fn created_canister_outcome(
5274    canister_id: Principal,
5275    receipt: String,
5276    requested_initial_cycles: u128,
5277) -> EffectOutcome {
5278    EffectOutcome {
5279        created_principal: Some(canister_id.to_text()),
5280        post_cycles: Some(requested_initial_cycles),
5281        receipt: Some(receipt),
5282    }
5283}
5284
5285fn create_canister_response_outcome(
5286    response: Result<CreateCanisterSuccess, CreateCanisterError>,
5287    requested_initial_cycles: u128,
5288) -> Result<EffectOutcome, IcpEnsurePlatformError> {
5289    match response {
5290        Ok(success) => Ok(created_canister_outcome(
5291            success.canister_id,
5292            success.block_id.to_string(),
5293            requested_initial_cycles,
5294        )),
5295        Err(CreateCanisterError::Duplicate {
5296            duplicate_of,
5297            canister_id: Some(canister_id),
5298        }) => Ok(created_canister_outcome(
5299            canister_id,
5300            duplicate_of.to_string(),
5301            requested_initial_cycles,
5302        )),
5303        Err(CreateCanisterError::Duplicate {
5304            canister_id: None, ..
5305        }) => Err(IcpEnsurePlatformError::LedgerCreatePending),
5306        Err(error) => Err(IcpEnsurePlatformError::LedgerCreate(render_create_error(
5307            error,
5308        ))),
5309    }
5310}
5311
5312fn ledger_fee_cycles(value: Nat) -> Result<u128, IcpEnsurePlatformError> {
5313    let rendered = value.to_string();
5314    u128::try_from(value.0).map_err(|_| IcpEnsurePlatformError::InvalidLedgerFee(rendered))
5315}
5316
5317const fn empty_outcome() -> EffectOutcome {
5318    EffectOutcome {
5319        created_principal: None,
5320        post_cycles: None,
5321        receipt: None,
5322    }
5323}
5324
5325fn parse_status_cycles(value: &str) -> Option<u128> {
5326    value.replace('_', "").trim().parse().ok()
5327}
5328
5329fn normalize_hash(value: &str) -> String {
5330    value
5331        .strip_prefix("0x")
5332        .unwrap_or(value)
5333        .to_ascii_lowercase()
5334}
5335
5336fn parse_principal(field: &'static str, value: &str) -> Result<Principal, IcpEnsurePlatformError> {
5337    Principal::from_text(value).map_err(|_| IcpEnsurePlatformError::InvalidPrincipal {
5338        field,
5339        value: value.to_string(),
5340    })
5341}
5342
5343fn reject_public_status_projection(
5344    report: &IcpCanisterStatusReport,
5345    principal: &str,
5346) -> Result<(), IcpEnsurePlatformError> {
5347    if report.status.is_none()
5348        && report.settings.is_none()
5349        && report.cycles.is_none()
5350        && report.public_controllers.is_some()
5351    {
5352        return Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable {
5353            canister: principal.to_string(),
5354        });
5355    }
5356    Ok(())
5357}
5358
5359fn resolve_path(root: &Path, path: &str) -> PathBuf {
5360    let path = Path::new(path);
5361    if path.is_absolute() {
5362        path.to_path_buf()
5363    } else {
5364        root.join(path)
5365    }
5366}
5367
5368fn verify_artifact(
5369    path: &Path,
5370    expected: &str,
5371    kind: &'static str,
5372) -> Result<(), IcpEnsurePlatformError> {
5373    let bytes = std::fs::read(path)
5374        .map_err(|_| IcpEnsurePlatformError::ArtifactUnavailable(path.to_path_buf()))?;
5375    let actual = canic_core::cdk::utils::hash::sha256_hex(&bytes);
5376    if actual != expected {
5377        return Err(IcpEnsurePlatformError::ArtifactDigestMismatch {
5378            actual,
5379            expected: expected.to_string(),
5380            kind,
5381        });
5382    }
5383    Ok(())
5384}
5385
5386fn artifact_hash(path: &Path) -> Result<String, IcpEnsurePlatformError> {
5387    let bytes = std::fs::read(path)
5388        .map_err(|_| IcpEnsurePlatformError::ArtifactUnavailable(path.to_path_buf()))?;
5389    Ok(canic_core::cdk::utils::hash::sha256_hex(&bytes))
5390}
5391
5392fn resolved_protocol_action(
5393    action: &EnsureAction,
5394    state: &FleetEnsureStateRecord,
5395) -> Result<EnsureAction, IcpEnsurePlatformError> {
5396    let mut resolved = action.clone();
5397    if let EnsureAction::Protocol { principal, .. } = &mut resolved {
5398        *principal = IcpEnsurePlatform::action_principal(state, principal)?.to_string();
5399        return Ok(resolved);
5400    }
5401    Err(IcpEnsurePlatformError::Protocol(
5402        protocol::ProtocolEffectError::WrongAction,
5403    ))
5404}
5405
5406fn render_create_error(error: CreateCanisterError) -> String {
5407    match error {
5408        CreateCanisterError::CreatedInFuture { ledger_time } => {
5409            format!("created in future of ledger time {ledger_time}")
5410        }
5411        CreateCanisterError::Duplicate { duplicate_of, .. } => {
5412            format!("duplicate block {duplicate_of}")
5413        }
5414        CreateCanisterError::FailedToCreate {
5415            error,
5416            fee_block,
5417            refund_block,
5418        } => format!("failed: {error}; fee_block={fee_block:?}; refund_block={refund_block:?}"),
5419        CreateCanisterError::GenericError {
5420            error_code,
5421            message,
5422        } => format!("error {error_code}: {message}"),
5423        CreateCanisterError::InsufficientFunds { balance } => {
5424            format!("insufficient funds: balance={balance}")
5425        }
5426        CreateCanisterError::TemporarilyUnavailable => "temporarily unavailable".to_string(),
5427        CreateCanisterError::TooOld => "request is too old".to_string(),
5428    }
5429}
5430
5431fn render_withdraw_error(error: WithdrawError) -> String {
5432    match error {
5433        WithdrawError::BadFee { expected_fee } => format!("bad fee; expected {expected_fee}"),
5434        WithdrawError::CreatedInFuture { ledger_time } => {
5435            format!("created in future of ledger time {ledger_time}")
5436        }
5437        WithdrawError::Duplicate { duplicate_of } => format!("duplicate block {duplicate_of}"),
5438        WithdrawError::FailedToWithdraw {
5439            fee_block,
5440            rejection_reason,
5441            ..
5442        } => format!("withdraw failed: {rejection_reason}; fee_block={fee_block:?}"),
5443        WithdrawError::GenericError {
5444            error_code,
5445            message,
5446        } => format!("error {error_code}: {message}"),
5447        WithdrawError::InsufficientFunds { balance } => {
5448            format!("insufficient funds: balance={balance}")
5449        }
5450        WithdrawError::InvalidReceiver { receiver } => format!("invalid receiver {receiver}"),
5451        WithdrawError::TemporarilyUnavailable => "temporarily unavailable".to_string(),
5452        WithdrawError::TooOld => "request is too old".to_string(),
5453    }
5454}
5455
5456fn render_ledger_transfer_error(error: CyclesLedgerTransferError) -> String {
5457    match error {
5458        CyclesLedgerTransferError::BadBurn { min_burn_amount } => {
5459            format!("bad burn; minimum {min_burn_amount}")
5460        }
5461        CyclesLedgerTransferError::BadFee { expected_fee } => {
5462            format!("bad fee; expected {expected_fee}")
5463        }
5464        CyclesLedgerTransferError::CreatedInFuture { ledger_time } => {
5465            format!("created in future of ledger time {ledger_time}")
5466        }
5467        CyclesLedgerTransferError::Duplicate { duplicate_of } => {
5468            format!("duplicate block {duplicate_of}")
5469        }
5470        CyclesLedgerTransferError::GenericError {
5471            error_code,
5472            message,
5473        } => format!("error {error_code}: {message}"),
5474        CyclesLedgerTransferError::InsufficientFunds { balance } => {
5475            format!("insufficient funds: balance={balance}")
5476        }
5477        CyclesLedgerTransferError::TemporarilyUnavailable => "temporarily unavailable".to_string(),
5478        CyclesLedgerTransferError::TooOld => "request is too old".to_string(),
5479    }
5480}
5481
5482#[cfg(test)]
5483mod tests {
5484    use super::*;
5485    use canic_core::dto::pool::CanisterPoolAssetStatus;
5486    #[cfg(unix)]
5487    use std::sync::{Arc, Mutex};
5488
5489    #[test]
5490    fn identity_binding_keeps_operator_fence_before_effects() {
5491        let mut fixture = ProtocolOwnersFixture::new();
5492        fixture.platform = fixture.platform.with_identity(Some("reviewed"));
5493        std::fs::write(
5494            fixture.root.join("icp"),
5495            crate::test_support::tool_script(
5496                r#"#!/bin/sh
5497if [ "$1" = --version ]; then echo 'icp @ICP_VERSION@'; exit 0; fi
5498while [ "$1" = --project-root-override ] || [ "$1" = --identity-password-file ]; do shift 2; done
5499if [ "$1 $2" = 'identity default' ]; then touch default-accessed; exit 91; fi
5500if [ "$1 $2" = 'identity principal' ]; then cat principal; exit 0; fi
5501echo effect >> effects
5502"#,
5503            ),
5504        )
5505        .unwrap();
5506        std::fs::write(fixture.root.join("principal"), "operator").unwrap();
5507        fixture.platform.require_operator().unwrap();
5508        assert!(!fixture.root.join("default-accessed").exists());
5509        std::fs::write(fixture.root.join("principal"), "changed-principal").unwrap();
5510        let action = EnsureAction::Start {
5511            name: "root".into(),
5512            principal: "root".into(),
5513        };
5514        let receipt = EffectRecord {
5515            maintenance_attempts: 0,
5516            publication_attempts: 0,
5517            action_sha256: crate::fleet_ensure::ops::action_sha256(&action),
5518            created_principal: None,
5519            destination_post_cycles: None,
5520            destination_pre_cycles: None,
5521            post_cycles: None,
5522            pre_cycles: None,
5523            pre_canister_version: None,
5524            progress_identity: None,
5525            receipt: None,
5526            state: crate::fleet_ensure::model::EffectState::Issued,
5527        };
5528        assert!(matches!(
5529            fixture
5530                .platform
5531                .apply("operation", &action, &receipt, &fixture.state),
5532            Err(IcpEnsurePlatformError::OperatorMismatch { .. })
5533        ));
5534        assert!(!fixture.root.join("effects").exists());
5535        std::fs::remove_dir_all(fixture.root).unwrap();
5536    }
5537
5538    #[test]
5539    fn inactive_source_inventory_rejects_spare_capacity_pending_work_and_omissions() {
5540        let root = crate::fleet_ensure::model::RootActivationResetRecord {
5541            root: "root".to_string(),
5542            activation_operation_id: [1; 32],
5543            inventory_hash: [2; 32],
5544            provisioning_receipt_hash: [3; 32],
5545            component_count: 1,
5546            managed_descendants: 2,
5547        };
5548        let page = inactive_source_pool_page();
5549        assert!(inactive_source_pool_has_full_inventory(&page, &root));
5550        let mut complete = EstatePoolInventoryAccumulator::default();
5551        assert_eq!(
5552            complete.observe_page("root", page.clone()).expect("page"),
5553            None
5554        );
5555        assert_eq!(complete.finish("root").expect("complete").assets.len(), 4);
5556
5557        let mut spare = page.clone();
5558        spare.config.maximum_size += 1;
5559        let mut insufficient_reserve = page.clone();
5560        insufficient_reserve.config.minimum_size = 2;
5561        let mut pending = page.clone();
5562        pending.pending_reset = 1;
5563        let mut wrong_workload = page.clone();
5564        wrong_workload.workload -= 1;
5565        wrong_workload.ready += 1;
5566        let mut overflow = page.clone();
5567        overflow.workload = u32::MAX;
5568        for changed in [
5569            spare,
5570            insufficient_reserve,
5571            pending,
5572            wrong_workload,
5573            overflow,
5574        ] {
5575            assert!(!inactive_source_pool_has_full_inventory(&changed, &root));
5576        }
5577
5578        let mut omitted = page;
5579        omitted.entries.pop();
5580        let mut inventory = EstatePoolInventoryAccumulator::default();
5581        inventory
5582            .observe_page("root", omitted)
5583            .expect("bounded page");
5584        assert!(matches!(
5585            inventory.finish("root"),
5586            Err(IcpEnsurePlatformError::CurrentProtocol(
5587                current_protocol::CurrentProtocolError::Configuration(_)
5588            ))
5589        ));
5590    }
5591
5592    fn inactive_source_pool_page() -> CanisterPoolResponse {
5593        let claim = canic_core::dto::pool::CanisterPoolClaim {
5594            component: canic_core::ids::ComponentInstanceId::from_generated_bytes([4; 32]),
5595            operation_id: [5; 32],
5596        };
5597        let entries = (0..5)
5598            .map(|index| CanisterPoolAsset {
5599                canister_id: Principal::from_slice(&[index]),
5600                creation_receipt: None,
5601                cycles: Cycles::new(100),
5602                origin: if index == 0 {
5603                    CanisterPoolAssetOrigin::InfrastructureStore
5604                } else {
5605                    CanisterPoolAssetOrigin::Imported
5606                },
5607                status: match index {
5608                    0 => CanisterPoolAssetStatus::Store,
5609                    1 => CanisterPoolAssetStatus::Ready,
5610                    _ => CanisterPoolAssetStatus::Workload {
5611                        claim: claim.clone(),
5612                    },
5613                },
5614                added_at_ns: 1,
5615                updated_at_ns: 1,
5616            })
5617            .collect();
5618        CanisterPoolResponse {
5619            config: canic_core::ids::FleetSubnetCanisterPoolConfig {
5620                minimum_size: 1,
5621                maximum_size: 4,
5622                canister_cycles: Cycles::new(100),
5623                creation_execution_margin: Cycles::new(1),
5624            },
5625            tracked: 5,
5626            store: 1,
5627            store_deletion_pending: 0,
5628            pooled: 1,
5629            workload: 3,
5630            surplus: 0,
5631            ready: 1,
5632            pending_reset: 0,
5633            claimed: 0,
5634            recycling: 0,
5635            handing_off: 0,
5636            failed: 0,
5637            completed_handoffs: 0,
5638            pending_creation: None,
5639            pending_handoff: None,
5640            entries,
5641            next_start_after: None,
5642        }
5643    }
5644
5645    #[cfg(unix)]
5646    #[test]
5647    fn pool_inspection_preflight_stops_before_update_and_requeries_on_retry() {
5648        use crate::icp::IcpRequestKind;
5649        use std::sync::{Arc, Mutex};
5650        let mut fixture = PoolInspectionFixture::new();
5651        let timings = Arc::new(Mutex::new(Vec::new()));
5652        let sink = Arc::clone(&timings);
5653        fixture.owners.platform.icp = fixture
5654            .owners
5655            .platform
5656            .icp
5657            .clone()
5658            .with_timing_handler(move |event| sink.lock().unwrap().push(event));
5659        let root = fixture.root_id;
5660        let target = fixture.target;
5661        let path = fixture.owners.root.clone();
5662        fixture.inspection_hook(&format!(
5663            "touch '{}'",
5664            path.join("inspection-attempted").display()
5665        ));
5666        PoolInspectionFixture::reserve_response(&path, root, target, 99);
5667        fixture
5668            .owners
5669            .platform
5670            .with_observation_snapshot(|platform| {
5671                let error = platform
5672                    .inspect_pool_balance(
5673                        "pool",
5674                        &root.to_text(),
5675                        &target.to_text(),
5676                        InspectedModule::Any,
5677                    )
5678                    .unwrap_err();
5679                let IcpEnsurePlatformError::CurrentProtocol(
5680                    current_protocol::CurrentProtocolError::Transport(
5681                        crate::CanisterProtocolError::InspectionPreflightReserve(evidence),
5682                    ),
5683                ) = error
5684                else {
5685                    panic!("preflight reserve shortfall");
5686                };
5687                assert_eq!(evidence.available_liquid_cycles, 99);
5688                assert!(!path.join("inspection-attempted").exists());
5689                PoolInspectionFixture::reserve_response(&path, root, target, 100);
5690                assert_eq!(
5691                    platform.inspect_pool_balance(
5692                        "pool",
5693                        &root.to_text(),
5694                        &target.to_text(),
5695                        InspectedModule::Any,
5696                    )?,
5697                    1000
5698                );
5699                assert!(path.join("inspection-attempted").exists());
5700                Ok(())
5701            })
5702            .unwrap();
5703        let timings = timings.lock().unwrap();
5704        let inspections = timings
5705            .iter()
5706            .filter(|event| {
5707                event.kind == IcpRequestKind::CanisterInspection && event.succeeded.is_some()
5708            })
5709            .collect::<Vec<_>>();
5710        assert_eq!(
5711            inspections
5712                .iter()
5713                .map(|event| event.succeeded)
5714                .collect::<Vec<_>>(),
5715            [Some(false), Some(true)]
5716        );
5717        for inspection in inspections {
5718            assert_eq!(inspection.subject, Some(target));
5719            assert_eq!(inspection.target.as_deref(), Some(root.to_text().as_str()));
5720            let children = timings
5721                .iter()
5722                .filter(|event| event.parent_request_id == Some(inspection.request_id))
5723                .collect::<Vec<_>>();
5724            assert!(children.iter().all(|event| event.subject == Some(target)));
5725            assert!(
5726                children
5727                    .iter()
5728                    .any(|event| event.method.as_deref()
5729                        == Some(canic_protocol::CANIC_OBSERVABILITY))
5730            );
5731            assert_eq!(
5732                children.iter().any(
5733                    |event| event.method.as_deref() == Some(canic_protocol::CANIC_ROOT_COMMAND)
5734                ),
5735                inspection.succeeded == Some(true)
5736            );
5737        }
5738        drop(timings);
5739        std::fs::remove_dir_all(path).unwrap();
5740    }
5741
5742    #[cfg(unix)]
5743    #[test]
5744    fn pool_inspection_retains_protected_reserve_evidence() {
5745        let mut fixture = PoolInspectionFixture::new();
5746        let root = fixture.root_id;
5747        let target = fixture.target;
5748        let path = fixture.owners.root.clone();
5749        let evidence = canic_core::dto::canister::CanisterInspectionReserveResponse {
5750            caller: root,
5751            canister_id: target,
5752            native_cycles: 1000,
5753            available_liquid_cycles: 50,
5754            required_liquid_cycles: 100,
5755        };
5756        let bytes = candid::encode_one(Ok::<_, canic_core::dto::error::Error>(
5757            RootInspectionResponse::InspectionReserveRequired(evidence.clone()),
5758        ))
5759        .unwrap();
5760        std::fs::write(
5761            path.join("inspection.json"),
5762            serde_json::json!({"response_bytes": hex_bytes(bytes)}).to_string(),
5763        )
5764        .unwrap();
5765        fixture
5766            .owners
5767            .platform
5768            .with_observation_snapshot(|platform| {
5769                let error = platform
5770                    .inspect_pool_balance(
5771                        "pool",
5772                        &root.to_text(),
5773                        &target.to_text(),
5774                        InspectedModule::Any,
5775                    )
5776                    .unwrap_err();
5777                let IcpEnsurePlatformError::CurrentProtocol(
5778                    current_protocol::CurrentProtocolError::Transport(
5779                        crate::CanisterProtocolError::InspectionReserve(actual),
5780                    ),
5781                ) = error
5782                else {
5783                    panic!("typed reserve failure");
5784                };
5785                assert_eq!(*actual, evidence);
5786                PoolInspectionFixture::response(&path, root, 1000);
5787                assert_eq!(
5788                    platform.inspect_pool_balance(
5789                        "pool",
5790                        &root.to_text(),
5791                        &target.to_text(),
5792                        InspectedModule::Any
5793                    )?,
5794                    1000
5795                );
5796                Ok(())
5797            })
5798            .unwrap();
5799        std::fs::remove_dir_all(path).unwrap();
5800    }
5801
5802    #[test]
5803    fn pool_funding_binds_reset_lifecycle_without_weakening_ready_inspection() {
5804        let failed = CanisterPoolAssetStatus::Failed {
5805            reason: "reset stopped".to_string(),
5806        };
5807        for (lifecycle, status, expected) in [
5808            (
5809                EstatePoolAssetLifecycle::Ready,
5810                CanisterPoolAssetStatus::Ready,
5811                InspectedModule::Empty,
5812            ),
5813            (
5814                EstatePoolAssetLifecycle::PendingReset,
5815                CanisterPoolAssetStatus::PendingReset,
5816                InspectedModule::Any,
5817            ),
5818            (
5819                EstatePoolAssetLifecycle::Failed,
5820                failed,
5821                InspectedModule::Any,
5822            ),
5823        ] {
5824            let module = pool_funding_inspected_module("pool", lifecycle, &status).unwrap();
5825            assert_eq!(module, expected);
5826            let installed =
5827                validate_inspected_cycles("pool", module, Some(&[1; 32]), &Nat::from(100_u8));
5828            assert_eq!(installed.is_ok(), module == InspectedModule::Any);
5829            for wrong in [
5830                EstatePoolAssetLifecycle::Ready,
5831                EstatePoolAssetLifecycle::PendingReset,
5832                EstatePoolAssetLifecycle::Failed,
5833                EstatePoolAssetLifecycle::Workload,
5834            ] {
5835                if wrong != lifecycle {
5836                    assert!(matches!(
5837                        pool_funding_inspected_module("pool", wrong, &status),
5838                        Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { .. })
5839                    ));
5840                }
5841            }
5842        }
5843    }
5844
5845    #[cfg(unix)]
5846    #[test]
5847    fn funding_pause_uses_fresh_ledger_balance_without_completing_protocol_work() {
5848        let mut fixture = ProtocolOwnersFixture::new();
5849        let root = Principal::from_slice(&[1]);
5850        let ledger = Principal::from_slice(&[2]);
5851        fixture.platform.desired.cycles_ledger = ledger.to_text();
5852        fixture.platform.desired.canisters[1].principal = Some(root.to_text());
5853        std::fs::write(
5854            fixture.root.join("icp"),
5855            crate::test_support::tool_script(&format!(
5856                "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp @ICP_VERSION@'; exit 0; fi\ncat '{}'/balance.json\n",
5857                fixture.root.display(),
5858            )),
5859        )
5860        .unwrap();
5861        let pause = canic_core::dto::component_provisioning::RootEstateFundingRequired {
5862            available: Cycles::new(0),
5863            attempt_count: 0,
5864            creation_amount: Cycles::new(100),
5865            cycles_ledger: ledger,
5866            execution_margin: Cycles::new(10),
5867            last_attempt_at_ns: None,
5868            ledger_fee: Cycles::new(1),
5869            management_creation_fee: Cycles::new(20),
5870            operation_id: [1; 32],
5871            readiness_floor: Cycles::new(70),
5872            required: Cycles::new(101),
5873            retry_at_ns: 60,
5874            root,
5875            shortfall: Cycles::new(101),
5876        };
5877        for available in [0_u128, 40, 101, 404] {
5878            std::fs::write(
5879                fixture.root.join("balance.json"),
5880                serde_json::json!({
5881                    "response_bytes": hex_bytes(candid::encode_one(Nat::from(available)).unwrap()),
5882                })
5883                .to_string(),
5884            )
5885            .unwrap();
5886            let mut observed = EffectObservation {
5887                provisioning_progress: None,
5888                provisioning_failure: None,
5889                applied: false,
5890                estate_funding_required: Some(pause.clone()),
5891                post_cycles: None,
5892                progress_identity: "pending-creation".to_string(),
5893                retry: EffectRetry::ContinuePoolMaintenance,
5894            };
5895            fixture
5896                .platform
5897                .refresh_estate_funding_observation(&mut observed, &fixture.state)
5898                .unwrap();
5899            assert!(!observed.applied);
5900            assert_eq!(observed.retry, EffectRetry::ContinuePoolMaintenance);
5901            assert_eq!(observed.progress_identity, "pending-creation");
5902            let expected = (available < 101).then(|| {
5903                let mut expected = pause.clone();
5904                expected.available = Cycles::new(available);
5905                expected.shortfall = Cycles::new(101 - available);
5906                expected
5907            });
5908            assert_eq!(observed.estate_funding_required, expected);
5909        }
5910        for (root, ledger) in [
5911            (Principal::anonymous(), ledger),
5912            (root, Principal::anonymous()),
5913        ] {
5914            let mut changed = pause.clone();
5915            changed.root = root;
5916            changed.cycles_ledger = ledger;
5917            let mut observed = EffectObservation {
5918                provisioning_progress: None,
5919                provisioning_failure: None,
5920                applied: false,
5921                estate_funding_required: Some(changed),
5922                post_cycles: None,
5923                progress_identity: String::new(),
5924                retry: EffectRetry::None,
5925            };
5926            assert!(matches!(
5927                fixture
5928                    .platform
5929                    .refresh_estate_funding_observation(&mut observed, &fixture.state),
5930                Err(IcpEnsurePlatformError::CurrentProtocol(
5931                    current_protocol::CurrentProtocolError::ResponseMismatch
5932                ))
5933            ));
5934        }
5935        std::fs::remove_dir_all(&fixture.root).unwrap();
5936    }
5937
5938    #[test]
5939    fn completed_withdrawal_outside_bounds_fails_without_endless_observation() {
5940        let observation = NativeFundingObservation {
5941            amount: 350,
5942            expected_post_cycles: 800,
5943            funding_deficit_cycles: 150,
5944            funding_margin_cycles: 200,
5945            live_cycles: Some(599),
5946            pre_cycles: Some(449),
5947        };
5948        assert!(!native_funding_completion(observation, false).unwrap());
5949        assert!(matches!(
5950            native_funding_completion(observation, true),
5951            Err(IcpEnsurePlatformError::NativeFundingBalanceDrift {
5952                observation: actual,
5953            }) if *actual == observation
5954        ));
5955        assert!(
5956            native_funding_completion(
5957                NativeFundingObservation {
5958                    live_cycles: Some(600),
5959                    ..observation
5960                },
5961                true
5962            )
5963            .unwrap()
5964        );
5965    }
5966
5967    #[test]
5968    fn donation_does_not_substitute_for_a_withdrawal_receipt() {
5969        let observation = NativeFundingObservation {
5970            amount: 350,
5971            expected_post_cycles: 800,
5972            funding_deficit_cycles: 150,
5973            funding_margin_cycles: 200,
5974            live_cycles: Some(10_000),
5975            pre_cycles: Some(9_000),
5976        };
5977        assert!(!native_funding_completion(observation, false).unwrap());
5978        assert!(native_funding_completion(observation, true).unwrap());
5979    }
5980
5981    #[cfg(unix)]
5982    struct ProtocolOwnersFixture {
5983        root: PathBuf,
5984        platform: IcpEnsurePlatform,
5985        state: FleetEnsureStateRecord,
5986    }
5987
5988    #[cfg(unix)]
5989    impl ProtocolOwnersFixture {
5990        fn new() -> Self {
5991            use std::{fs, os::unix::fs::PermissionsExt};
5992
5993            let root = crate::test_support::temp_dir("canic-protocol-owner-observations");
5994            fs::create_dir_all(&root).unwrap();
5995            fs::write(root.join("owner.wasm"), b"current-owner-module").unwrap();
5996            let executable = root.join("icp");
5997            fs::write(&executable, crate::test_support::tool_script(&format!(
5998                "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp @ICP_VERSION@'; exit 0; fi\nwhile [ \"$#\" -gt 0 ] && [ \"$1\" != status ]; do shift; done\nshift\nprintf '%s\\n' \"$1\" >> '{}'/calls\ncat '{}'/\"$1\".json\n",
5999                root.display(), root.display(),
6000            ))).unwrap();
6001            fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
6002            let canisters = ["coordinator", "root", "store"].map(|name| {
6003                serde_json::json!({
6004                    "controller_canisters": [], "controllers": [], "drain": null,
6005                    "initial_cycles": "1T", "init_arg": null, "init_candid": null,
6006                    "kind": name, "minimum_cycles": "1T", "name": name,
6007                    "parent": null, "presence": "present", "principal": name,
6008                    "replace": false, "subnet": "subnet", "wasm": "owner.wasm",
6009                })
6010            });
6011            let desired = serde_json::from_value(serde_json::json!({
6012                "bootstrap": null, "canisters": canisters, "cycles_ledger": "ledger",
6013                "environment": "local", "fleet": "owners", "ledger_fee_cycles": "100M",
6014                "management_creation_fee_cycles": "500B", "material_cycle_threshold": "1B",
6015                "maximum_observation_burn_cycles": "1B", "maximum_stalled_observations": 4,
6016                "maximum_update_burn_cycles": "1B", "operator": "operator", "protocol": null,
6017                "schema_version": 1, "treasury": "treasury",
6018            }))
6019            .unwrap();
6020            let platform = IcpEnsurePlatform::new(desired, executable.to_str().unwrap(), &root)
6021                .with_process_fixture_pool_reader();
6022            let paths = crate::fleet_ensure::ops::EnsurePaths::under(&root, "local", "owners");
6023            let state = crate::fleet_ensure::ops::read_state(&paths, "owners").unwrap();
6024            let fixture = Self {
6025                root,
6026                platform,
6027                state,
6028            };
6029            for name in ["coordinator", "root", "store"] {
6030                fixture.status(name, "Running", true);
6031            }
6032            fixture
6033        }
6034
6035        fn synchronized_owners(count: usize) -> Self {
6036            Self::synchronized_owner_reads(count, false)
6037        }
6038
6039        fn synchronized_reinstall_owners(independent: usize) -> Self {
6040            Self::synchronized_owner_reads(independent + 1, true)
6041        }
6042
6043        fn synchronized_owner_reads(count: usize, roots_first: bool) -> Self {
6044            let mut fixture = Self::new();
6045            for index in fixture.platform.desired.canisters.len()..count {
6046                let mut root = fixture.platform.desired.canisters[1].clone();
6047                root.name = format!("owner-{index:02}");
6048                root.principal = Some(root.name.clone());
6049                fixture.status(&root.name, "Running", true);
6050                fixture.platform.desired.canisters.push(root);
6051            }
6052            if roots_first {
6053                let root = fixture.platform.desired.canisters.remove(1);
6054                for owner in &mut fixture.platform.desired.canisters {
6055                    owner.kind = DesiredCanisterKind::Coordinator;
6056                }
6057                fixture.platform.desired.canisters.push(root);
6058                for owner in &fixture.platform.desired.canisters {
6059                    let file = fixture.root.join(format!("{}.json", owner.name));
6060                    let mut status: serde_json::Value =
6061                        serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
6062                    status["version"] = serde_json::json!(7);
6063                    std::fs::write(file, serde_json::to_vec(&status).unwrap()).unwrap();
6064                }
6065            }
6066            let parallel_count = count - usize::from(roots_first);
6067            let delay_peer = if roots_first { "store" } else { "root" };
6068            let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
6069            std::fs::write(
6070                fixture.root.join("icp"),
6071                crate::test_support::tool_script(&format!(
6072                    r#"#!/bin/sh
6073if [ "$1" = '--version' ]; then echo 'icp @ICP_VERSION@'; exit 0; fi
6074while [ "$#" -gt 0 ] && [ "$1" != status ] && [ "$1" != identity ] && [ "$1" != cycles ]; do shift; done
6075case "$1" in
6076identity) echo operator; exit;;
6077cycles) echo '{{"balance":"1000000000000 cycles"}}'; exit;;
6078status) shift;;
6079*) exit 1;;
6080esac
6081cd '{}'
6082if [ '{roots_first}' = true ] && [ "$1" = root ]; then
6083    printf 'root\n' >> prerequisites
6084    cat root.json
6085    exit
6086fi
6087printf 'start:%s\n' "$1" >> events
6088started=$(grep -c '^start:' events)
6089goal=$(( (started + {bound} - 1) / {bound} * {bound} ))
6090[ "$goal" -le {parallel_count} ] || goal={parallel_count}
6091attempts=0
6092while [ "$(grep -c '^start:' events)" -lt "$goal" ]; do
6093    attempts=$((attempts + 1))
6094    [ "$attempts" -lt 500 ] || exit 1
6095    sleep 0.01
6096done
6097if [ -e "$1.delay" ]; then
6098    attempts=0
6099    while ! grep -q '^finish:{delay_peer}$' events; do
6100        attempts=$((attempts + 1))
6101        [ "$attempts" -lt 500 ] || exit 1
6102        sleep 0.01
6103    done
6104fi
6105printf 'finish:%s\n' "$1" >> events
6106cat "$1.json"
6107"#,
6108                    fixture.root.display()
6109                )),
6110            )
6111            .unwrap();
6112            fixture
6113        }
6114
6115        fn status(&self, name: &str, status: &str, current_module: bool) {
6116            let module = if current_module {
6117                artifact_hash(&self.root.join("owner.wasm")).unwrap()
6118            } else {
6119                "ff".repeat(32)
6120            };
6121            std::fs::write(
6122                self.root.join(format!("{name}.json")),
6123                serde_json::json!({
6124                    "id": name, "status": status, "settings": { "controllers": [] },
6125                    "module_hash": module, "cycles": "1000000000000",
6126                })
6127                .to_string(),
6128            )
6129            .unwrap();
6130        }
6131    }
6132
6133    #[cfg(unix)]
6134    fn install_preparation_action() -> EnsureAction {
6135        EnsureAction::Install {
6136            canic_init: None,
6137            reinstall_witness: None,
6138            init_arg: None,
6139            init_arg_sha256: None,
6140            init_candid: None,
6141            init_candid_sha256: None,
6142            mode: InstallMode::Install,
6143            name: "root".into(),
6144            principal: "root".into(),
6145            wasm: "owner.wasm".into(),
6146            wasm_sha256: "ff".repeat(32),
6147        }
6148    }
6149
6150    #[cfg(unix)]
6151    fn preparation_status(fixture: &ProtocolOwnersFixture, version: u64) {
6152        let path = fixture.root.join("root.json");
6153        let mut status: serde_json::Value =
6154            serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
6155        status["version"] = serde_json::json!(version);
6156        std::fs::write(path, serde_json::to_vec(&status).unwrap()).unwrap();
6157    }
6158
6159    #[cfg(unix)]
6160    #[test]
6161    fn reconciliation_reuses_direct_balance_and_refreshes_state_after_failure() {
6162        let mut install = install_preparation_action();
6163        let mut fixture = ProtocolOwnersFixture::new();
6164        if let EnsureAction::Install { wasm_sha256, .. } = &mut install {
6165            *wasm_sha256 = artifact_hash(&fixture.root.join("owner.wasm")).unwrap();
6166        }
6167        let actions = [
6168            install,
6169            EnsureAction::Start {
6170                name: "root".into(),
6171                principal: "root".into(),
6172            },
6173            EnsureAction::Stop {
6174                name: "root".into(),
6175                principal: "root".into(),
6176            },
6177            EnsureAction::SetControllers {
6178                name: "root".into(),
6179                principal: "root".into(),
6180                controllers: vec![],
6181                controller_canisters: vec![],
6182            },
6183        ];
6184        for action in actions {
6185            fixture.status("root", "Running", true);
6186            preparation_status(&fixture, 7);
6187            let prepared = super::super::effect_preparation::prepare_effect(
6188                &mut fixture.platform,
6189                "op",
6190                &action,
6191                &fixture.state,
6192            )
6193            .unwrap();
6194            let before = fixture.platform.icp.remote_call_count();
6195            let observed = fixture
6196                .platform
6197                .observe_effect("op", &action, &prepared.record, &fixture.state)
6198                .unwrap();
6199            assert_eq!(observed.post_cycles, Some(1_000_000_000_000));
6200            assert_eq!(fixture.platform.icp.remote_call_count() - before, 1);
6201            assert_eq!(
6202                observed.applied,
6203                !matches!(action, EnsureAction::Stop { .. })
6204            );
6205
6206            let file = fixture.root.join("root.json");
6207            let mut status: serde_json::Value =
6208                serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
6209            status["cycles"] = serde_json::json!("123");
6210            status["status"] = serde_json::json!("Stopped");
6211            status["module_hash"] = serde_json::json!("ab".repeat(32));
6212            status["settings"]["controllers"] = serde_json::json!(["other"]);
6213            std::fs::write(&file, serde_json::to_vec(&status).unwrap()).unwrap();
6214            let changed = fixture
6215                .platform
6216                .observe_effect("op", &action, &prepared.record, &fixture.state)
6217                .unwrap();
6218            assert_eq!(changed.post_cycles, Some(123));
6219            assert_eq!(changed.applied, matches!(action, EnsureAction::Stop { .. }));
6220
6221            status["id"] = serde_json::json!("wrong");
6222            std::fs::write(&file, serde_json::to_vec(&status).unwrap()).unwrap();
6223            assert!(matches!(
6224                fixture
6225                    .platform
6226                    .observe_effect("op", &action, &prepared.record, &fixture.state,),
6227                Err(IcpEnsurePlatformError::StatusIdentityMismatch { .. })
6228            ));
6229            status["id"] = serde_json::json!("root");
6230            status["cycles"] = serde_json::json!("456");
6231            std::fs::write(&file, serde_json::to_vec(&status).unwrap()).unwrap();
6232            let retried = fixture
6233                .platform
6234                .observe_effect("op", &action, &prepared.record, &fixture.state)
6235                .unwrap();
6236            assert_eq!(retried.post_cycles, Some(456));
6237            assert_eq!(fixture.platform.icp.remote_call_count() - before, 4);
6238        }
6239        std::fs::remove_dir_all(fixture.root).unwrap();
6240    }
6241
6242    #[cfg(unix)]
6243    #[test]
6244    fn reconciliation_keeps_protected_pool_balance_authority() {
6245        let mut fixture = PoolInspectionFixture::fresh(1);
6246        let principal = fixture.target.to_text();
6247        std::fs::write(
6248            fixture.owners.root.join(format!("{principal}.json")),
6249            serde_json::json!({
6250                "id": principal, "version": 7, "status": "Running",
6251                "settings": { "controllers": [fixture.root_id.to_text()] },
6252                "module_hash": null, "cycles": "9999",
6253            })
6254            .to_string(),
6255        )
6256        .unwrap();
6257        let mut install = install_preparation_action();
6258        if let EnsureAction::Install {
6259            name,
6260            principal: target,
6261            ..
6262        } = &mut install
6263        {
6264            *name = "pool-000".into();
6265            *target = principal.clone();
6266        }
6267        let actions = [
6268            install,
6269            EnsureAction::Start {
6270                name: "pool-000".into(),
6271                principal: principal.clone(),
6272            },
6273            EnsureAction::Stop {
6274                name: "pool-000".into(),
6275                principal: principal.clone(),
6276            },
6277            EnsureAction::SetControllers {
6278                name: "pool-000".into(),
6279                principal,
6280                controllers: vec![fixture.root_id.to_text()],
6281                controller_canisters: vec![],
6282            },
6283        ];
6284        for action in actions {
6285            PoolInspectionFixture::fresh_response(
6286                &fixture.owners.root,
6287                vec![fixture.root_id],
6288                None,
6289                1000,
6290            );
6291            let prepared = super::super::effect_preparation::prepare_effect(
6292                &mut fixture.owners.platform,
6293                "op",
6294                &action,
6295                &fixture.owners.state,
6296            )
6297            .unwrap();
6298            let observed = fixture
6299                .owners
6300                .platform
6301                .observe_effect("op", &action, &prepared.record, &fixture.owners.state)
6302                .unwrap();
6303            assert_eq!(observed.post_cycles, None);
6304            assert_eq!(
6305                fixture
6306                    .owners
6307                    .platform
6308                    .action_cycles(&action, &fixture.owners.state)
6309                    .unwrap(),
6310                Some(1000)
6311            );
6312            PoolInspectionFixture::fresh_response(
6313                &fixture.owners.root,
6314                vec![fixture.root_id],
6315                None,
6316                900,
6317            );
6318            assert_eq!(
6319                fixture
6320                    .owners
6321                    .platform
6322                    .action_cycles(&action, &fixture.owners.state)
6323                    .unwrap(),
6324                Some(900)
6325            );
6326            PoolInspectionFixture::fresh_response(
6327                &fixture.owners.root,
6328                vec![Principal::anonymous()],
6329                None,
6330                900,
6331            );
6332            assert!(matches!(
6333                fixture
6334                    .owners
6335                    .platform
6336                    .action_cycles(&action, &fixture.owners.state),
6337                Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { .. })
6338            ));
6339        }
6340        std::fs::remove_dir_all(fixture.owners.root).unwrap();
6341    }
6342
6343    #[cfg(unix)]
6344    #[test]
6345    fn install_preparation_shares_one_fresh_status_and_expires_on_success_and_failure() {
6346        use std::sync::{Arc, Mutex};
6347        let mut fixture = ProtocolOwnersFixture::new();
6348        let action = install_preparation_action();
6349        preparation_status(&fixture, 7);
6350        let events = Arc::new(Mutex::new(Vec::new()));
6351        let sink = Arc::clone(&events);
6352        fixture.platform = fixture
6353            .platform
6354            .with_observation_handler(move |event| sink.lock().unwrap().push(event));
6355        let prepared = super::super::effect_preparation::prepare_effect(
6356            &mut fixture.platform,
6357            "op",
6358            &action,
6359            &fixture.state,
6360        )
6361        .unwrap();
6362        assert_eq!(prepared.record.pre_cycles, Some(1_000_000_000_000));
6363        assert_eq!(prepared.record.pre_canister_version, Some(7));
6364        assert_eq!(fixture.platform.icp.remote_call_count(), 1);
6365        assert!(fixture.platform.observation_snapshot.borrow().is_none());
6366        let measured = events.lock().unwrap();
6367        let start = measured.first().unwrap();
6368        let end = measured.last().unwrap();
6369        assert_eq!(start.succeeded, None);
6370        assert_eq!(end.succeeded, Some(true));
6371        assert_eq!(start.span_id, end.span_id);
6372        assert_eq!(end.remote_call_attempts, 1);
6373        assert_eq!(end.cached_read_hits, 1);
6374        drop(measured);
6375        preparation_status(&fixture, 8);
6376        let next = super::super::effect_preparation::prepare_effect(
6377            &mut fixture.platform,
6378            "op",
6379            &action,
6380            &fixture.state,
6381        )
6382        .unwrap();
6383        assert_eq!(next.record.pre_canister_version, Some(8));
6384        assert_eq!(fixture.platform.icp.remote_call_count(), 2);
6385        let status = std::fs::read(fixture.root.join("root.json")).unwrap();
6386        std::fs::write(fixture.root.join("root.json"), b"{}").unwrap();
6387        assert!(matches!(
6388            super::super::effect_preparation::prepare_effect(
6389                &mut fixture.platform,
6390                "op",
6391                &action,
6392                &fixture.state
6393            ),
6394            Err(IcpEnsurePlatformError::Icp(_))
6395        ));
6396        assert!(fixture.platform.observation_snapshot.borrow().is_none());
6397        std::fs::write(fixture.root.join("root.json"), status).unwrap();
6398        assert!(
6399            super::super::effect_preparation::prepare_effect(
6400                &mut fixture.platform,
6401                "op",
6402                &action,
6403                &fixture.state
6404            )
6405            .is_ok()
6406        );
6407        assert_eq!(fixture.platform.icp.remote_call_count(), 4);
6408        std::fs::remove_dir_all(fixture.root).unwrap();
6409    }
6410
6411    #[cfg(unix)]
6412    #[test]
6413    #[ignore = "opt-in matched local IPC measurement; no live IC timing claim"]
6414    fn install_preparation_matched_measurement() {
6415        let mut fixture = ProtocolOwnersFixture::new();
6416        preparation_status(&fixture, 7);
6417        let action = install_preparation_action();
6418        // Prime tool compatibility identically; this measures warm IPC, not first CLI launch.
6419        fixture.platform.status_optional("root").unwrap();
6420        for pair in 0..8 {
6421            for shared in if pair % 2 == 0 {
6422                [false, true]
6423            } else {
6424                [true, false]
6425            } {
6426                let before = fixture.platform.icp.remote_call_count();
6427                let started = std::time::Instant::now();
6428                let read = |platform: &mut IcpEnsurePlatform| -> Result<_, IcpEnsurePlatformError> {
6429                    Ok((
6430                        platform.action_cycles(&action, &fixture.state)?,
6431                        platform.action_canister_version(&action, &fixture.state)?,
6432                    ))
6433                };
6434                let result = if shared {
6435                    fixture
6436                        .platform
6437                        .with_preparation_observations(&action, read)
6438                } else {
6439                    read(&mut fixture.platform)
6440                }
6441                .unwrap();
6442                let elapsed = started.elapsed().as_micros();
6443                let calls = fixture.platform.icp.remote_call_count() - before;
6444                assert_eq!(result, (Some(1_000_000_000_000), Some(7)));
6445                assert_eq!(calls, if shared { 1 } else { 2 });
6446                eprintln!(
6447                    "[CANIC-PREPARATION] {}",
6448                    serde_json::json!({"pair": pair, "shared": shared, "elapsed_micros": elapsed, "calls": calls, "cycles": result.0.map(|v| v.to_string()), "version": result.1})
6449                );
6450            }
6451        }
6452        std::fs::remove_dir_all(fixture.root).unwrap();
6453    }
6454
6455    #[cfg(unix)]
6456    struct PoolInspectionFixture {
6457        owners: ProtocolOwnersFixture,
6458        root_id: Principal,
6459        target: Principal,
6460    }
6461
6462    #[cfg(unix)]
6463    impl PoolInspectionFixture {
6464        fn new() -> Self {
6465            let mut owners = ProtocolOwnersFixture::new();
6466            let root_id = Principal::from_slice(&[2]);
6467            let operator = Principal::from_slice(&[9]);
6468            owners.platform.desired.operator = operator.to_text();
6469            owners.platform.desired.protocol =
6470                Some(crate::fleet_ensure::model::DesiredFleetProtocol {
6471                    app_config: "unused.toml".to_string(),
6472                    component_group_placements: Vec::new(),
6473                    root_candid: "root.did".to_string(),
6474                    coordinator_candid: "unused.did".to_string(),
6475                    store_candid: "unused.did".to_string(),
6476                });
6477            std::fs::write(owners.root.join("root.did"),
6478                "service : { canic_observability : (variant { InspectionReserve : record { canister_id : principal } }) -> () query }"
6479            ).unwrap();
6480            std::fs::write(owners.root.join("operator"), operator.to_text()).unwrap();
6481            for id in [root_id, Principal::from_slice(&[3])] {
6482                std::fs::write(
6483                    owners.root.join(format!("{id}.json")),
6484                    serde_json::json!({
6485                        "id": id.to_text(), "status": "Running", "settings": {"controllers": []},
6486                        "module_hash": null, "cycles": "10000",
6487                    })
6488                    .to_string(),
6489                )
6490                .unwrap();
6491            }
6492            std::fs::write(owners.root.join("icp"), crate::test_support::tool_script(&format!(
6493                "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp @ICP_VERSION@'; 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",
6494                owners.root.display(), owners.root.display(), owners.root.display(), owners.root.display(),
6495            ))).unwrap();
6496            let script = std::fs::read_to_string(owners.root.join("icp")).unwrap();
6497            let reserve_route = format!(
6498                r#"call) if [ "$3" = canic_observability ]; then
6499for expected in '{}'/reserve-"$2"-*.bin; do
6500    if cmp -s "$5" "$expected"; then cat "${{expected%.bin}}.json"; exit; fi
6501done
6502exit 1
6503fi
6504if"#,
6505                owners.root.display()
6506            );
6507            std::fs::write(
6508                owners.root.join("icp"),
6509                script.replace("call) if", &reserve_route),
6510            )
6511            .unwrap();
6512            Self::response(&owners.root, root_id, 1_000);
6513            for (caller, target) in [
6514                (root_id, Principal::from_slice(&[4])),
6515                (root_id, Principal::from_slice(&[5])),
6516                (Principal::from_slice(&[3]), Principal::from_slice(&[4])),
6517            ] {
6518                Self::reserve_response(&owners.root, caller, target, 1_000);
6519            }
6520            Self {
6521                owners,
6522                root_id,
6523                target: Principal::from_slice(&[4]),
6524            }
6525        }
6526
6527        fn reserve_response(path: &Path, caller: Principal, target: Principal, liquid: u128) {
6528            use crate::canister_protocol::inspection::{
6529                InspectionReserveRequest, InspectionReserveResponse,
6530            };
6531            let base = path.join(format!("reserve-{caller}-{target}"));
6532            std::fs::write(
6533                base.with_extension("bin"),
6534                candid::encode_one(InspectionReserveRequest::InspectionReserve(
6535                    CanisterInspectionRequest {
6536                        canister_id: target,
6537                    },
6538                ))
6539                .unwrap(),
6540            )
6541            .unwrap();
6542            let response = Ok::<_, canic_core::dto::error::Error>(
6543                InspectionReserveResponse::InspectionReserve(
6544                    canic_core::dto::canister::CanisterInspectionReserveResponse {
6545                        caller,
6546                        canister_id: target,
6547                        native_cycles: 1000,
6548                        available_liquid_cycles: liquid,
6549                        required_liquid_cycles: 100,
6550                    },
6551                ),
6552            );
6553            std::fs::write(
6554                base.with_extension("json"),
6555                serde_json::json!({
6556                    "response_bytes": hex_bytes(candid::encode_one(response).unwrap()),
6557                })
6558                .to_string(),
6559            )
6560            .unwrap();
6561        }
6562
6563        fn response(path: &Path, controller: Principal, cycles: u128) {
6564            let response = Ok::<_, canic_core::dto::error::Error>(
6565                RootInspectionResponse::InspectCanister(RootInspectionStatus {
6566                    status: canic_core::dto::canister::CanisterStatusType::Running,
6567                    settings: ManagementCanisterObservationSettings {
6568                        controllers: vec![controller],
6569                    },
6570                    module_hash: Some(vec![1; 32]),
6571                    cycles: Nat::from(cycles),
6572                }),
6573            );
6574            std::fs::write(
6575                path.join("inspection.json"),
6576                serde_json::json!({
6577                    "response_bytes": hex_bytes(candid::encode_one(response).unwrap()),
6578                })
6579                .to_string(),
6580            )
6581            .unwrap();
6582        }
6583    }
6584
6585    // Exercise preparation and completion directly when checking scope expiry.
6586    impl IcpEnsurePlatform {
6587        /// Native projection fixtures supply passive query records; PocketIC owns transport proof.
6588        pub(in crate::fleet_ensure) fn with_process_fixture_pool_reader(mut self) -> Self {
6589            self.pool_reader = RootPoolReader(|icp, root, request| {
6590                crate::canister_protocol::query_with_candid(
6591                    icp,
6592                    Path::new("fixture-root.did"),
6593                    root,
6594                    canic_protocol::CANIC_ROOT_STATUS,
6595                    request,
6596                )
6597            });
6598            self
6599        }
6600
6601        #[cfg(unix)]
6602        fn inspect_pending_pool_balance(
6603            &self,
6604            configured: &crate::fleet_ensure::model::DesiredCanister,
6605            principal: &str,
6606            state: &FleetEnsureStateRecord,
6607        ) -> Result<u128, IcpEnsurePlatformError> {
6608            let inspection = self.prepare_pending_pool_inspection(configured, principal, state)?;
6609            let response = self.read_pool_inspection(inspection.authority)?;
6610            self.complete_pool_inspection(inspection, response)
6611        }
6612    }
6613
6614    #[cfg(unix)]
6615    impl PoolInspectionFixture {
6616        fn fresh(pool_count: u32) -> Self {
6617            let mut fixture = Self::new();
6618            let owners = &mut fixture.owners;
6619            let operator = Principal::from_text(&owners.platform.desired.operator).unwrap();
6620            let mut root = owners.platform.desired.canisters[1].clone();
6621            root.principal = Some(fixture.root_id.to_text());
6622            root.controllers = vec![operator.to_text()];
6623            owners.platform.desired.canisters = vec![root.clone()];
6624            let config = canic_core::bootstrap::parse_config_model(
6625                "[app]\nname = 'inspection'\n[roles.root]\nkind = 'root'\n",
6626            )
6627            .unwrap();
6628            owners.platform.desired.bootstrap = Some(crate::fleet_ensure::model::DesiredFleetBootstrap {
6629            admission_identity_origin: None,
6630                admission: canic_core::shared_support::fleet_admission_policy::compile_fleet_admission_policy_template(vec![operator], Vec::new()).unwrap(),
6631                app: canic_core::ids::AppId::from("inspection"),
6632                canonical_network_id: canic_core::ids::CanonicalNetworkId::ic_mainnet(),
6633                component_deployment_configuration: config.compile_component_deployment_configuration().unwrap(),
6634                coordinator: "coordinator".into(),
6635                coordinator_subnet: canic_core::ids::SubnetId::from_principal(fixture.root_id),
6636                fleet_id: canic_core::ids::FleetId::from_generated_bytes([5; 32]),
6637                fresh_estate: true,
6638                release_build_id: canic_core::ids::ReleaseBuildId::from_nonce(canic_core::ids::ReleaseBuildNonce::from_random_bytes([7; 32])),
6639                root_funding: None,
6640                roots: Vec::new(),
6641                            recovery_controllers: Vec::new(),
6642});
6643            std::fs::write(
6644                owners.root.join(format!("{}.json", fixture.root_id)),
6645                serde_json::json!({
6646                    "id": fixture.root_id.to_text(), "status": "Running",
6647                    "settings": {"controllers": [operator.to_text()]},
6648                    "module_hash": artifact_hash(&owners.root.join("owner.wasm")).unwrap(),
6649                    "cycles": "10000",
6650                })
6651                .to_string(),
6652            )
6653            .unwrap();
6654            let mut entries = Vec::new();
6655            for index in 0..pool_count {
6656                let target = Principal::from_slice(&[u8::try_from(index + 10).unwrap()]);
6657                Self::reserve_response(&owners.root, fixture.root_id, target, 1_000);
6658                let mut configured = root.clone();
6659                configured.name = format!("pool-{index:03}");
6660                configured.kind = DesiredCanisterKind::Pool;
6661                configured.principal = None;
6662                configured.parent = Some("root".into());
6663                owners
6664                    .state
6665                    .pending_principals
6666                    .insert(configured.name.clone(), target.to_text());
6667                owners.platform.desired.canisters.push(configured);
6668                entries.push(CanisterPoolAsset {
6669                    canister_id: target,
6670                    creation_receipt: None,
6671                    cycles: Cycles::new(0),
6672                    origin: CanisterPoolAssetOrigin::Created,
6673                    status: CanisterPoolAssetStatus::PendingReset,
6674                    added_at_ns: 1,
6675                    updated_at_ns: 1,
6676                });
6677            }
6678            fixture.target = entries[0].canister_id;
6679            Self::fresh_pool_page(&owners.root, entries);
6680            let script = std::fs::read_to_string(owners.root.join("icp")).unwrap();
6681            let script = script.replace(
6682                "call) if",
6683                &format!(
6684                    "call) case \"$*\" in *canic_root_status*) cat '{}'/pool.json; exit;; esac; if",
6685                    owners.root.display()
6686                ),
6687            );
6688            std::fs::write(owners.root.join("icp"), script).unwrap();
6689            Self::fresh_response(&owners.root, vec![fixture.root_id], None, 1_000);
6690            fixture
6691        }
6692
6693        fn fresh_pool_page(path: &Path, entries: Vec<CanisterPoolAsset>) {
6694            let pool_count = u32::try_from(entries.len()).unwrap();
6695            let page = CanisterPoolResponse {
6696                config: canic_core::ids::FleetSubnetCanisterPoolConfig {
6697                    minimum_size: 1,
6698                    maximum_size: 128,
6699                    canister_cycles: Cycles::new(1_000),
6700                    creation_execution_margin: Cycles::new(100),
6701                },
6702                tracked: pool_count,
6703                store: 0,
6704                store_deletion_pending: 0,
6705                pooled: pool_count,
6706                workload: 0,
6707                surplus: 0,
6708                ready: 0,
6709                pending_reset: pool_count,
6710                claimed: 0,
6711                recycling: 0,
6712                handing_off: 0,
6713                failed: 0,
6714                completed_handoffs: 0,
6715                pending_creation: None,
6716                pending_handoff: None,
6717                entries,
6718                next_start_after: None,
6719            };
6720            let response = Ok::<_, canic_core::dto::error::Error>(RootPoolStatusResponse::Pool(
6721                Box::new(page),
6722            ));
6723            std::fs::write(
6724                path.join("pool.json"),
6725                serde_json::json!({
6726                    "response_bytes": hex_bytes(candid::encode_one(response).unwrap()),
6727                })
6728                .to_string(),
6729            )
6730            .unwrap();
6731        }
6732
6733        fn fresh_response(
6734            path: &Path,
6735            controllers: Vec<Principal>,
6736            module_hash: Option<Vec<u8>>,
6737            cycles: u128,
6738        ) {
6739            let response = Ok::<_, canic_core::dto::error::Error>(
6740                RootInspectionResponse::InspectCanister(RootInspectionStatus {
6741                    status: canic_core::dto::canister::CanisterStatusType::Running,
6742                    settings: ManagementCanisterObservationSettings { controllers },
6743                    module_hash,
6744                    cycles: Nat::from(cycles),
6745                }),
6746            );
6747            std::fs::write(
6748                path.join("inspection.json"),
6749                serde_json::json!({
6750                    "response_bytes": hex_bytes(candid::encode_one(response).unwrap()),
6751                })
6752                .to_string(),
6753            )
6754            .unwrap();
6755        }
6756    }
6757
6758    #[cfg(unix)]
6759    impl PoolInspectionFixture {
6760        fn retained_observation(&self) -> FleetObservation {
6761            let assets = self
6762                .owners
6763                .state
6764                .pending_principals
6765                .values()
6766                .map(|principal| EstatePoolAssetObservation {
6767                    creation_receipt: None,
6768                    cycles: 500,
6769                    lifecycle: EstatePoolAssetLifecycle::Workload,
6770                    origin: EstatePoolAssetOrigin::Created,
6771                    principal: principal.clone(),
6772                })
6773                .collect::<Vec<_>>();
6774            FleetObservation {
6775                additional_controlled_cycles: assets
6776                    .iter()
6777                    .map(|asset| (asset.principal.clone(), asset.cycles))
6778                    .collect(),
6779                canisters: BTreeMap::new(),
6780                estate_funding_domains: BTreeMap::from([(
6781                    "root".into(),
6782                    EstateFundingDomainObservation {
6783                        balance_cycles: Some(0),
6784                        cycles_ledger: self.owners.platform.desired.cycles_ledger.clone(),
6785                        pool: Some(EstatePoolInventoryObservation {
6786                            assets,
6787                            maximum_size: 128,
6788                            minimum_size: 1,
6789                            pending_creation: None,
6790                            readiness_floor_cycles: 1_000,
6791                            creation_execution_margin_cycles: 100,
6792                        }),
6793                        root_principal: Some(self.root_id.to_text()),
6794                    },
6795                )]),
6796                ledger_fee_cycles: 100,
6797                operator_cycles: 1_000,
6798                protocol_ready: BTreeMap::new(),
6799            }
6800        }
6801
6802        fn inspect_retained(
6803            &self,
6804            observation: &mut FleetObservation,
6805            allow_pending: bool,
6806        ) -> Result<
6807            Vec<crate::fleet_ensure::model::FleetReinstallAssetRecord>,
6808            IcpEnsurePlatformError,
6809        > {
6810            let authorities = BTreeMap::from([(
6811                "root".into(),
6812                RootManagementCanisterObservation {
6813                    live: LiveCanister {
6814                        canister_version: None,
6815                        controllers: vec![self.owners.platform.desired.operator.clone()],
6816                        cycles: 10_000,
6817                        module_sha256: Some("01".repeat(32)),
6818                        principal: self.root_id.to_text(),
6819                        reinstall_required: false,
6820                        root_owned_lifecycle: None,
6821                        status: CanisterRuntimeStatus::Running,
6822                    },
6823                    name: "root".into(),
6824                    subnet: "subnet".into(),
6825                },
6826            )]);
6827            let candid = BTreeMap::from([(
6828                "root".into(),
6829                self.owners.platform.root_protocol_candid().unwrap(),
6830            )]);
6831            self.owners.platform.inspect_reinstall_assets(
6832                &self.owners.state,
6833                &authorities,
6834                observation,
6835                &candid,
6836                allow_pending,
6837            )
6838        }
6839
6840        fn verify_retained(
6841            &self,
6842            assets: &[crate::fleet_ensure::model::FleetReinstallAssetRecord],
6843            check: super::super::ReinstallAssetCheck,
6844        ) -> Result<bool, IcpEnsurePlatformError> {
6845            IcpEnsurePlatform::reinstall_assets_match_bound(
6846                &self.owners.platform.icp,
6847                &self.owners.platform.root_protocol_candid().unwrap(),
6848                self.root_id,
6849                &assets.iter().collect::<Vec<_>>(),
6850                check,
6851            )
6852        }
6853
6854        fn inspection_hook(&self, hook: &str) {
6855            let path = &self.owners.root;
6856            let script = std::fs::read_to_string(path.join("icp")).unwrap();
6857            let response = format!("cat '{}'/inspection.json", path.display());
6858            assert!(script.contains(&response));
6859            std::fs::write(
6860                path.join("icp"),
6861                script.replace(&response, &format!("{hook}\n{response}")),
6862            )
6863            .unwrap();
6864        }
6865
6866        fn route_inspection_targets(&self) {
6867            let path = &self.owners.root;
6868            for configured in self
6869                .owners
6870                .platform
6871                .desired
6872                .canisters
6873                .iter()
6874                .filter(|canister| canister.kind == DesiredCanisterKind::Pool)
6875            {
6876                let target = &self.owners.state.pending_principals[&configured.name];
6877                let request = RootInspectionCommand::InspectCanister(CanisterInspectionRequest {
6878                    canister_id: Principal::from_text(target).unwrap(),
6879                });
6880                std::fs::write(
6881                    path.join(format!("request-{}.bin", configured.name)),
6882                    candid::encode_one(request).unwrap(),
6883                )
6884                .unwrap();
6885            }
6886            self.inspection_hook(&format!(
6887                r#"
6888cd '{}'
6889request_name=
6890for expected in request-*.bin; do
6891    if cmp -s "$5" "$expected"; then
6892        request_name=${{expected#request-}}
6893        request_name=${{request_name%.bin}}
6894        break
6895    fi
6896done
6897[ -n "$request_name" ] || exit 1
6898printf 'start:%s\n' "$request_name" >> events
6899if [ -f "$request_name.fail" ]; then
6900    printf 'finish:%s\n' "$request_name" >> events
6901    exit 1
6902fi
6903if [ -f "$request_name.response" ]; then
6904    attempts=0
6905    while ! grep -q '^finish:pool-001$' events; do
6906        attempts=$((attempts + 1))
6907        [ "$attempts" -lt 500 ] || exit 1
6908        sleep 0.01
6909    done
6910    printf 'finish:%s\n' "$request_name" >> events
6911    cat "$request_name.response"
6912    exit
6913fi
6914printf 'finish:%s\n' "$request_name" >> events
6915"#,
6916                path.display()
6917            ));
6918        }
6919    }
6920
6921    #[cfg(unix)]
6922    #[test]
6923    fn reinstall_inspections_overlap_and_retain_exact_inventory() {
6924        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
6925        let count = bound * 2 + 1;
6926        let fixture = PoolInspectionFixture::fresh(u32::try_from(count).unwrap());
6927        let path = &fixture.owners.root;
6928        fixture.inspection_hook(&format!(
6929            r#"
6930cd '{}'
6931printf 'start\n' >> events
6932started=$(grep -c '^start$' events)
6933goal=$(( (started + {bound} - 1) / {bound} * {bound} ))
6934[ "$goal" -le {count} ] || goal={count}
6935attempts=0
6936while [ "$(grep -c '^start$' events)" -lt "$goal" ]; do
6937    attempts=$((attempts + 1))
6938    [ "$attempts" -lt 500 ] || exit 1
6939    sleep 0.01
6940done
6941printf 'finish\n' >> events
6942"#,
6943            path.display()
6944        ));
6945        let mut observation = fixture.retained_observation();
6946        let assets = fixture.inspect_retained(&mut observation, false).unwrap();
6947        assert_eq!(assets.len(), count);
6948        assert!(
6949            assets
6950                .windows(2)
6951                .all(|pair| pair[0].principal < pair[1].principal)
6952        );
6953        assert!(
6954            assets
6955                .iter()
6956                .all(|asset| asset.controllers == [fixture.root_id.to_text()]
6957                    && asset.module_sha256.is_none())
6958        );
6959        assert_eq!(observation.canisters.len(), count);
6960        assert!(observation.additional_controlled_cycles.is_empty());
6961        assert!(
6962            observation
6963                .canisters
6964                .values()
6965                .all(|live| live.as_ref().unwrap().cycles == 1_000)
6966        );
6967        std::fs::write(path.join("events"), []).unwrap();
6968        assert!(
6969            fixture
6970                .verify_retained(&assets, super::super::ReinstallAssetCheck::BeforeReset)
6971                .unwrap()
6972        );
6973        let (mut active, mut maximum) = (0, 0);
6974        for event in std::fs::read_to_string(path.join("events"))
6975            .unwrap()
6976            .lines()
6977        {
6978            match event {
6979                "start" => {
6980                    active += 1;
6981                    maximum = maximum.max(active);
6982                }
6983                "finish" => {
6984                    assert!(active > 0);
6985                    active -= 1;
6986                }
6987                _ => panic!("unknown inspection event"),
6988            }
6989            assert!(active <= bound);
6990        }
6991        assert_eq!((active, maximum), (0, bound));
6992        assert_eq!(
6993            fixture.owners.platform.icp.remote_call_count(),
6994            4 * u64::try_from(count).unwrap()
6995        );
6996        std::fs::remove_dir_all(path).unwrap();
6997    }
6998
6999    #[cfg(unix)]
7000    #[test]
7001    fn reinstall_verification_drains_mismatch_before_later_error_and_reads_fresh() {
7002        use super::super::ReinstallAssetCheck::{BeforeReset, Terminal};
7003        let fixture = PoolInspectionFixture::fresh(5);
7004        let path = &fixture.owners.root;
7005        let assets = fixture
7006            .owners
7007            .state
7008            .pending_principals
7009            .values()
7010            .map(
7011                |principal| crate::fleet_ensure::model::FleetReinstallAssetRecord {
7012                    controllers: vec![fixture.root_id.to_text()],
7013                    module_sha256: None,
7014                    principal: principal.clone(),
7015                    root: "root".into(),
7016                    subnet: "subnet".into(),
7017                },
7018            )
7019            .collect::<Vec<_>>();
7020        PoolInspectionFixture::fresh_response(path, vec![Principal::anonymous()], None, 1000);
7021        std::fs::copy(path.join("inspection.json"), path.join("pool-000.response")).unwrap();
7022        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 1000);
7023        std::fs::write(path.join("pool-001.fail"), []).unwrap();
7024        fixture.route_inspection_targets();
7025        assert!(!fixture.verify_retained(&assets, BeforeReset).unwrap());
7026        let events = std::fs::read_to_string(path.join("events")).unwrap();
7027        for index in 0..4 {
7028            assert!(events.contains(&format!("finish:pool-{index:03}")));
7029        }
7030        assert!(!events.contains("pool-004"));
7031        assert!(events.find("finish:pool-001").unwrap() < events.find("finish:pool-000").unwrap());
7032        std::fs::remove_file(path.join("pool-000.response")).unwrap();
7033        assert!(matches!(
7034            fixture.verify_retained(&assets, BeforeReset),
7035            Err(IcpEnsurePlatformError::CurrentProtocol(_))
7036        ));
7037        std::fs::remove_file(path.join("pool-001.fail")).unwrap();
7038        assert!(fixture.verify_retained(&assets, BeforeReset).unwrap());
7039        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], Some(vec![1; 32]), 1000);
7040        assert!(!fixture.verify_retained(&assets, BeforeReset).unwrap());
7041        assert!(fixture.verify_retained(&assets, Terminal).unwrap());
7042        PoolInspectionFixture::fresh_response(
7043            path,
7044            vec![Principal::anonymous()],
7045            Some(vec![1; 32]),
7046            1000,
7047        );
7048        assert!(!fixture.verify_retained(&assets, Terminal).unwrap());
7049        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 1000);
7050        PoolInspectionFixture::reserve_response(path, fixture.root_id, fixture.target, 0);
7051        assert!(matches!(
7052            fixture.verify_retained(&assets, BeforeReset),
7053            Err(IcpEnsurePlatformError::CurrentProtocol(
7054                current_protocol::CurrentProtocolError::Transport(
7055                    CanisterProtocolError::InspectionPreflightReserve(_)
7056                )
7057            ))
7058        ));
7059        std::fs::remove_dir_all(path).unwrap();
7060    }
7061
7062    #[cfg(unix)]
7063    #[test]
7064    fn reinstall_inspections_drain_failure_in_order_and_retry_fresh() {
7065        let fixture = PoolInspectionFixture::fresh(5);
7066        let path = &fixture.owners.root;
7067        PoolInspectionFixture::fresh_response(
7068            path,
7069            vec![fixture.root_id, Principal::anonymous()],
7070            None,
7071            1_000,
7072        );
7073        std::fs::copy(path.join("inspection.json"), path.join("pool-000.response")).unwrap();
7074        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 2_000);
7075        std::fs::write(path.join("pool-001.fail"), []).unwrap();
7076        fixture.route_inspection_targets();
7077        let mut observation = fixture.retained_observation();
7078        let before = observation.clone();
7079        assert!(
7080            matches!(fixture.inspect_retained(&mut observation, false), Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { field: "exact Root controllers", canister }) if canister == fixture.target.to_text())
7081        );
7082        assert_eq!(observation, before);
7083        let events = std::fs::read_to_string(path.join("events")).unwrap();
7084        for index in 0..4 {
7085            assert!(events.contains(&format!("finish:pool-{index:03}")));
7086        }
7087        assert!(!events.contains("pool-004"));
7088        assert!(events.find("finish:pool-001").unwrap() < events.find("finish:pool-000").unwrap());
7089        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 8);
7090        std::fs::remove_file(path.join("pool-000.response")).unwrap();
7091        std::fs::remove_file(path.join("pool-001.fail")).unwrap();
7092        for cycles in [2_000, 3_000] {
7093            PoolInspectionFixture::fresh_response(
7094                path,
7095                vec![fixture.root_id],
7096                Some(vec![1; 32]),
7097                cycles,
7098            );
7099            let assets = fixture.inspect_retained(&mut observation, false).unwrap();
7100            assert_eq!(assets.len(), 5);
7101            assert!(
7102                assets
7103                    .iter()
7104                    .all(|asset| asset.module_sha256.as_deref() == Some(&"01".repeat(32)))
7105            );
7106            assert!(
7107                observation
7108                    .canisters
7109                    .values()
7110                    .all(|live| live.as_ref().unwrap().cycles == cycles)
7111            );
7112        }
7113        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 28);
7114        std::fs::remove_dir_all(path).unwrap();
7115    }
7116
7117    #[cfg(unix)]
7118    #[test]
7119    fn reinstall_inspections_reject_duplicates_and_preserve_lifecycle_admission() {
7120        let fixture = PoolInspectionFixture::fresh(5);
7121        let mut observation = fixture.retained_observation();
7122        let pool = observation
7123            .estate_funding_domains
7124            .get_mut("root")
7125            .unwrap()
7126            .pool
7127            .as_mut()
7128            .unwrap();
7129        pool.assets[4].principal = pool.assets[0].principal.clone();
7130        assert!(matches!(
7131            fixture.inspect_retained(&mut observation, false),
7132            Err(IcpEnsurePlatformError::RootManagement(_))
7133        ));
7134        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 8);
7135        for lifecycle in [
7136            EstatePoolAssetLifecycle::PendingReset,
7137            EstatePoolAssetLifecycle::Failed,
7138            EstatePoolAssetLifecycle::Claimed,
7139        ] {
7140            let mut observation = fixture.retained_observation();
7141            let pool = observation
7142                .estate_funding_domains
7143                .get_mut("root")
7144                .unwrap()
7145                .pool
7146                .as_mut()
7147                .unwrap();
7148            pool.assets.truncate(1);
7149            pool.assets[0].lifecycle = lifecycle;
7150            let before = fixture.owners.platform.icp.remote_call_count();
7151            assert!(matches!(
7152                fixture.inspect_retained(&mut observation, false),
7153                Err(IcpEnsurePlatformError::RootManagement(_))
7154            ));
7155            assert_eq!(fixture.owners.platform.icp.remote_call_count(), before);
7156            let result = fixture.inspect_retained(&mut observation, true);
7157            if lifecycle == EstatePoolAssetLifecycle::Claimed {
7158                assert!(matches!(
7159                    result,
7160                    Err(IcpEnsurePlatformError::RootManagement(_))
7161                ));
7162                assert_eq!(fixture.owners.platform.icp.remote_call_count(), before);
7163            } else {
7164                assert_eq!(result.unwrap().len(), 1);
7165                assert_eq!(fixture.owners.platform.icp.remote_call_count(), before + 2);
7166            }
7167        }
7168        std::fs::remove_dir_all(&fixture.owners.root).unwrap();
7169    }
7170
7171    #[cfg(unix)]
7172    #[test]
7173    fn fresh_root_pool_observation_waits_for_wasm_and_preserves_query_failures() {
7174        let mut fixture = PoolInspectionFixture::fresh(1);
7175        let root = fixture.root_id.to_text();
7176        let owners = &mut fixture.owners;
7177        owners.platform.desired.canisters[0].principal = None;
7178        owners.state.principals.insert("root".into(), root.clone());
7179        let path = owners.root.join(format!("{root}.json"));
7180        let installed = std::fs::read(&path).unwrap();
7181        let mut status: serde_json::Value = serde_json::from_slice(&installed).unwrap();
7182        status["module_hash"] = serde_json::Value::Null;
7183        std::fs::write(&path, serde_json::to_vec(&status).unwrap()).unwrap();
7184        let pool = std::fs::read(owners.root.join("pool.json")).unwrap();
7185        std::fs::remove_file(owners.root.join("pool.json")).unwrap();
7186        let before = owners.platform.icp.remote_call_count();
7187        assert!(
7188            owners
7189                .platform
7190                .observe_estate_pool_inventory("root", &root, &owners.state)
7191                .unwrap()
7192                .is_none()
7193        );
7194        assert_eq!(owners.platform.icp.remote_call_count(), before + 1);
7195
7196        // Supplied identities and non-fresh estates retain protocol failures.
7197        for supplied in [false, true] {
7198            owners
7199                .platform
7200                .desired
7201                .bootstrap
7202                .as_mut()
7203                .unwrap()
7204                .fresh_estate = supplied;
7205            owners.platform.desired.canisters[0].principal = supplied.then(|| root.clone());
7206            assert!(matches!(
7207                owners
7208                    .platform
7209                    .observe_estate_pool_inventory("root", &root, &owners.state),
7210                Err(IcpEnsurePlatformError::CurrentProtocol(_))
7211            ));
7212        }
7213        owners.platform.desired.canisters[0].principal = None;
7214        std::fs::write(&path, installed).unwrap();
7215        std::fs::write(owners.root.join("pool.json"), pool).unwrap();
7216        let observed = owners
7217            .platform
7218            .observe_estate_pool_inventory("root", &root, &owners.state)
7219            .unwrap()
7220            .unwrap();
7221        assert_eq!(observed.assets.len(), 1);
7222        std::fs::remove_dir_all(&owners.root).unwrap();
7223    }
7224
7225    #[cfg(unix)]
7226    #[test]
7227    fn reinstall_inspections_require_fresh_reserve_even_with_cached_evidence() {
7228        let fixture = PoolInspectionFixture::fresh(1);
7229        let platform = &fixture.owners.platform;
7230        *platform.observation_snapshot.borrow_mut() = Some(FleetObservationSnapshot::default());
7231        platform.retain_pool_inspection(
7232            PoolInspectionAuthority {
7233                root: fixture.root_id,
7234                target: fixture.target,
7235            },
7236            RootInspectionStatus {
7237                status: canic_core::dto::canister::CanisterStatusType::Running,
7238                settings: ManagementCanisterObservationSettings {
7239                    controllers: vec![fixture.root_id],
7240                },
7241                module_hash: None,
7242                cycles: Nat::from(9_999_u64),
7243            },
7244        );
7245        let mut observation = fixture.retained_observation();
7246        PoolInspectionFixture::reserve_response(
7247            &fixture.owners.root,
7248            fixture.root_id,
7249            fixture.target,
7250            0,
7251        );
7252        assert!(matches!(fixture.inspect_retained(&mut observation, false),
7253            Err(IcpEnsurePlatformError::CurrentProtocol(current_protocol::CurrentProtocolError::Transport(
7254                CanisterProtocolError::InspectionPreflightReserve(evidence)
7255            ))) if evidence.canister_id == fixture.target && evidence.caller == fixture.root_id
7256        ));
7257        assert_eq!(platform.icp.remote_call_count(), 1);
7258        PoolInspectionFixture::reserve_response(
7259            &fixture.owners.root,
7260            fixture.root_id,
7261            fixture.target,
7262            1_000,
7263        );
7264        fixture.inspect_retained(&mut observation, false).unwrap();
7265        assert_eq!(
7266            observation.canisters["pool-000"].as_ref().unwrap().cycles,
7267            1_000
7268        );
7269        assert_eq!(platform.icp.remote_call_count(), 3);
7270        std::fs::remove_dir_all(&fixture.owners.root).unwrap();
7271    }
7272
7273    #[cfg(unix)]
7274    #[test]
7275    fn planning_observations_share_root_reads_and_expire_before_retry() {
7276        use std::sync::{Arc, Mutex};
7277        let mut fixture = PoolInspectionFixture::fresh(1);
7278        let events = Arc::new(Mutex::new(Vec::new()));
7279        let sink = Arc::clone(&events);
7280        fixture.owners.platform = fixture
7281            .owners
7282            .platform
7283            .with_observation_handler(move |event| {
7284                if event.succeeded.is_some() {
7285                    sink.lock().unwrap().push(event);
7286                }
7287            })
7288            .with_observation_delay_bounds(Duration::ZERO, Duration::ZERO);
7289        let platform = &mut fixture.owners.platform;
7290        let state = &fixture.owners.state;
7291        let root = fixture.root_id.to_text();
7292        platform
7293            .with_planning_observations(|platform| {
7294                assert!(
7295                    platform
7296                        .observe_root_management(state, &BTreeSet::new())?
7297                        .is_none()
7298                );
7299                platform.timed_observation(
7300                    FleetObservationStage::ConfiguredCanisters,
7301                    |platform| {
7302                        platform.with_observation_snapshot(|platform| {
7303                            platform.observe_configured_canisters(state)
7304                        })
7305                    },
7306                )?;
7307                assert_eq!(platform.icp.remote_call_count(), 4);
7308                platform.pace_root_owned_observation("root", 1);
7309                platform.status_optional(&root)?;
7310                assert_eq!(platform.icp.remote_call_count(), 5);
7311                Ok::<_, IcpEnsurePlatformError>(())
7312            })
7313            .unwrap();
7314        assert!(platform.observation_snapshot.borrow().is_none());
7315        let events = events.lock().unwrap();
7316        let total = events.last().unwrap();
7317        assert_eq!(total.stage, FleetObservationStage::Planning);
7318        assert_eq!(total.parent_stage, None);
7319        assert_eq!(total.remote_call_attempts, 5);
7320        assert!(total.cached_read_hits > 0);
7321        for event in &events[..events.len() - 1] {
7322            assert_eq!(event.parent_stage, Some(FleetObservationStage::Planning));
7323        }
7324        drop(events);
7325        platform
7326            .with_planning_observations(|platform| {
7327                platform.status_optional(&root)?;
7328                let desired = platform.desired.clone();
7329                platform.bind_reviewed_desired(&desired)?;
7330                platform.status_optional(&root)?;
7331                Err::<(), _>(IcpEnsurePlatformError::Arithmetic("failed planning"))
7332            })
7333            .unwrap_err();
7334        assert!(platform.observation_snapshot.borrow().is_none());
7335        assert_eq!(platform.icp.remote_call_count(), 7);
7336        platform.status_optional(&root).unwrap();
7337        assert_eq!(platform.icp.remote_call_count(), 8);
7338        std::fs::remove_dir_all(&fixture.owners.root).unwrap();
7339    }
7340
7341    #[cfg(unix)]
7342    #[test]
7343    fn pool_balance_timings_distinguish_cached_reads_and_bound_identity_lookups() {
7344        use std::sync::{Arc, Mutex};
7345        let count = super::super::bounded_observations::MAX_IN_FLIGHT * 2 + 1;
7346        let mut fixture = PoolInspectionFixture::fresh(u32::try_from(count).unwrap());
7347        let mut observation = fixture.retained_observation();
7348        let assets = &mut observation
7349            .estate_funding_domains
7350            .get_mut("root")
7351            .unwrap()
7352            .pool
7353            .as_mut()
7354            .unwrap()
7355            .assets;
7356        for asset in assets.iter_mut() {
7357            asset.lifecycle = EstatePoolAssetLifecycle::Failed;
7358        }
7359        let timings = Arc::new(Mutex::new(Vec::new()));
7360        let sink = Arc::clone(&timings);
7361        fixture.owners.platform = fixture
7362            .owners
7363            .platform
7364            .with_observation_handler(move |timing| {
7365                if timing.succeeded.is_some() {
7366                    sink.lock().unwrap().push(timing);
7367                }
7368            });
7369        let platform = &mut fixture.owners.platform;
7370        let root = fixture.root_id.to_text();
7371        platform
7372            .with_observation_snapshot(|platform| {
7373                for _ in 0..2 {
7374                    platform
7375                        .timed_observation(FleetObservationStage::PoolBalances, |platform| {
7376                            platform.refresh_pool_balances(&root, assets)
7377                        })?;
7378                }
7379                Ok(())
7380            })
7381            .unwrap();
7382        let recorded = timings.lock().unwrap();
7383        assert_eq!(recorded.len(), 2);
7384        let batches =
7385            u64::try_from(count.div_ceil(super::super::bounded_observations::MAX_IN_FLIGHT))
7386                .unwrap();
7387        assert_eq!(
7388            recorded[0].remote_call_attempts,
7389            1 + 2 * u64::try_from(count).unwrap()
7390        );
7391        assert_eq!(recorded[0].identity_lookup_attempts, batches);
7392        assert_eq!(recorded[1].remote_call_attempts, 0);
7393        assert_eq!(recorded[1].identity_lookup_attempts, batches);
7394        assert_eq!(
7395            recorded[1].cached_read_hits,
7396            u64::try_from(count).unwrap() + batches
7397        );
7398        drop(recorded);
7399        std::fs::write(
7400            fixture.owners.root.join("operator"),
7401            Principal::from_slice(&[99]).to_text(),
7402        )
7403        .unwrap();
7404        let failed = platform.with_observation_snapshot(|platform| {
7405            platform.timed_observation(FleetObservationStage::PoolBalances, |platform| {
7406                platform.refresh_pool_balances(&root, assets)
7407            })
7408        });
7409        assert!(matches!(
7410            failed,
7411            Err(IcpEnsurePlatformError::OperatorMismatch { .. })
7412        ));
7413        let recorded = timings.lock().unwrap();
7414        assert_eq!(recorded[2].succeeded, Some(false));
7415        assert_eq!(recorded[2].identity_lookup_attempts, 1);
7416        assert_eq!(recorded[2].remote_call_attempts, 1);
7417        drop(recorded);
7418        std::fs::remove_dir_all(&fixture.owners.root).unwrap();
7419    }
7420
7421    #[cfg(unix)]
7422    #[test]
7423    fn pool_balance_batch_rechecks_operator_before_issuing_later_reads() {
7424        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
7425        let mut fixture = PoolInspectionFixture::fresh(u32::try_from(bound + 1).unwrap());
7426        fixture.inspection_hook(&format!(
7427            "printf '%s' '{}' > '{}/operator'",
7428            Principal::from_slice(&[99]),
7429            fixture.owners.root.display()
7430        ));
7431        let mut observation = fixture.retained_observation();
7432        let assets = &mut observation
7433            .estate_funding_domains
7434            .get_mut("root")
7435            .unwrap()
7436            .pool
7437            .as_mut()
7438            .unwrap()
7439            .assets;
7440        for asset in assets.iter_mut() {
7441            asset.lifecycle = EstatePoolAssetLifecycle::Failed;
7442        }
7443        let result = fixture
7444            .owners
7445            .platform
7446            .with_observation_snapshot(|platform| {
7447                platform.refresh_pool_balances(&fixture.root_id.to_text(), assets)
7448            });
7449        assert!(matches!(
7450            result,
7451            Err(IcpEnsurePlatformError::OperatorMismatch { .. })
7452        ));
7453        assert!(assets[..bound].iter().all(|asset| asset.cycles == 1_000));
7454        assert_eq!(assets[bound].cycles, 500);
7455        assert_eq!(
7456            fixture.owners.platform.icp.remote_call_count(),
7457            1 + 2 * u64::try_from(bound).unwrap()
7458        );
7459        std::fs::remove_dir_all(&fixture.owners.root).unwrap();
7460    }
7461
7462    #[cfg(unix)]
7463    #[test]
7464    fn pool_balances_overlap_only_pending_and_failed_assets_within_the_bound() {
7465        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
7466        let count = bound * 2 + 1;
7467        let mut fixture = PoolInspectionFixture::fresh(u32::try_from(count + 1).unwrap());
7468        let path = &fixture.owners.root;
7469        fixture.inspection_hook(&format!(
7470            r#"
7471cd '{}'
7472printf 'start\n' >> events
7473started=$(grep -c '^start$' events)
7474goal=$(( (started + {bound} - 1) / {bound} * {bound} ))
7475[ "$goal" -le {count} ] || goal={count}
7476attempts=0
7477while [ "$(grep -c '^start$' events)" -lt "$goal" ]; do
7478    attempts=$((attempts + 1))
7479    [ "$attempts" -lt 500 ] || exit 1
7480    sleep 0.01
7481done
7482printf 'finish\n' >> events
7483"#,
7484            path.display()
7485        ));
7486        let mut observed = fixture.retained_observation();
7487        let assets = &mut observed
7488            .estate_funding_domains
7489            .get_mut("root")
7490            .unwrap()
7491            .pool
7492            .as_mut()
7493            .unwrap()
7494            .assets;
7495        for (index, asset) in assets.iter_mut().take(count).enumerate() {
7496            asset.lifecycle = if index % 2 == 0 {
7497                EstatePoolAssetLifecycle::Failed
7498            } else {
7499                EstatePoolAssetLifecycle::PendingReset
7500            };
7501        }
7502        fixture
7503            .owners
7504            .platform
7505            .with_observation_snapshot(|platform| {
7506                platform.refresh_pool_balances(&fixture.root_id.to_text(), assets)
7507            })
7508            .unwrap();
7509        assert!(assets[..count].iter().all(|asset| asset.cycles == 1_000));
7510        assert_eq!(assets[count].cycles, 500);
7511        let mut active = 0;
7512        let mut maximum = 0;
7513        for event in std::fs::read_to_string(path.join("events"))
7514            .unwrap()
7515            .lines()
7516        {
7517            match event {
7518                "start" => {
7519                    active += 1;
7520                    maximum = maximum.max(active);
7521                }
7522                "finish" => {
7523                    assert!(active > 0);
7524                    active -= 1;
7525                }
7526                _ => panic!("unknown inspection event"),
7527            }
7528            assert!(active <= bound);
7529        }
7530        assert_eq!((active, maximum), (0, bound));
7531        assert_eq!(
7532            fixture.owners.platform.icp.remote_call_count(),
7533            2 * u64::try_from(count).unwrap() + 1
7534        );
7535        std::fs::remove_dir_all(path).unwrap();
7536    }
7537
7538    #[cfg(unix)]
7539    #[test]
7540    fn pool_balances_drain_failed_batch_preserve_inventory_and_retry_fresh() {
7541        let mut fixture = PoolInspectionFixture::fresh(5);
7542        let path = &fixture.owners.root;
7543        PoolInspectionFixture::fresh_response(path, vec![Principal::from_slice(&[99])], None, 900);
7544        std::fs::copy(path.join("inspection.json"), path.join("pool-000.response")).unwrap();
7545        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 1_000);
7546        std::fs::write(path.join("pool-001.fail"), []).unwrap();
7547        fixture.route_inspection_targets();
7548        let mut observed = fixture.retained_observation();
7549        let assets = &mut observed
7550            .estate_funding_domains
7551            .get_mut("root")
7552            .unwrap()
7553            .pool
7554            .as_mut()
7555            .unwrap()
7556            .assets;
7557        for asset in assets.iter_mut() {
7558            asset.lifecycle = EstatePoolAssetLifecycle::Failed;
7559        }
7560        let unchanged = assets.clone();
7561        let failed = fixture
7562            .owners
7563            .platform
7564            .with_observation_snapshot(|platform| {
7565                platform.refresh_pool_balances(&fixture.root_id.to_text(), assets)
7566            });
7567        assert!(
7568            matches!(failed, Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { canister, field: "exact Root controllers" }) if canister == unchanged[0].principal)
7569        );
7570        assert_eq!(*assets, unchanged);
7571        let events = std::fs::read_to_string(path.join("events")).unwrap();
7572        for index in 0..4 {
7573            assert!(events.contains(&format!("finish:pool-{index:03}")));
7574        }
7575        assert!(!events.contains("pool-004"));
7576        assert!(events.find("finish:pool-001").unwrap() < events.find("finish:pool-000").unwrap());
7577        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 9);
7578        std::fs::remove_file(path.join("pool-000.response")).unwrap();
7579        std::fs::remove_file(path.join("pool-001.fail")).unwrap();
7580        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 2_000);
7581        fixture
7582            .owners
7583            .platform
7584            .with_observation_snapshot(|platform| {
7585                platform.refresh_pool_balances(&fixture.root_id.to_text(), assets)
7586            })
7587            .unwrap();
7588        assert!(assets.iter().all(|asset| asset.cycles == 2_000));
7589        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 20);
7590        assert!(
7591            fixture
7592                .owners
7593                .platform
7594                .observation_snapshot
7595                .borrow()
7596                .is_none()
7597        );
7598        std::fs::remove_dir_all(path).unwrap();
7599    }
7600
7601    #[cfg(unix)]
7602    #[test]
7603    fn configured_pool_inspections_overlap_within_the_bound_and_drain_partial_batches() {
7604        let bound = crate::fleet_ensure::ops::bounded_observations::MAX_IN_FLIGHT;
7605        let pool_count = bound * 2 + 1;
7606        let mut fixture = PoolInspectionFixture::fresh(u32::try_from(pool_count).unwrap());
7607        let path = &fixture.owners.root;
7608        fixture.inspection_hook(&format!(
7609            r#"
7610cd '{}'
7611printf 'start\n' >> events
7612started=$(grep -c '^start$' events)
7613goal=$(( (started + {bound} - 1) / {bound} * {bound} ))
7614[ "$goal" -le {pool_count} ] || goal={pool_count}
7615attempts=0
7616while [ "$(grep -c '^start$' events)" -lt "$goal" ]; do
7617    attempts=$((attempts + 1))
7618    [ "$attempts" -lt 500 ] || exit 1
7619    sleep 0.01
7620done
7621printf 'finish\n' >> events
7622"#,
7623            path.display()
7624        ));
7625        let observed = fixture
7626            .owners
7627            .platform
7628            .with_observation_snapshot(|platform| {
7629                platform.observe_configured_canisters(&fixture.owners.state)
7630            })
7631            .unwrap();
7632        assert_eq!(
7633            observed.len(),
7634            fixture.owners.platform.desired.canisters.len()
7635        );
7636        let events = std::fs::read_to_string(path.join("events")).unwrap();
7637        let mut active = 0;
7638        let mut maximum = 0;
7639        for event in events.lines() {
7640            match event {
7641                "start" => {
7642                    active += 1;
7643                    maximum = maximum.max(active);
7644                }
7645                "finish" => {
7646                    assert!(active > 0);
7647                    active -= 1;
7648                }
7649                _ => panic!("unknown fixture event"),
7650            }
7651            assert!(active <= bound);
7652        }
7653        assert_eq!(active, 0);
7654        assert_eq!(maximum, bound);
7655        assert_eq!(
7656            fixture.owners.platform.icp.remote_call_count(),
7657            2 * u64::try_from(pool_count).unwrap() + 2
7658        );
7659        assert!(
7660            fixture
7661                .owners
7662                .platform
7663                .observation_snapshot
7664                .borrow()
7665                .is_none()
7666        );
7667        std::fs::remove_dir_all(path).unwrap();
7668    }
7669
7670    #[cfg(unix)]
7671    #[test]
7672    fn pool_batch_keeps_consumer_error_before_later_transport_error_and_retries_fresh() {
7673        let mut fixture = PoolInspectionFixture::fresh(5);
7674        let path = &fixture.owners.root;
7675        PoolInspectionFixture::fresh_response(
7676            path,
7677            vec![fixture.root_id],
7678            Some(vec![1; 32]),
7679            1_000,
7680        );
7681        std::fs::copy(path.join("inspection.json"), path.join("pool-000.response")).unwrap();
7682        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 1_000);
7683        std::fs::write(path.join("pool-001.fail"), []).unwrap();
7684        fixture.route_inspection_targets();
7685        let failed = fixture
7686            .owners
7687            .platform
7688            .with_observation_snapshot(|platform| {
7689                platform.observe_configured_canisters(&fixture.owners.state)
7690            });
7691        assert!(
7692            matches!(failed, Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { canister, field: "module-free pool asset" }) if canister == "pool-000")
7693        );
7694        let events = std::fs::read_to_string(path.join("events")).unwrap();
7695        for index in 0..4 {
7696            assert!(events.contains(&format!("finish:pool-{index:03}")));
7697        }
7698        assert!(!events.contains("pool-004"));
7699        assert!(events.find("finish:pool-001").unwrap() < events.find("finish:pool-000").unwrap());
7700        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 10);
7701        assert!(
7702            fixture
7703                .owners
7704                .platform
7705                .observation_snapshot
7706                .borrow()
7707                .is_none()
7708        );
7709        std::fs::remove_file(path.join("pool-000.response")).unwrap();
7710        std::fs::remove_file(path.join("pool-001.fail")).unwrap();
7711        let observed = fixture
7712            .owners
7713            .platform
7714            .with_observation_snapshot(|platform| {
7715                platform.observe_configured_canisters(&fixture.owners.state)
7716            })
7717            .unwrap();
7718        assert_eq!(observed.len(), 6);
7719        assert_eq!(fixture.owners.platform.icp.remote_call_count(), 22);
7720        std::fs::remove_dir_all(path).unwrap();
7721    }
7722
7723    #[cfg(unix)]
7724    #[test]
7725    fn pool_batch_preserves_preparation_and_transport_error_precedence() {
7726        for invalid_index in [1, 2] {
7727            let mut fixture = PoolInspectionFixture::fresh(5);
7728            let path = &fixture.owners.root;
7729            // Index zero is the Root; reject authority for exactly one pool request.
7730            fixture.owners.platform.desired.canisters[invalid_index].parent =
7731                Some("missing-root".into());
7732            fixture.route_inspection_targets();
7733            for index in 0..4 {
7734                std::fs::write(path.join(format!("pool-{index:03}.fail")), []).unwrap();
7735            }
7736            let failed = fixture
7737                .owners
7738                .platform
7739                .with_observation_snapshot(|platform| {
7740                    platform.observe_configured_canisters(&fixture.owners.state)
7741                });
7742            if invalid_index == 1 {
7743                assert!(matches!(
7744                    failed,
7745                    Err(IcpEnsurePlatformError::CurrentProtocol(
7746                        current_protocol::CurrentProtocolError::Configuration(_)
7747                    ))
7748                ));
7749            } else {
7750                assert!(matches!(
7751                    failed,
7752                    Err(IcpEnsurePlatformError::CurrentProtocol(
7753                        current_protocol::CurrentProtocolError::Transport(_)
7754                    ))
7755                ));
7756            }
7757            let events = std::fs::read_to_string(path.join("events")).unwrap();
7758            assert_eq!(
7759                events
7760                    .lines()
7761                    .filter(|line| line.starts_with("finish:"))
7762                    .count(),
7763                3
7764            );
7765            assert!(!events.contains(&format!("pool-{:03}", invalid_index - 1)));
7766            assert!(!events.contains("pool-004"));
7767            assert_eq!(fixture.owners.platform.icp.remote_call_count(), 8);
7768            assert!(
7769                fixture
7770                    .owners
7771                    .platform
7772                    .observation_snapshot
7773                    .borrow()
7774                    .is_none()
7775            );
7776            std::fs::remove_dir_all(path).unwrap();
7777        }
7778    }
7779
7780    #[cfg(unix)]
7781    #[test]
7782    fn pool_batch_benchmark_matches_serial_observation_with_bounded_reads() {
7783        for pool_count in [1, 14, 27] {
7784            let mut serial_observed = None;
7785            for concurrent in [false, true] {
7786                let mut fixture = PoolInspectionFixture::fresh(pool_count);
7787                fixture.inspection_hook("sleep 0.02");
7788                let observed = fixture.owners.platform.with_observation_snapshot(|platform| {
7789                    let started = std::time::Instant::now();
7790                    let observed = if concurrent {
7791                        platform.observe_configured_canisters(&fixture.owners.state)?
7792                    } else {
7793                        platform.desired.canisters.iter().map(|configured| {
7794                            let principal = platform.current_principal(&fixture.owners.state, &configured.name).unwrap();
7795                            platform.observe_configured_canister(configured, principal, &fixture.owners.state).map(|live| (configured.name.clone(), live))
7796                        }).collect::<Result<BTreeMap<_, _>, _>>()?
7797                    };
7798                    let configured_ms = started.elapsed().as_millis();
7799                    let configured_calls = platform.icp.remote_call_count();
7800                    for live in observed.values().filter_map(Option::as_ref).filter(|live| live.principal != fixture.root_id.to_text()) {
7801                        assert_eq!(platform.inspect_pool_balance("pool", &fixture.root_id.to_text(), &live.principal, InspectedModule::Any)?, live.cycles);
7802                    }
7803                    assert_eq!(platform.icp.remote_call_count(), configured_calls);
7804                    assert_eq!(configured_calls, 2 * u64::from(pool_count) + 2);
7805                    println!("pool_batch_benchmark assets={pool_count} concurrent={concurrent} configured_ms={configured_ms} configured_calls={configured_calls} extra_balance_calls=0 latency_ms=20");
7806                    Ok(observed)
7807                }).unwrap();
7808                if let Some(serial) = &serial_observed {
7809                    assert_eq!(&observed, serial);
7810                } else {
7811                    serial_observed = Some(observed);
7812                }
7813                std::fs::remove_dir_all(&fixture.owners.root).unwrap();
7814            }
7815        }
7816    }
7817
7818    #[cfg(unix)]
7819    #[test]
7820    fn fresh_pool_observation_shares_inspections_with_balance_review() {
7821        let mut redundant_calls = 0;
7822        for pool_count in [1, 14, 27] {
7823            let mut fixture = PoolInspectionFixture::fresh(pool_count);
7824            let root = fixture.root_id.to_text();
7825            let path = &fixture.owners.root;
7826            fixture.owners.platform.with_observation_snapshot(|platform| {
7827                let started = std::time::Instant::now();
7828                let observed = platform.observe_configured_canisters(&fixture.owners.state)?;
7829                let configured_elapsed = started.elapsed();
7830                let configured_calls = platform.icp.remote_call_count();
7831                assert_eq!(observed.len(), usize::try_from(pool_count).unwrap() + 1);
7832                assert_eq!(configured_calls, 2 * u64::from(pool_count) + 2);
7833                let started = std::time::Instant::now();
7834                for live in observed.values().filter_map(Option::as_ref).filter(|live| live.principal != root) {
7835                    assert_eq!(live.cycles, 1_000);
7836                    assert_eq!(platform.inspect_pool_balance("pool", &root, &live.principal, InspectedModule::Any)?, 1_000);
7837                }
7838                let balance_calls = platform.icp.remote_call_count() - configured_calls;
7839                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());
7840                redundant_calls += balance_calls;
7841                Ok(())
7842            }).unwrap();
7843            PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 900);
7844            let configured = &fixture.owners.platform.desired.canisters[1];
7845            assert_eq!(
7846                fixture
7847                    .owners
7848                    .platform
7849                    .inspect_pending_pool_balance(
7850                        configured,
7851                        &fixture.target.to_text(),
7852                        &fixture.owners.state
7853                    )
7854                    .unwrap(),
7855                900
7856            );
7857            std::fs::remove_dir_all(path).unwrap();
7858        }
7859        assert_eq!(redundant_calls, 0);
7860    }
7861
7862    #[cfg(unix)]
7863    #[test]
7864    fn fresh_pool_inspection_preserves_consumer_authority_and_failed_scope_expiry() {
7865        let mut fixture = PoolInspectionFixture::fresh(1);
7866        let root = fixture.root_id.to_text();
7867        let target = fixture.target.to_text();
7868        let operator = Principal::from_text(&fixture.owners.platform.desired.operator).unwrap();
7869        let configured = fixture.owners.platform.desired.canisters[1].clone();
7870        let path = &fixture.owners.root;
7871        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id, operator], None, 1_000);
7872        let failed = fixture
7873            .owners
7874            .platform
7875            .with_observation_snapshot(|platform| {
7876                assert_eq!(
7877                    platform.inspect_pending_pool_balance(
7878                        &configured,
7879                        &target,
7880                        &fixture.owners.state
7881                    )?,
7882                    1_000
7883                );
7884                let calls = platform.icp.remote_call_count();
7885                assert!(matches!(
7886                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any),
7887                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
7888                        field: "exact Root controllers",
7889                        ..
7890                    })
7891                ));
7892                assert_eq!(platform.icp.remote_call_count(), calls);
7893                std::fs::write(path.join("operator"), Principal::from_slice(&[8]).to_text())
7894                    .unwrap();
7895                assert!(matches!(
7896                    platform.inspect_pending_pool_balance(
7897                        &configured,
7898                        &target,
7899                        &fixture.owners.state
7900                    ),
7901                    Err(IcpEnsurePlatformError::OperatorMismatch { .. })
7902                ));
7903                Err::<(), _>(IcpEnsurePlatformError::Arithmetic(
7904                    "injected observation failure",
7905                ))
7906            });
7907        assert!(matches!(failed, Err(IcpEnsurePlatformError::Arithmetic(_))));
7908        assert!(
7909            fixture
7910                .owners
7911                .platform
7912                .observation_snapshot
7913                .borrow()
7914                .is_none()
7915        );
7916        std::fs::write(path.join("operator"), operator.to_text()).unwrap();
7917        PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], Some(vec![1; 32]), 900);
7918        fixture
7919            .owners
7920            .platform
7921            .with_observation_snapshot(|platform| {
7922                assert_eq!(
7923                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
7924                    900
7925                );
7926                assert!(matches!(
7927                    platform.inspect_pending_pool_balance(
7928                        &configured,
7929                        &target,
7930                        &fixture.owners.state
7931                    ),
7932                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
7933                        field: "module-free pool asset",
7934                        ..
7935                    })
7936                ));
7937                Ok(())
7938            })
7939            .unwrap();
7940        std::fs::remove_dir_all(path).unwrap();
7941    }
7942
7943    #[cfg(unix)]
7944    #[test]
7945    fn fresh_pool_inspection_retries_failures_and_rechecks_root_authority_on_cache_hits() {
7946        let mut fixture = PoolInspectionFixture::fresh(1);
7947        let target = fixture.target.to_text();
7948        let root = fixture.root_id.to_text();
7949        let configured = fixture.owners.platform.desired.canisters[1].clone();
7950        let path = &fixture.owners.root;
7951        fixture
7952            .owners
7953            .platform
7954            .with_observation_snapshot(|platform| {
7955                let inspect = |platform: &IcpEnsurePlatform| {
7956                    platform.inspect_pending_pool_balance(
7957                        &configured,
7958                        &target,
7959                        &fixture.owners.state,
7960                    )
7961                };
7962                std::fs::write(path.join("fail"), []).unwrap();
7963                assert!(matches!(
7964                    inspect(platform),
7965                    Err(IcpEnsurePlatformError::CurrentProtocol(
7966                        current_protocol::CurrentProtocolError::Transport(_)
7967                    ))
7968                ));
7969                std::fs::remove_file(path.join("fail")).unwrap();
7970                PoolInspectionFixture::fresh_response(
7971                    path,
7972                    vec![Principal::from_slice(&[8])],
7973                    None,
7974                    900,
7975                );
7976                assert!(matches!(
7977                    inspect(platform),
7978                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
7979                        field: "reviewed fresh pool controllers",
7980                        ..
7981                    })
7982                ));
7983                PoolInspectionFixture::fresh_response(path, vec![fixture.root_id], None, 800);
7984                assert_eq!(inspect(platform)?, 800);
7985                assert_eq!(platform.icp.remote_call_count(), 7);
7986                // Funding accepts the same response, but fresh observation must still
7987                // establish its own exact Root artifact and controller authority.
7988                assert_eq!(
7989                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
7990                    800
7991                );
7992                std::fs::write(path.join("owner.wasm"), b"changed-root-artifact").unwrap();
7993                assert!(matches!(
7994                    inspect(platform),
7995                    Err(
7996                        IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
7997                            field: "Root module SHA-256",
7998                            ..
7999                        }
8000                    )
8001                ));
8002                std::fs::write(path.join("owner.wasm"), b"current-owner-module").unwrap();
8003                platform.desired.canisters[0].controllers =
8004                    vec![Principal::from_slice(&[8]).to_text()];
8005                assert!(matches!(
8006                    inspect(platform),
8007                    Err(
8008                        IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
8009                            field: "Root controllers",
8010                            ..
8011                        }
8012                    )
8013                ));
8014                Ok(())
8015            })
8016            .unwrap();
8017        std::fs::remove_dir_all(path).unwrap();
8018    }
8019
8020    #[cfg(unix)]
8021    #[test]
8022    fn pool_inspection_honors_bound_source_contract_without_bypassing_query_failures() {
8023        let fixture = PoolInspectionFixture::new();
8024        let root = fixture.root_id.to_text();
8025        let target = fixture.target.to_text();
8026        let path = &fixture.owners.root;
8027        let platform = &fixture.owners.platform;
8028        let candid_path = path.join("root.did");
8029        let current = std::fs::read_to_string(&candid_path).unwrap();
8030        std::fs::write(
8031            &candid_path,
8032            "service : { canic_observability : (variant { CycleBalance }) -> () query }",
8033        )
8034        .unwrap();
8035        let inspect =
8036            || platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any);
8037        assert_eq!(inspect().unwrap(), 1000);
8038        // Source observation still checks Root authority and executes protected inspection.
8039        assert_eq!(platform.icp.remote_call_count(), 2);
8040        std::fs::write(&candid_path, &current).unwrap();
8041        PoolInspectionFixture::reserve_response(path, fixture.root_id, fixture.target, 99);
8042        let before = platform.icp.remote_call_count();
8043        assert!(matches!(
8044            inspect(),
8045            Err(IcpEnsurePlatformError::CurrentProtocol(
8046                current_protocol::CurrentProtocolError::Transport(
8047                    CanisterProtocolError::InspectionPreflightReserve(_)
8048                )
8049            ))
8050        ));
8051        assert_eq!(platform.icp.remote_call_count() - before, 2);
8052        // A declared query that fails cannot fall through to the inspection update.
8053        std::fs::write(
8054            path.join(format!(
8055                "reserve-{}-{}.json",
8056                fixture.root_id, fixture.target
8057            )),
8058            "invalid response",
8059        )
8060        .unwrap();
8061        let before = platform.icp.remote_call_count();
8062        assert!(matches!(
8063            inspect(),
8064            Err(IcpEnsurePlatformError::CurrentProtocol(
8065                current_protocol::CurrentProtocolError::Transport(
8066                    CanisterProtocolError::Response { .. }
8067                )
8068            ))
8069        ));
8070        assert_eq!(platform.icp.remote_call_count() - before, 2);
8071        std::fs::write(&candid_path, "invalid Candid").unwrap();
8072        let before = platform.icp.remote_call_count();
8073        assert!(matches!(
8074            inspect(),
8075            Err(IcpEnsurePlatformError::CurrentProtocol(
8076                current_protocol::CurrentProtocolError::Transport(
8077                    CanisterProtocolError::InspectionContract { .. }
8078                )
8079            ))
8080        ));
8081        assert_eq!(platform.icp.remote_call_count() - before, 1);
8082        std::fs::write(&candid_path, current).unwrap();
8083        PoolInspectionFixture::reserve_response(path, fixture.root_id, fixture.target, 1000);
8084        let before = platform.icp.remote_call_count();
8085        assert_eq!(inspect().unwrap(), 1000);
8086        assert_eq!(platform.icp.remote_call_count() - before, 3);
8087        std::fs::remove_dir_all(path).unwrap();
8088    }
8089
8090    #[cfg(unix)]
8091    #[test]
8092    fn pool_inspection_snapshot_expires_after_success_and_failure() {
8093        let mut fixture = PoolInspectionFixture::new();
8094        let root = fixture.root_id.to_text();
8095        let target = fixture.target.to_text();
8096        let path = &fixture.owners.root;
8097        let inspect = |platform: &IcpEnsurePlatform| {
8098            platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)
8099        };
8100        fixture
8101            .owners
8102            .platform
8103            .with_observation_snapshot(|platform| {
8104                assert_eq!(inspect(platform)?, 1_000);
8105                PoolInspectionFixture::response(path, fixture.root_id, 900);
8106                assert_eq!(inspect(platform)?, 1_000);
8107                // Root status, reserve preflight and inspection serve both projections.
8108                assert_eq!(platform.icp.remote_call_count(), 3);
8109                Ok(())
8110            })
8111            .unwrap();
8112        assert!(
8113            fixture
8114                .owners
8115                .platform
8116                .observation_snapshot
8117                .borrow()
8118                .is_none()
8119        );
8120        assert_eq!(inspect(&fixture.owners.platform).unwrap(), 900);
8121        let failed: Result<(), IcpEnsurePlatformError> = fixture
8122            .owners
8123            .platform
8124            .with_observation_snapshot(|platform| {
8125                assert_eq!(inspect(platform)?, 900);
8126                PoolInspectionFixture::response(path, fixture.root_id, 800);
8127                Err(IcpEnsurePlatformError::Arithmetic(
8128                    "injected observation failure",
8129                ))
8130            });
8131        assert!(matches!(failed, Err(IcpEnsurePlatformError::Arithmetic(_))));
8132        assert!(
8133            fixture
8134                .owners
8135                .platform
8136                .observation_snapshot
8137                .borrow()
8138                .is_none()
8139        );
8140        assert_eq!(inspect(&fixture.owners.platform).unwrap(), 800);
8141        fixture
8142            .owners
8143            .platform
8144            .with_observation_snapshot(|platform| {
8145                assert_eq!(inspect(platform)?, 800);
8146                Ok(())
8147            })
8148            .unwrap();
8149        std::fs::remove_dir_all(path).unwrap();
8150    }
8151
8152    #[cfg(unix)]
8153    #[test]
8154    fn pool_inspection_snapshot_binds_root_target_module_and_operator() {
8155        let mut fixture = PoolInspectionFixture::new();
8156        let root = fixture.root_id.to_text();
8157        let target = fixture.target.to_text();
8158        let other_root = Principal::from_slice(&[3]);
8159        let other_target = Principal::from_slice(&[5]).to_text();
8160        let path = &fixture.owners.root;
8161        fixture
8162            .owners
8163            .platform
8164            .with_observation_snapshot(|platform| {
8165                assert_eq!(
8166                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
8167                    1_000
8168                );
8169                assert!(matches!(
8170                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Empty),
8171                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
8172                        field: "module-free pool asset",
8173                        ..
8174                    })
8175                ));
8176                PoolInspectionFixture::response(path, fixture.root_id, 900);
8177                assert_eq!(
8178                    platform.inspect_pool_balance(
8179                        "other asset",
8180                        &root,
8181                        &other_target,
8182                        InspectedModule::Any
8183                    )?,
8184                    900
8185                );
8186                PoolInspectionFixture::response(path, other_root, 800);
8187                assert_eq!(
8188                    platform.inspect_pool_balance(
8189                        "other Root",
8190                        &other_root.to_text(),
8191                        &target,
8192                        InspectedModule::Any
8193                    )?,
8194                    800
8195                );
8196                assert_eq!(
8197                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
8198                    1_000
8199                );
8200                // Cached data cannot authorize an operator selected after the first inspection.
8201                std::fs::write(path.join("operator"), other_root.to_text()).unwrap();
8202                assert!(matches!(
8203                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any),
8204                    Err(IcpEnsurePlatformError::OperatorMismatch { .. })
8205                ));
8206                Ok(())
8207            })
8208            .unwrap();
8209        std::fs::remove_dir_all(path).unwrap();
8210    }
8211
8212    #[cfg(unix)]
8213    #[test]
8214    fn pool_inspection_snapshot_does_not_retain_transport_or_authority_failures() {
8215        let mut fixture = PoolInspectionFixture::new();
8216        let root = fixture.root_id.to_text();
8217        let target = fixture.target.to_text();
8218        let path = &fixture.owners.root;
8219        fixture
8220            .owners
8221            .platform
8222            .with_observation_snapshot(|platform| {
8223                std::fs::write(path.join("fail"), []).unwrap();
8224                assert!(matches!(
8225                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any),
8226                    Err(IcpEnsurePlatformError::CurrentProtocol(
8227                        current_protocol::CurrentProtocolError::Transport(_)
8228                    ))
8229                ));
8230                std::fs::remove_file(path.join("fail")).unwrap();
8231                PoolInspectionFixture::response(path, Principal::from_slice(&[3]), 900);
8232                assert!(matches!(
8233                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any),
8234                    Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
8235                        field: "exact Root controllers",
8236                        ..
8237                    })
8238                ));
8239                PoolInspectionFixture::response(path, fixture.root_id, 800);
8240                assert_eq!(
8241                    platform.inspect_pool_balance("pool", &root, &target, InspectedModule::Any)?,
8242                    800
8243                );
8244                assert_eq!(platform.icp.remote_call_count(), 7);
8245                Ok(())
8246            })
8247            .unwrap();
8248        std::fs::remove_dir_all(path).unwrap();
8249    }
8250
8251    #[cfg(unix)]
8252    #[test]
8253    #[expect(
8254        clippy::too_many_lines,
8255        reason = "one measured transport fixture keeps equivalent retained inventories and call accounting together"
8256    )]
8257    fn configured_observation_benchmark_retains_snapshot_call_bound() {
8258        use std::{
8259            fs,
8260            os::unix::fs::PermissionsExt,
8261            sync::{Arc, Mutex},
8262        };
8263        for pool_count in [0_u32, 8, 24, 96] {
8264            let mut fixture = ProtocolOwnersFixture::new();
8265            let root_pid = Principal::from_slice(&[2]);
8266            for (index, canister) in fixture.platform.desired.canisters.iter_mut().enumerate() {
8267                let pid = Principal::from_slice(&[u8::try_from(index + 1).unwrap()]).to_text();
8268                let mut status: serde_json::Value = serde_json::from_slice(
8269                    &fs::read(fixture.root.join(format!("{}.json", canister.name))).unwrap(),
8270                )
8271                .unwrap();
8272                status["id"] = serde_json::Value::String(pid.clone());
8273                fs::write(fixture.root.join(format!("{pid}.json")), status.to_string()).unwrap();
8274                canister.principal = Some(pid);
8275            }
8276            fixture.platform.desired.protocol =
8277                Some(crate::fleet_ensure::model::DesiredFleetProtocol {
8278                    app_config: "unused.toml".to_string(),
8279                    component_group_placements: Vec::new(),
8280                    root_candid: "root.did".to_string(),
8281                    coordinator_candid: "unused.did".to_string(),
8282                    store_candid: "unused.did".to_string(),
8283                });
8284            fs::write(fixture.root.join("root.did"), "service : {}").unwrap();
8285            let mut entries = Vec::new();
8286            for index in 0..pool_count {
8287                let pid = Principal::from_slice(&[u8::try_from(index + 10).unwrap()]);
8288                let mut configured = fixture.platform.desired.canisters[0].clone();
8289                configured.name = format!("pool-{index:03}");
8290                configured.kind = DesiredCanisterKind::Pool;
8291                configured.principal = Some(pid.to_text());
8292                configured.parent = Some("root".to_string());
8293                fixture.platform.desired.canisters.push(configured);
8294                entries.push(CanisterPoolAsset {
8295                    canister_id: pid,
8296                    creation_receipt: None,
8297                    cycles: Cycles::new(1_000_000_000_000),
8298                    origin: CanisterPoolAssetOrigin::Imported,
8299                    status: CanisterPoolAssetStatus::Ready,
8300                    added_at_ns: 1,
8301                    updated_at_ns: 1,
8302                });
8303            }
8304            let page = CanisterPoolResponse {
8305                config: canic_core::ids::FleetSubnetCanisterPoolConfig {
8306                    minimum_size: 5,
8307                    maximum_size: 128,
8308                    canister_cycles: Cycles::new(1_000_000_000_000),
8309                    creation_execution_margin: Cycles::new(1_000_000_000),
8310                },
8311                tracked: pool_count,
8312                store: 0,
8313                store_deletion_pending: 0,
8314                pooled: pool_count,
8315                workload: 0,
8316                surplus: 0,
8317                ready: pool_count,
8318                pending_reset: 0,
8319                claimed: 0,
8320                recycling: 0,
8321                handing_off: 0,
8322                failed: 0,
8323                completed_handoffs: 0,
8324                pending_creation: None,
8325                pending_handoff: None,
8326                entries,
8327                next_start_after: None,
8328            };
8329            let response = Ok::<_, canic_core::dto::error::Error>(RootPoolStatusResponse::Pool(
8330                Box::new(page),
8331            ));
8332            fs::write(fixture.root.join("pool.json"), serde_json::json!({"response_bytes": hex_bytes(candid::encode_one(response).unwrap())}).to_string()).unwrap();
8333            let executable = fixture.root.join("icp");
8334            fs::write(&executable, crate::test_support::tool_script(&format!(
8335                "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp @ICP_VERSION@'; 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",
8336                fixture.root.display(), fixture.root.display()))).unwrap();
8337            fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
8338            let timings = Arc::new(Mutex::new(Vec::new()));
8339            let sink = Arc::clone(&timings);
8340            fixture.platform = fixture.platform.with_observation_handler(move |timing| {
8341                if timing.succeeded.is_some() {
8342                    sink.lock().unwrap().push(timing);
8343                }
8344            });
8345            let observed = fixture
8346                .platform
8347                .with_observation_snapshot(|platform| {
8348                    platform
8349                        .timed_observation(FleetObservationStage::ConfiguredCanisters, |platform| {
8350                            platform.observe_configured_canisters(&fixture.state)
8351                        })
8352                })
8353                .unwrap();
8354            assert_eq!(observed.len(), usize::try_from(pool_count).unwrap() + 3);
8355            assert!(observed.values().all(Option::is_some));
8356            assert_eq!(
8357                observed["root"].as_ref().unwrap().principal,
8358                root_pid.to_text()
8359            );
8360            let timing = timings.lock().unwrap().pop().unwrap();
8361            assert_eq!(timing.remote_call_attempts, 3 + u64::from(pool_count > 0));
8362            assert_eq!(timing.succeeded, Some(true));
8363            println!(
8364                "configured_observation pool_assets={pool_count} elapsed_ms={} calls={}",
8365                timing.elapsed_millis, timing.remote_call_attempts
8366            );
8367            fs::remove_dir_all(fixture.root).unwrap();
8368        }
8369    }
8370
8371    #[cfg(unix)]
8372    #[test]
8373    fn configured_observation_failure_drains_batch_and_keeps_first_error() {
8374        use std::{
8375            fs,
8376            sync::{Arc, Mutex},
8377        };
8378        let mut fixture = ProtocolOwnersFixture::new();
8379        let timings = Arc::new(Mutex::new(Vec::new()));
8380        let sink = Arc::clone(&timings);
8381        fixture.platform = fixture.platform.with_observation_handler(move |timing| {
8382            if timing.succeeded.is_some() {
8383                sink.lock().unwrap().push(timing);
8384            }
8385        });
8386        let mut first: serde_json::Value =
8387            serde_json::from_slice(&fs::read(fixture.root.join("coordinator.json")).unwrap())
8388                .unwrap();
8389        first["id"] = serde_json::json!("different-principal");
8390        fs::write(fixture.root.join("coordinator.json"), first.to_string()).unwrap();
8391        fs::remove_file(fixture.root.join("root.json")).unwrap();
8392        let failure = fixture.platform.with_observation_snapshot(|platform| {
8393            platform.timed_observation(FleetObservationStage::ConfiguredCanisters, |platform| {
8394                platform.observe_configured_canisters(&fixture.state)
8395            })
8396        });
8397        assert!(
8398            matches!(failure, Err(IcpEnsurePlatformError::StatusIdentityMismatch { expected, .. }) if expected == "coordinator")
8399        );
8400        let timing = timings.lock().unwrap().pop().unwrap();
8401        assert_eq!(timing.succeeded, Some(false));
8402        assert_eq!(timing.remote_call_attempts, 3);
8403        assert!(fixture.platform.observation_snapshot.borrow().is_none());
8404        fixture.status("coordinator", "Running", true);
8405        fixture.status("root", "Running", true);
8406        assert_eq!(
8407            fixture
8408                .platform
8409                .with_observation_snapshot(
8410                    |platform| platform.observe_configured_canisters(&fixture.state)
8411                )
8412                .unwrap()
8413                .len(),
8414            3
8415        );
8416        assert_eq!(fixture.platform.icp.remote_call_count(), 6);
8417        fs::remove_dir_all(fixture.root).unwrap();
8418    }
8419
8420    #[cfg(unix)]
8421    #[test]
8422    fn reinstall_authorities_overlap_after_root_and_drain_partial_batches() {
8423        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
8424        let independent = bound * 2 + 1;
8425        let mut fixture = ProtocolOwnersFixture::synchronized_reinstall_owners(independent);
8426        let timings = Arc::new(Mutex::new(Vec::new()));
8427        let sink = Arc::clone(&timings);
8428        fixture.platform = fixture.platform.with_observation_handler(move |timing| {
8429            sink.lock().unwrap().push(timing);
8430        });
8431        let observed = fixture
8432            .platform
8433            .reinstall_authorities(&fixture.state)
8434            .unwrap()
8435            .unwrap();
8436        assert_eq!(observed.len(), independent + 1);
8437        let recorded = timings.lock().unwrap();
8438        let started = recorded.first().unwrap();
8439        let finished = recorded.last().unwrap();
8440        assert_eq!(started.span_id, finished.span_id);
8441        assert_eq!(started.succeeded, None);
8442        assert_eq!(finished.succeeded, Some(true));
8443        assert_eq!(finished.stage, FleetObservationStage::RootManagement);
8444        assert_eq!(finished.parent_span_id, None);
8445        assert_eq!(
8446            finished.remote_call_attempts,
8447            fixture.platform.icp.remote_call_count()
8448        );
8449        assert!(
8450            recorded
8451                .iter()
8452                .any(|timing| timing.parent_span_id == Some(started.span_id))
8453        );
8454        drop(recorded);
8455        assert!(
8456            observed
8457                .values()
8458                .all(|a| a.live.canister_version == Some(7))
8459        );
8460        let (mut active, mut maximum, mut finished) = (0, 0, 0);
8461        for event in std::fs::read_to_string(fixture.root.join("events"))
8462            .unwrap()
8463            .lines()
8464        {
8465            let (kind, _) = event.split_once(':').unwrap();
8466            match kind {
8467                "start" => {
8468                    active += 1;
8469                    maximum = maximum.max(active);
8470                }
8471                "finish" => {
8472                    assert!(active > 0);
8473                    active -= 1;
8474                    finished += 1;
8475                }
8476                _ => panic!("unknown observation event"),
8477            }
8478            assert!(active <= bound);
8479        }
8480        assert_eq!((active, maximum, finished), (0, bound, independent));
8481        assert_eq!(
8482            fixture.platform.icp.remote_call_count(),
8483            u64::try_from(independent + 2).unwrap()
8484        );
8485        assert_eq!(
8486            std::fs::read_to_string(fixture.root.join("prerequisites"))
8487                .unwrap()
8488                .lines()
8489                .collect::<Vec<_>>(),
8490            ["root"]
8491        );
8492        std::fs::remove_dir_all(fixture.root).unwrap();
8493    }
8494
8495    #[cfg(unix)]
8496    #[test]
8497    fn reinstall_authorities_drain_failed_batch_keep_error_order_and_retry_fresh() {
8498        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
8499        let mut fixture = ProtocolOwnersFixture::synchronized_reinstall_owners(bound + 1);
8500        let timings = Arc::new(Mutex::new(Vec::new()));
8501        let sink = Arc::clone(&timings);
8502        fixture.platform = fixture.platform.with_observation_handler(move |timing| {
8503            sink.lock().unwrap().push(timing);
8504        });
8505        let original = std::fs::read(fixture.root.join("coordinator.json")).unwrap();
8506        std::fs::copy(
8507            fixture.root.join("store.json"),
8508            fixture.root.join("coordinator.json"),
8509        )
8510        .unwrap();
8511        std::fs::rename(
8512            fixture.root.join("store.json"),
8513            fixture.root.join("store.saved"),
8514        )
8515        .unwrap();
8516        std::fs::write(fixture.root.join("coordinator.delay"), []).unwrap();
8517        assert!(
8518            matches!(fixture.platform.reinstall_authorities(&fixture.state),
8519            Err(IcpEnsurePlatformError::StatusIdentityMismatch { expected, .. }) if expected == "coordinator")
8520        );
8521        let finished = timings.lock().unwrap().last().unwrap().clone();
8522        assert_eq!(finished.succeeded, Some(false));
8523        assert_eq!(finished.stage, FleetObservationStage::RootManagement);
8524        assert_eq!(finished.parent_span_id, None);
8525        assert_eq!(
8526            finished.remote_call_attempts,
8527            fixture.platform.icp.remote_call_count()
8528        );
8529        let events = std::fs::read_to_string(fixture.root.join("events")).unwrap();
8530        let finished = events
8531            .lines()
8532            .filter_map(|line| line.strip_prefix("finish:"))
8533            .collect::<Vec<_>>();
8534        assert_eq!(finished.len(), bound);
8535        assert!(!finished.contains(&"owner-05"));
8536        assert!(
8537            finished.iter().position(|name| *name == "store").unwrap()
8538                < finished
8539                    .iter()
8540                    .position(|name| *name == "coordinator")
8541                    .unwrap()
8542        );
8543        assert_eq!(
8544            fixture.platform.icp.remote_call_count(),
8545            u64::try_from(bound + 2).unwrap()
8546        );
8547
8548        let mut changed: serde_json::Value = serde_json::from_slice(&original).unwrap();
8549        changed["version"] = serde_json::json!(9);
8550        std::fs::write(
8551            fixture.root.join("coordinator.json"),
8552            serde_json::to_vec(&changed).unwrap(),
8553        )
8554        .unwrap();
8555        std::fs::rename(
8556            fixture.root.join("store.saved"),
8557            fixture.root.join("store.json"),
8558        )
8559        .unwrap();
8560        std::fs::remove_file(fixture.root.join("events")).unwrap();
8561        preparation_status(&fixture, 8);
8562        let observed = fixture
8563            .platform
8564            .reinstall_authorities(&fixture.state)
8565            .unwrap()
8566            .unwrap();
8567        assert_eq!(observed["root"].live.canister_version, Some(8));
8568        assert_eq!(observed["coordinator"].live.canister_version, Some(9));
8569        assert_eq!(observed.len(), bound + 2);
8570        assert!(fixture.platform.observation_snapshot.borrow().is_none());
8571
8572        std::fs::remove_file(fixture.root.join("events")).unwrap();
8573        std::fs::remove_file(fixture.root.join("root.json")).unwrap();
8574        assert!(matches!(
8575            fixture.platform.reinstall_authorities(&fixture.state),
8576            Err(IcpEnsurePlatformError::Icp(_))
8577        ));
8578        assert!(!fixture.root.join("events").exists());
8579        std::fs::remove_dir_all(fixture.root).unwrap();
8580    }
8581
8582    #[cfg(unix)]
8583    #[test]
8584    fn reinstall_authority_pass_reads_statuses_and_refreshes_after_failure() {
8585        let mut fixture = ProtocolOwnersFixture::new();
8586        let path = &fixture.root;
8587        std::fs::write(path.join("icp"), crate::test_support::tool_script(r#"#!/bin/sh
8588if [ "$1" = '--version' ]; then echo 'icp @ICP_VERSION@'; exit 0; fi
8589while [ "$#" -gt 0 ] && [ "$1" != canister ] && [ "$1" != identity ] && [ "$1" != cycles ]; do shift; done
8590case "$1" in
8591identity) echo operator;;
8592cycles) echo '{"balance":"1000000000000 cycles"}';;
8593canister) shift; [ "$1" = status ] || exit 1; shift; printf '%s\n' "$1" >> calls; cat "$1.json";;
8594*) exit 1;;
8595esac
8596"#)).unwrap();
8597        for name in ["root", "coordinator", "store"] {
8598            let file = path.join(format!("{name}.json"));
8599            let mut status: serde_json::Value =
8600                serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
8601            status["version"] = serde_json::json!(7);
8602            std::fs::write(file, serde_json::to_vec(&status).unwrap()).unwrap();
8603        }
8604        let first = fixture
8605            .platform
8606            .reinstall_authorities(&fixture.state)
8607            .unwrap()
8608            .unwrap();
8609        assert_eq!(first.len(), 3);
8610        assert!(
8611            first
8612                .values()
8613                .all(|value| value.live.canister_version == Some(7))
8614        );
8615        let calls = std::fs::read_to_string(path.join("calls")).unwrap();
8616        eprintln!(
8617            "reinstall authority status calls: {calls:?}; remote calls: {}",
8618            fixture.platform.icp.remote_call_count()
8619        );
8620        let mut calls = calls.lines().collect::<Vec<_>>();
8621        assert_eq!(calls.first(), Some(&"root"));
8622        calls.sort_unstable();
8623        assert_eq!(calls, ["coordinator", "root", "store"]);
8624        assert_eq!(fixture.platform.icp.remote_call_count(), 4);
8625        let file = path.join("root.json");
8626        let mut status: serde_json::Value =
8627            serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
8628        status["version"] = serde_json::json!(8);
8629        status["status"] = serde_json::json!("Stopped");
8630        status["module_hash"] = serde_json::json!("ab".repeat(32));
8631        status["settings"]["controllers"] = serde_json::json!(["changed-controller"]);
8632        std::fs::write(&file, serde_json::to_vec(&status).unwrap()).unwrap();
8633        std::fs::rename(path.join("store.json"), path.join("store.saved")).unwrap();
8634        assert!(
8635            fixture
8636                .platform
8637                .reinstall_authorities(&fixture.state)
8638                .is_err()
8639        );
8640        std::fs::rename(path.join("store.saved"), path.join("store.json")).unwrap();
8641        let after = fixture
8642            .platform
8643            .reinstall_authorities(&fixture.state)
8644            .unwrap()
8645            .unwrap();
8646        let root = &after["root"].live;
8647        assert_eq!(root.canister_version, Some(8));
8648        assert_eq!(root.status, CanisterRuntimeStatus::Stopped);
8649        assert_eq!(
8650            root.module_sha256.as_deref(),
8651            Some("ab".repeat(32).as_str())
8652        );
8653        assert_eq!(root.controllers, vec!["changed-controller"]);
8654        std::fs::remove_dir_all(path).unwrap();
8655    }
8656
8657    #[cfg(unix)]
8658    #[test]
8659    #[ignore = "manual matched host-readiness latency measurement"]
8660    fn protocol_owner_readiness_matched_latency_measurement() {
8661        for count in [3_u64, 9] {
8662            let mut fixture = ProtocolOwnersFixture::new();
8663            for index in 3..count {
8664                let mut owner = fixture.platform.desired.canisters[1].clone();
8665                owner.name = format!("owner-{index}");
8666                owner.principal = Some(owner.name.clone());
8667                fixture.status(&owner.name, "Running", true);
8668                fixture.platform.desired.canisters.push(owner);
8669            }
8670            let executable = fixture.root.join("icp");
8671            let script = std::fs::read_to_string(&executable)
8672                .unwrap()
8673                .replace("\ncat ", "\nsleep 0.02\ncat ");
8674            std::fs::write(&executable, script).unwrap();
8675            for round in 0..3 {
8676                // Alternate order to avoid assigning warm-up only to one candidate.
8677                for concurrent in if round % 2 == 0 {
8678                    [false, true]
8679                } else {
8680                    [true, false]
8681                } {
8682                    let calls = fixture.platform.icp.remote_call_count();
8683                    let started = std::time::Instant::now();
8684                    let ready = if concurrent {
8685                        fixture
8686                            .platform
8687                            .current_protocol_owners_are_ready(&fixture.state)
8688                            .unwrap()
8689                    } else {
8690                        let mut ready = true;
8691                        for configured in &fixture.platform.desired.canisters {
8692                            let live = fixture
8693                                .platform
8694                                .status_optional(configured.principal.as_deref().unwrap())
8695                                .unwrap()
8696                                .unwrap();
8697                            ready &= live.status == CanisterRuntimeStatus::Running
8698                                && live.module_sha256
8699                                    == Some(
8700                                        artifact_hash(&fixture.root.join("owner.wasm")).unwrap(),
8701                                    );
8702                        }
8703                        ready
8704                    };
8705                    let elapsed_us = started.elapsed().as_micros();
8706                    let calls = fixture.platform.icp.remote_call_count() - calls;
8707                    assert!(ready);
8708                    assert_eq!(calls, count);
8709                    println!(
8710                        "protocol_owner_readiness owners={count} round={round} concurrent={concurrent} elapsed_us={elapsed_us} calls={calls} latency_ms=20"
8711                    );
8712                }
8713            }
8714            std::fs::remove_dir_all(fixture.root).unwrap();
8715        }
8716    }
8717
8718    #[cfg(unix)]
8719    #[test]
8720    fn protocol_owner_observations_overlap_within_bound_and_drain_partial_batches() {
8721        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
8722        let count = bound * 2 + 1;
8723        let fixture = ProtocolOwnersFixture::synchronized_owners(count);
8724        assert!(
8725            fixture
8726                .platform
8727                .current_protocol_owners_are_ready(&fixture.state)
8728                .unwrap()
8729        );
8730        let events = std::fs::read_to_string(fixture.root.join("events")).unwrap();
8731        let mut active = 0;
8732        let mut maximum = 0;
8733        let mut completed = 0;
8734        for event in events.lines() {
8735            if event.starts_with("start:") {
8736                active += 1;
8737                maximum = maximum.max(active);
8738            } else {
8739                assert!(event.starts_with("finish:"));
8740                assert!(active > 0);
8741                active -= 1;
8742                completed += 1;
8743            }
8744            assert!(active <= bound);
8745        }
8746        assert_eq!(active, 0);
8747        assert_eq!(maximum, bound);
8748        assert_eq!(completed, count);
8749        assert_eq!(
8750            fixture.platform.icp.remote_call_count(),
8751            u64::try_from(count).unwrap()
8752        );
8753        std::fs::remove_dir_all(fixture.root).unwrap();
8754    }
8755
8756    #[cfg(unix)]
8757    #[test]
8758    fn protocol_owner_observations_drain_failure_keep_order_and_retry_fresh() {
8759        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
8760        let count = bound + 1;
8761        let fixture = ProtocolOwnersFixture::synchronized_owners(count);
8762        let wrong = std::fs::read(fixture.root.join("store.json")).unwrap();
8763        std::fs::write(fixture.root.join("coordinator.json"), wrong).unwrap();
8764        std::fs::write(fixture.root.join("coordinator.delay"), b"").unwrap();
8765        std::fs::remove_file(fixture.root.join("root.json")).unwrap();
8766        assert!(
8767            matches!(fixture.platform.current_protocol_owners_are_ready(&fixture.state),
8768                Err(IcpEnsurePlatformError::StatusIdentityMismatch { expected, .. }) if expected == "coordinator"
8769            )
8770        );
8771        let events = std::fs::read_to_string(fixture.root.join("events")).unwrap();
8772        assert_eq!(
8773            events
8774                .lines()
8775                .filter(|event| event.starts_with("finish:"))
8776                .count(),
8777            bound
8778        );
8779        assert!(!events.contains("owner-04"));
8780        assert!(events.find("finish:root").unwrap() < events.find("finish:coordinator").unwrap());
8781        assert_eq!(
8782            fixture.platform.icp.remote_call_count(),
8783            u64::try_from(bound).unwrap()
8784        );
8785        fixture.status("coordinator", "Running", true);
8786        fixture.status("root", "Running", true);
8787        std::fs::remove_file(fixture.root.join("events")).unwrap();
8788        assert!(
8789            fixture
8790                .platform
8791                .current_protocol_owners_are_ready(&fixture.state)
8792                .unwrap()
8793        );
8794        assert_eq!(
8795            fixture.platform.icp.remote_call_count(),
8796            u64::try_from(bound + count).unwrap()
8797        );
8798        assert!(fixture.platform.observation_snapshot.borrow().is_none());
8799        std::fs::remove_dir_all(fixture.root).unwrap();
8800    }
8801
8802    #[cfg(unix)]
8803    #[test]
8804    fn protocol_owner_observations_stopped_owner_precedes_later_failure() {
8805        let bound = super::super::bounded_observations::MAX_IN_FLIGHT;
8806        let fixture = ProtocolOwnersFixture::synchronized_owners(bound + 1);
8807        fixture.status("coordinator", "Stopped", true);
8808        std::fs::remove_file(fixture.root.join("root.json")).unwrap();
8809        assert!(
8810            !fixture
8811                .platform
8812                .current_protocol_owners_are_ready(&fixture.state)
8813                .unwrap()
8814        );
8815        let events = std::fs::read_to_string(fixture.root.join("events")).unwrap();
8816        assert_eq!(
8817            events
8818                .lines()
8819                .filter(|event| event.starts_with("finish:"))
8820                .count(),
8821            bound
8822        );
8823        assert!(!events.contains("owner-04"));
8824        std::fs::remove_dir_all(fixture.root).unwrap();
8825    }
8826
8827    #[cfg(unix)]
8828    #[test]
8829    fn replay_planning_shares_configured_owner_reads_and_refreshes_next_decision() {
8830        let mut fixture = ProtocolOwnersFixture::new();
8831        let state = &fixture.state;
8832        let platform = &mut fixture.platform;
8833        platform
8834            .with_planning_observations(|platform| {
8835                platform.with_observation_snapshot(|platform| {
8836                    platform.observe_configured_canisters(state)
8837                })?;
8838                assert_eq!(platform.icp.remote_call_count(), 3);
8839                assert!(platform.current_protocol_owners_are_ready(state)?);
8840                assert_eq!(platform.icp.remote_call_count(), 3);
8841                Ok::<_, IcpEnsurePlatformError>(())
8842            })
8843            .unwrap();
8844        assert!(platform.observation_snapshot.borrow().is_none());
8845        fixture.status("store", "Stopped", true);
8846        let platform = &mut fixture.platform;
8847        let ready = platform
8848            .with_planning_observations(|platform| {
8849                platform.with_observation_snapshot(|platform| {
8850                    platform.observe_configured_canisters(state)
8851                })?;
8852                platform.current_protocol_owners_are_ready(state)
8853            })
8854            .unwrap();
8855        assert!(!ready);
8856        assert_eq!(platform.icp.remote_call_count(), 6);
8857        assert!(platform.observation_snapshot.borrow().is_none());
8858        std::fs::remove_dir_all(fixture.root).unwrap();
8859    }
8860
8861    #[cfg(unix)]
8862    #[test]
8863    fn protocol_owner_observations_read_each_owner_once_and_refresh_between_calls() {
8864        let fixture = ProtocolOwnersFixture::new();
8865        assert!(
8866            fixture
8867                .platform
8868                .current_protocol_owners_are_ready(&fixture.state)
8869                .unwrap()
8870        );
8871        let calls = std::fs::read_to_string(fixture.root.join("calls")).unwrap();
8872        let mut owners = calls.lines().collect::<Vec<_>>();
8873        owners.sort_unstable();
8874        assert_eq!(owners, ["coordinator", "root", "store"]);
8875        fixture.status("store", "Stopping", true);
8876        assert!(
8877            !fixture
8878                .platform
8879                .current_protocol_owners_are_ready(&fixture.state)
8880                .unwrap()
8881        );
8882        fixture.status("store", "Stopped", true);
8883        assert!(
8884            !fixture
8885                .platform
8886                .current_protocol_owners_are_ready(&fixture.state)
8887                .unwrap()
8888        );
8889        fixture.status("store", "Running", false);
8890        assert!(
8891            !fixture
8892                .platform
8893                .current_protocol_owners_are_ready(&fixture.state)
8894                .unwrap()
8895        );
8896        fixture.status("store", "Running", true);
8897        assert!(
8898            fixture
8899                .platform
8900                .current_protocol_owners_are_ready(&fixture.state)
8901                .unwrap()
8902        );
8903        assert_eq!(
8904            std::fs::read_to_string(fixture.root.join("calls"))
8905                .unwrap()
8906                .lines()
8907                .count(),
8908            15
8909        );
8910        std::fs::remove_dir_all(fixture.root).unwrap();
8911    }
8912
8913    #[cfg(unix)]
8914    #[test]
8915    fn protocol_owner_observations_preserve_missing_owner_and_transport_failures() {
8916        let mut fixture = ProtocolOwnersFixture::new();
8917        fixture.platform.desired.canisters[0].principal = None;
8918        assert!(
8919            !fixture
8920                .platform
8921                .current_protocol_owners_are_ready(&fixture.state)
8922                .unwrap()
8923        );
8924        std::fs::remove_file(fixture.root.join("store.json")).unwrap();
8925        assert!(matches!(
8926            fixture
8927                .platform
8928                .current_protocol_owners_are_ready(&fixture.state),
8929            Err(IcpEnsurePlatformError::Icp(_))
8930        ));
8931        fixture.platform.desired.canisters[0].principal = Some("coordinator".to_string());
8932        fixture.status("coordinator", "Running", false);
8933        assert!(matches!(
8934            fixture
8935                .platform
8936                .current_protocol_owners_are_ready(&fixture.state),
8937            Err(IcpEnsurePlatformError::Icp(_))
8938        ));
8939        std::fs::remove_dir_all(fixture.root).unwrap();
8940    }
8941
8942    #[cfg(unix)]
8943    #[test]
8944    #[expect(
8945        clippy::too_many_lines,
8946        reason = "one transport sequence proves snapshot reuse and expiry across success, failure and effects"
8947    )]
8948    fn observation_snapshot_expires_before_effects_and_after_failed_observation() {
8949        use std::{fs, os::unix::fs::PermissionsExt};
8950
8951        let root = crate::test_support::temp_dir("canic-observation-snapshot");
8952        fs::create_dir_all(&root).expect("create observation fixture");
8953        let executable = root.join("icp");
8954        let commands = root.join("commands.log");
8955        let response = root.join("status.json");
8956        let canister = "rrkah-fqaaa-aaaaa-aaaaq-cai";
8957        let operator = "rdmx6-jaaaa-aaaaa-aaadq-cai";
8958        let status = |cycles: u128| {
8959            serde_json::json!({
8960                "id": canister,
8961                "status": "Running",
8962                "settings": { "controllers": [operator] },
8963                "module_hash": null,
8964                "cycles": cycles.to_string(),
8965            })
8966            .to_string()
8967        };
8968        fs::write(&response, status(1_000)).expect("write initial live balance");
8969        fs::write(&executable, crate::test_support::tool_script(&format!(
8970            "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'icp @ICP_VERSION@'; exit 0; fi\nprintf '%s\\n' \"$*\" >> '{}'\ncat '{}'\n",
8971            commands.display(), response.display(),
8972        ))).expect("write observation transport");
8973        fs::set_permissions(&executable, fs::Permissions::from_mode(0o755))
8974            .expect("make observation transport executable");
8975        let desired = DesiredFleet {
8976            bootstrap: None,
8977            canisters: Vec::new(),
8978            cycles_ledger: "um5iw-rqaaa-aaaaq-qaaba-cai".to_string(),
8979            environment: "local".to_string(),
8980            fleet: "observation-snapshot".to_string(),
8981            ledger_fee_cycles: "100M".to_string(),
8982            management_creation_fee_cycles: "500B".to_string(),
8983            material_cycle_threshold: "1B".to_string(),
8984            maximum_observation_burn_cycles: "1B".to_string(),
8985            maximum_stalled_observations: 4,
8986            maximum_update_burn_cycles: "1B".to_string(),
8987            operator: operator.to_string(),
8988            protocol: None,
8989            protocol_steps: Vec::new(),
8990            schema_version: 1,
8991            treasury: "treasury".to_string(),
8992        };
8993        let mut platform =
8994            IcpEnsurePlatform::new(desired, executable.to_str().expect("transport path"), &root);
8995        let failed: Result<(), IcpEnsurePlatformError> =
8996            platform.with_observation_snapshot(|platform| {
8997                assert_eq!(
8998                    platform
8999                        .status_optional(canister)?
9000                        .expect("live canister")
9001                        .cycles,
9002                    1_000
9003                );
9004                fs::write(&response, status(900)).expect("advance simulated live balance");
9005                assert_eq!(
9006                    platform
9007                        .status_optional(canister)?
9008                        .expect("same snapshot")
9009                        .cycles,
9010                    1_000
9011                );
9012                Err(IcpEnsurePlatformError::Arithmetic(
9013                    "injected observation failure",
9014                ))
9015            });
9016        assert!(matches!(failed, Err(IcpEnsurePlatformError::Arithmetic(_))));
9017        assert_eq!(
9018            fs::read_to_string(&commands)
9019                .expect("first observation calls")
9020                .lines()
9021                .count(),
9022            1
9023        );
9024        assert_eq!(
9025            platform
9026                .status_optional(canister)
9027                .expect("fresh action read")
9028                .expect("live canister")
9029                .cycles,
9030            900
9031        );
9032        platform
9033            .with_observation_snapshot(|platform| {
9034                assert_eq!(
9035                    platform
9036                        .status_optional(canister)?
9037                        .expect("next observation")
9038                        .cycles,
9039                    900
9040                );
9041                fs::write(&response, status(800)).expect("advance live balance again");
9042                assert_eq!(
9043                    platform
9044                        .status_optional(canister)?
9045                        .expect("next snapshot")
9046                        .cycles,
9047                    900
9048                );
9049                Ok(())
9050            })
9051            .expect("next observation succeeds");
9052        assert_eq!(
9053            platform
9054                .status_optional(canister)
9055                .expect("post-observation action read")
9056                .expect("live canister")
9057                .cycles,
9058            800
9059        );
9060        assert_eq!(
9061            fs::read_to_string(&commands)
9062                .expect("all observation calls")
9063                .lines()
9064                .count(),
9065            4
9066        );
9067        fs::remove_dir_all(root).expect("remove observation fixture");
9068    }
9069
9070    #[test]
9071    fn protocol_observation_delay_uses_bounded_exponential_backoff() {
9072        let production_cap = MAXIMUM_PROTOCOL_OBSERVATION_DELAY;
9073        assert_eq!(
9074            protocol_observation_delay(0, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
9075            Duration::from_millis(250)
9076        );
9077        assert_eq!(
9078            protocol_observation_delay(1, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
9079            Duration::from_millis(250)
9080        );
9081        assert_eq!(
9082            protocol_observation_delay(2, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
9083            Duration::from_millis(500)
9084        );
9085        assert_eq!(
9086            protocol_observation_delay(5, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
9087            Duration::from_secs(4)
9088        );
9089        assert_eq!(
9090            protocol_observation_delay(6, INITIAL_PROTOCOL_OBSERVATION_DELAY, production_cap),
9091            Duration::from_secs(5)
9092        );
9093        assert_eq!(
9094            protocol_observation_delay(
9095                u32::MAX,
9096                INITIAL_PROTOCOL_OBSERVATION_DELAY,
9097                production_cap
9098            ),
9099            Duration::from_secs(5)
9100        );
9101        assert_eq!(
9102            protocol_observation_delay(u32::MAX, Duration::from_secs(1), Duration::from_secs(1)),
9103            Duration::from_secs(1)
9104        );
9105    }
9106
9107    #[cfg(unix)]
9108    #[test]
9109    fn icp_1_5_public_non_controller_status_is_typed_unavailable_evidence() {
9110        use std::{fs, os::unix::fs::PermissionsExt};
9111
9112        let canister = "rrkah-fqaaa-aaaaa-aaaaq-cai";
9113        let root = crate::test_support::temp_dir("canic-public-status-projection");
9114        fs::create_dir_all(&root).expect("create public status fixture");
9115        let executable = root.join("icp");
9116        let commands = root.join("commands.log");
9117        let script = crate::test_support::tool_script(&format!(
9118            "#!/bin/sh\n\
9119             if [ \"$1\" = \"--version\" ]; then echo 'icp @ICP_VERSION@'; exit 0; fi\n\
9120             printf '%s\\n' \"$*\" >> '{}'\n\
9121             printf '%s\\n' '{{\"id\":\"{canister}\",\"controllers\":[\"rdmx6-jaaaa-aaaaa-aaadq-cai\"],\"module_hash\":null}}'\n",
9122            commands.display(),
9123        ));
9124        fs::write(&executable, script).expect("write fake ICP 1.5.0");
9125        fs::set_permissions(&executable, fs::Permissions::from_mode(0o755))
9126            .expect("make fake ICP executable");
9127        let desired = DesiredFleet {
9128            bootstrap: None,
9129            canisters: Vec::new(),
9130            cycles_ledger: "um5iw-rqaaa-aaaaq-qaaba-cai".to_string(),
9131            environment: "local".to_string(),
9132            fleet: "public-status".to_string(),
9133            ledger_fee_cycles: "100M".to_string(),
9134            management_creation_fee_cycles: "500B".to_string(),
9135            material_cycle_threshold: "1B".to_string(),
9136            maximum_observation_burn_cycles: "1B".to_string(),
9137            maximum_stalled_observations: 4,
9138            maximum_update_burn_cycles: "1B".to_string(),
9139            operator: "rdmx6-jaaaa-aaaaa-aaadq-cai".to_string(),
9140            protocol: None,
9141            protocol_steps: Vec::new(),
9142            schema_version: 1,
9143            treasury: "treasury".to_string(),
9144        };
9145        let platform =
9146            IcpEnsurePlatform::new(desired, executable.to_string_lossy().as_ref(), &root);
9147
9148        assert!(matches!(
9149            platform.status_optional(canister),
9150            Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable {
9151                canister: unavailable,
9152            }) if unavailable == canister
9153        ));
9154        assert!(
9155            fs::read_to_string(commands)
9156                .expect("read fake ICP commands")
9157                .contains("canister status rrkah-fqaaa-aaaaa-aaaaq-cai --json")
9158        );
9159    }
9160
9161    #[cfg(unix)]
9162    #[test]
9163    fn versionless_icp_status_uses_exact_typed_management_version() {
9164        #[derive(CandidType)]
9165        struct CanonicalManagementCanisterStatusFixture {
9166            version: u64,
9167            module_hash: Option<Vec<u8>>,
9168        }
9169
9170        use std::{fs, os::unix::fs::PermissionsExt};
9171
9172        let root = crate::test_support::temp_dir("canic-install-version-fallback");
9173        fs::create_dir_all(&root).expect("create version fallback fixture");
9174        let executable = root.join("icp");
9175        let commands = root.join("commands.log");
9176        let canister = "rrkah-fqaaa-aaaaa-aaaaq-cai";
9177        let status_json = format!(
9178            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"}}}}"#,
9179            "11".repeat(32),
9180        );
9181        let response_bytes = candid::encode_one(CanonicalManagementCanisterStatusFixture {
9182            version: 42,
9183            module_hash: Some(vec![0x22; 32]),
9184        })
9185        .expect("encode independently modelled management status");
9186        let script = crate::test_support::tool_script(&format!(
9187            "#!/bin/sh\n\
9188             if [ \"$1\" = \"--version\" ]; then printf '%s\\n' 'icp @ICP_VERSION@'; exit 0; fi\n\
9189             printf '%s\\n' \"$*\" >> '{}'\n\
9190             case \"$*\" in\n\
9191               *\"canister status {canister}\"*) printf '%s\\n' '{}' ;;\n\
9192               *) printf '%s\\n' 'unexpected fake ICP command' >&2; exit 23 ;;\n\
9193             esac\n",
9194            commands.display(),
9195            status_json,
9196        ));
9197        fs::write(&executable, script).expect("write fake ICP 1.5.0");
9198        fs::set_permissions(&executable, fs::Permissions::from_mode(0o755))
9199            .expect("make fake ICP executable");
9200        let icp = IcpCli::new(executable.to_string_lossy(), Some("ic".to_string()));
9201
9202        let projected = icp
9203            .canister_status_report(canister)
9204            .expect("read ICP 1.5.0 status projection");
9205        assert_eq!(projected.canister_version, None);
9206        let exact = exact_install_canister_status_with(
9207            canister,
9208            projected.canister_version,
9209            projected.module_hash.map(|hash| normalize_hash(&hash)),
9210            |effective_canister_id| {
9211                assert_eq!(
9212                    effective_canister_id,
9213                    canister.parse().expect("target Principal")
9214                );
9215                candid::decode_one(&response_bytes).map_err(IcpManagementCallError::CandidResponse)
9216            },
9217        )
9218        .expect("obtain exact typed management status");
9219        assert_eq!(exact.canister_version, 42);
9220        assert_eq!(exact.module_sha256, Some("22".repeat(32)));
9221        let commands = fs::read_to_string(&commands).expect("read fake ICP commands");
9222        assert!(commands.contains("canister status rrkah-fqaaa-aaaaa-aaaaq-cai"));
9223        assert!(!commands.contains("canister call aaaaa-aa canister_status"));
9224
9225        fs::write(
9226            &executable,
9227            crate::test_support::tool_script("#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'icp @ICP_VERSION@'; exit 0; fi\necho '{\"not_response_bytes\":true}'\n"),
9228        )
9229        .expect("replace unavailable management fixture");
9230        let error = exact_install_canister_status_with(canister, None, None, |_| {
9231            Err(IcpManagementCallError::MissingEnvironment)
9232        })
9233        .expect_err("missing projected and typed version must fail closed");
9234        assert!(matches!(
9235            error,
9236            IcpEnsurePlatformError::InstallVersionProofUnavailable {
9237                canister: observed_canister,
9238                source,
9239            } if observed_canister == canister
9240                && matches!(source.as_ref(), IcpManagementCallError::MissingEnvironment)
9241        ));
9242
9243        fs::remove_dir_all(root).expect("remove version fallback fixture");
9244    }
9245
9246    #[test]
9247    fn root_owned_observation_classifies_bootstrap_and_workload_lifecycle() {
9248        assert_eq!(
9249            root_owned_lifecycle(DesiredCanisterKind::Store, &CanisterPoolAssetStatus::Store),
9250            Some(RootOwnedCanisterLifecycle::Store)
9251        );
9252        assert_eq!(
9253            root_owned_lifecycle(DesiredCanisterKind::Pool, &CanisterPoolAssetStatus::Ready),
9254            Some(RootOwnedCanisterLifecycle::Idle)
9255        );
9256        assert_eq!(
9257            root_owned_lifecycle(
9258                DesiredCanisterKind::Pool,
9259                &CanisterPoolAssetStatus::Claimed {
9260                    claim: canic_core::dto::pool::CanisterPoolClaim {
9261                        component: canic_core::ids::ComponentInstanceId::from_generated_bytes(
9262                            [1; 32]
9263                        ),
9264                        operation_id: [2; 32],
9265                    },
9266                }
9267            ),
9268            Some(RootOwnedCanisterLifecycle::Claimed)
9269        );
9270        assert_eq!(
9271            root_owned_lifecycle(
9272                DesiredCanisterKind::Pool,
9273                &CanisterPoolAssetStatus::Workload {
9274                    claim: canic_core::dto::pool::CanisterPoolClaim {
9275                        component: canic_core::ids::ComponentInstanceId::from_generated_bytes(
9276                            [1; 32]
9277                        ),
9278                        operation_id: [2; 32],
9279                    },
9280                }
9281            ),
9282            Some(RootOwnedCanisterLifecycle::Workload)
9283        );
9284        assert_eq!(
9285            root_owned_lifecycle(
9286                DesiredCanisterKind::Pool,
9287                &CanisterPoolAssetStatus::HandingOff {
9288                    recipient: Principal::anonymous(),
9289                }
9290            ),
9291            None
9292        );
9293        assert_eq!(
9294            root_owned_lifecycle(
9295                DesiredCanisterKind::Store,
9296                &CanisterPoolAssetStatus::PendingReset
9297            ),
9298            None
9299        );
9300    }
9301
9302    #[cfg(unix)]
9303    #[test]
9304    #[expect(
9305        clippy::too_many_lines,
9306        reason = "one platform-to-policy matrix proves lifecycle projection and exclusive funding authority"
9307    )]
9308    fn configured_pool_observation_preserves_one_funding_owner() {
9309        use crate::fleet_ensure::{model::DesiredFleetArtifacts, policy::compile_plan};
9310
9311        let mut fixture = PoolInspectionFixture::fresh(1);
9312        fixture.target = Principal::from_slice(&[10]);
9313        PoolInspectionFixture::reserve_response(
9314            &fixture.owners.root,
9315            fixture.root_id,
9316            fixture.target,
9317            1_000,
9318        );
9319        let root_id = fixture.root_id.to_text();
9320        let target = fixture.target.to_text();
9321        let mut desired = fixture.owners.platform.desired.clone();
9322        desired.cycles_ledger = Principal::from_slice(&[8]).to_text();
9323        desired.treasury = "root".into();
9324        desired.ledger_fee_cycles = "5".into();
9325        desired.management_creation_fee_cycles = "500".into();
9326        desired.material_cycle_threshold = "1".into();
9327        desired.maximum_observation_burn_cycles = "10".into();
9328        desired.maximum_update_burn_cycles = "20".into();
9329        let mut root = desired.canisters[0].clone();
9330        root.principal = Some(root_id.clone());
9331        root.subnet = root_id.clone();
9332        root.controllers = vec![desired.operator.clone()];
9333        root.wasm = None;
9334        root.initial_cycles = "1000".into();
9335        root.minimum_cycles = "1000".into();
9336        let mut configured = root.clone();
9337        configured.name = "pool".into();
9338        configured.kind = DesiredCanisterKind::Pool;
9339        configured.principal = Some(target.clone());
9340        configured.controllers = vec![root_id.clone()];
9341        configured.parent = Some(root.name.clone());
9342        let mut coordinator = root.clone();
9343        coordinator.name = "coordinator".into();
9344        coordinator.kind = DesiredCanisterKind::Coordinator;
9345        coordinator.principal = Some(Principal::from_slice(&[20]).to_text());
9346        root.parent = Some(coordinator.name.clone());
9347        desired.canisters = vec![coordinator.clone(), root, configured.clone()];
9348        let bootstrap = desired.bootstrap.as_mut().unwrap();
9349        bootstrap.fresh_estate = false;
9350        bootstrap.roots = vec![crate::fleet_ensure::model::DesiredFleetBootstrapRoot {
9351            capacity_import_bootstrap: None,
9352            canister_pool_imports: vec![target.clone()],
9353            component_admissions: Vec::new(),
9354            component_topology_digest: canic_core::ids::ComponentTopologyDigest::from_bytes(
9355                [1; 32],
9356            ),
9357            funding: crate::test_support::fleet_subnet_root_funding_authority(),
9358            limits: canic_core::ids::FleetSubnetRootLimits {
9359                maximum_component_instances: 1,
9360                maximum_registry_bytes: 1,
9361                maximum_wasm_store_bytes: 1,
9362                canister_pool: canic_core::ids::FleetSubnetCanisterPoolConfig {
9363                    minimum_size: 0,
9364                    maximum_size: 1,
9365                    canister_cycles: Cycles::new(1_000),
9366                    creation_execution_margin: Cycles::new(100),
9367                },
9368                cycles_funding: canic_core::ids::CyclesFundingBudget {
9369                    window_secs: 1,
9370                    maximum_cycles: Cycles::new(1),
9371                },
9372                maximum_group_placements: 1,
9373            },
9374            placement_subnet: canic_core::ids::SubnetId::from_principal(fixture.root_id),
9375            root: "root".into(),
9376            store: "store".into(),
9377        }];
9378        let claim = canic_core::dto::pool::CanisterPoolClaim {
9379            component: canic_core::ids::ComponentInstanceId::from_generated_bytes([1; 32]),
9380            operation_id: [2; 32],
9381        };
9382        for status in [
9383            CanisterPoolAssetStatus::PendingReset,
9384            CanisterPoolAssetStatus::Failed {
9385                reason: "reset interrupted".into(),
9386            },
9387            CanisterPoolAssetStatus::Ready,
9388            CanisterPoolAssetStatus::Claimed {
9389                claim: claim.clone(),
9390            },
9391            CanisterPoolAssetStatus::Workload { claim },
9392        ] {
9393            for cycles in [900, 1_050, 1_100] {
9394                let lifecycle = estate_pool_lifecycle(&status).unwrap();
9395                let retained = matches!(
9396                    lifecycle,
9397                    EstatePoolAssetLifecycle::PendingReset | EstatePoolAssetLifecycle::Failed
9398                );
9399                let live = IcpEnsurePlatform::observed_root_owned_asset(
9400                    &configured,
9401                    &target,
9402                    &root_id,
9403                    CanisterPoolAsset {
9404                        canister_id: fixture.target,
9405                        creation_receipt: None,
9406                        cycles: Cycles::new(cycles),
9407                        origin: CanisterPoolAssetOrigin::Imported,
9408                        status: status.clone(),
9409                        added_at_ns: 1,
9410                        updated_at_ns: 1,
9411                    },
9412                )
9413                .unwrap();
9414                let mut observation = FleetObservation {
9415                    additional_controlled_cycles: BTreeMap::new(),
9416                    canisters: BTreeMap::from([
9417                        ("pool".into(), live),
9418                        (
9419                            "root".into(),
9420                            Some(LiveCanister {
9421                                canister_version: None,
9422                                controllers: vec![desired.operator.clone()],
9423                                cycles: 10_000,
9424                                module_sha256: None,
9425                                principal: root_id.clone(),
9426                                reinstall_required: false,
9427                                root_owned_lifecycle: None,
9428                                status: CanisterRuntimeStatus::Running,
9429                            }),
9430                        ),
9431                    ]),
9432                    estate_funding_domains: BTreeMap::from([(
9433                        "root".into(),
9434                        EstateFundingDomainObservation {
9435                            balance_cycles: Some(0),
9436                            cycles_ledger: desired.cycles_ledger.clone(),
9437                            root_principal: Some(root_id.clone()),
9438                            pool: Some(EstatePoolInventoryObservation {
9439                                assets: vec![EstatePoolAssetObservation {
9440                                    creation_receipt: None,
9441                                    cycles,
9442                                    lifecycle,
9443                                    origin: EstatePoolAssetOrigin::Imported,
9444                                    principal: target.clone(),
9445                                }],
9446                                maximum_size: 1,
9447                                minimum_size: 0,
9448                                pending_creation: None,
9449                                readiness_floor_cycles: 1_000,
9450                                creation_execution_margin_cycles: 100,
9451                            }),
9452                        },
9453                    )]),
9454                    ledger_fee_cycles: 5,
9455                    operator_cycles: 10_000,
9456                    protocol_ready: BTreeMap::new(),
9457                };
9458                let mut coordinator_live = observation.canisters["root"].clone().unwrap();
9459                coordinator_live.principal = coordinator.principal.clone().unwrap();
9460                observation
9461                    .canisters
9462                    .insert(coordinator.name.clone(), Some(coordinator_live));
9463                let actions = current_protocol::compile_pool_reconciliation(
9464                    &fixture.owners.root,
9465                    &desired,
9466                    &fixture.owners.state,
9467                    &observation.estate_funding_domains,
9468                )
9469                .unwrap();
9470                assert_eq!(actions.len(), usize::from(retained));
9471                let plan = compile_plan(
9472                    &desired,
9473                    &DesiredFleetArtifacts::default(),
9474                    &actions,
9475                    &"11".repeat(32),
9476                    &desired.fleet,
9477                    &observation,
9478                    1,
9479                    &"22".repeat(32),
9480                    None,
9481                )
9482                .expect("configured pool and reconciliation must share one funding owner");
9483                let pool_plan = plan
9484                    .canisters
9485                    .iter()
9486                    .find(|entry| entry.name == "pool")
9487                    .unwrap();
9488                let funds = pool_plan
9489                    .actions
9490                    .iter()
9491                    .filter(|action| matches!(action, EnsureAction::Fund { .. }))
9492                    .collect::<Vec<_>>();
9493                let should_fund = if retained {
9494                    cycles < 1_100
9495                } else {
9496                    cycles < 1_000 && lifecycle == EstatePoolAssetLifecycle::Ready
9497                };
9498                assert_eq!(funds.len(), usize::from(should_fund));
9499                if let Some(EnsureAction::Fund {
9500                    amount,
9501                    pool_funding,
9502                    expected_post_cycles,
9503                    ..
9504                }) = funds.first()
9505                {
9506                    let authority = pool_funding.as_ref().unwrap();
9507                    assert_eq!(
9508                        (&authority.root, authority.lifecycle),
9509                        (&root_id, lifecycle)
9510                    );
9511                    if retained {
9512                        assert_eq!((*amount, *expected_post_cycles), (1_130 - cycles, 1_130));
9513                    }
9514                }
9515                if retained && cycles == 1_100 {
9516                    observation
9517                        .canisters
9518                        .get_mut(&coordinator.name)
9519                        .unwrap()
9520                        .as_mut()
9521                        .unwrap()
9522                        .status = CanisterRuntimeStatus::Stopped;
9523                    let prerequisite = compile_plan(
9524                        &desired,
9525                        &DesiredFleetArtifacts::default(),
9526                        &[],
9527                        &"11".repeat(32),
9528                        &desired.fleet,
9529                        &observation,
9530                        1,
9531                        &"22".repeat(32),
9532                        None,
9533                    )
9534                    .expect("live reset evidence must not block Coordinator readiness");
9535                    assert!(prerequisite.canisters.iter().flat_map(|entry| &entry.actions)
9536                        .any(|action| matches!(action, EnsureAction::Start { name, .. } if name == &coordinator.name)));
9537                }
9538            }
9539        }
9540        std::fs::remove_dir_all(&fixture.owners.root).unwrap();
9541    }
9542
9543    #[test]
9544    fn estate_pool_inventory_reconciles_every_lifecycle_and_declared_total() {
9545        let claim = canic_core::dto::pool::CanisterPoolClaim {
9546            component: canic_core::ids::ComponentInstanceId::from_generated_bytes([1; 32]),
9547            operation_id: [2; 32],
9548        };
9549        let statuses = [
9550            CanisterPoolAssetStatus::Store,
9551            CanisterPoolAssetStatus::StoreDeletionPending {
9552                operation_id: [3; 32],
9553            },
9554            CanisterPoolAssetStatus::PendingReset,
9555            CanisterPoolAssetStatus::Ready,
9556            CanisterPoolAssetStatus::Claimed {
9557                claim: claim.clone(),
9558            },
9559            CanisterPoolAssetStatus::Workload {
9560                claim: claim.clone(),
9561            },
9562            CanisterPoolAssetStatus::Recycling {
9563                claim,
9564                reset: canic_core::dto::pool::CanisterPoolRecycleReset::Pending,
9565            },
9566            CanisterPoolAssetStatus::HandingOff {
9567                recipient: Principal::anonymous(),
9568            },
9569            CanisterPoolAssetStatus::Failed {
9570                reason: "retained failure".to_string(),
9571            },
9572        ];
9573        let mut observed = EstatePoolLifecycleCounts::default();
9574        for status in &statuses {
9575            observed.observe(status).expect("bounded lifecycle count");
9576        }
9577        let authority = EstatePoolPageAuthority {
9578            completed_handoffs: 0,
9579            counts: observed,
9580            pending_handoff: None,
9581            pooled: 4,
9582            surplus: 1,
9583        };
9584
9585        assert!(authority.matches_complete_inventory(observed, 3));
9586        let mut incomplete = observed;
9587        incomplete.failed -= 1;
9588        incomplete.tracked -= 1;
9589        assert!(!authority.matches_complete_inventory(incomplete, 3));
9590
9591        assert_eq!(estate_pool_lifecycle(&statuses[0]), None);
9592        assert_eq!(estate_pool_lifecycle(&statuses[1]), None);
9593        assert_eq!(
9594            statuses[2..]
9595                .iter()
9596                .map(estate_pool_lifecycle)
9597                .collect::<Vec<_>>(),
9598            vec![
9599                Some(EstatePoolAssetLifecycle::PendingReset),
9600                Some(EstatePoolAssetLifecycle::Ready),
9601                Some(EstatePoolAssetLifecycle::Claimed),
9602                Some(EstatePoolAssetLifecycle::Workload),
9603                Some(EstatePoolAssetLifecycle::Recycling),
9604                Some(EstatePoolAssetLifecycle::HandingOff),
9605                Some(EstatePoolAssetLifecycle::Failed),
9606            ]
9607        );
9608    }
9609
9610    #[test]
9611    fn estate_funding_requires_exact_source_debit_and_destination_credit() {
9612        let exact = EstateFundingObservation {
9613            amount: 60,
9614            destination_after: 100,
9615            destination_before: Some(40),
9616            expected_destination_after: 100,
9617            ledger_fee_cycles: 5,
9618            source_after: 135,
9619            source_before: Some(200),
9620        };
9621        assert!(estate_funding_applied(exact));
9622
9623        for drifted in [
9624            EstateFundingObservation {
9625                source_after: 136,
9626                ..exact
9627            },
9628            EstateFundingObservation {
9629                destination_after: 99,
9630                ..exact
9631            },
9632            EstateFundingObservation {
9633                source_before: None,
9634                ..exact
9635            },
9636            EstateFundingObservation {
9637                destination_before: None,
9638                ..exact
9639            },
9640        ] {
9641            assert!(!estate_funding_applied(drifted));
9642        }
9643    }
9644
9645    #[test]
9646    #[expect(
9647        clippy::too_many_lines,
9648        reason = "the binding regression mutates each authority field independently"
9649    )]
9650    fn retained_root_owned_topology_requires_exact_child_root_and_parent_binding() {
9651        let mut state = FleetEnsureStateRecord {
9652            active_registry: None,
9653            completed_reinstall_action_sha256: BTreeMap::new(),
9654            completed_reinstall_operation_id: None,
9655            completed_reinstalls: BTreeMap::new(),
9656            fleet: "fleet".to_string(),
9657            pending_principals: BTreeMap::new(),
9658            principals: BTreeMap::from([
9659                ("root".to_string(), "root-principal".to_string()),
9660                ("store".to_string(), "store-principal".to_string()),
9661            ]),
9662            retained_cycles_by_principal: BTreeMap::new(),
9663            schema_version: crate::fleet_ensure::model::FLEET_ENSURE_SCHEMA_VERSION,
9664            topology: BTreeMap::from([(
9665                "store".to_string(),
9666                crate::fleet_ensure::model::FleetEnsureTopologyRecord {
9667                    kind: DesiredCanisterKind::Store,
9668                    module_hash: Some("11".repeat(32)),
9669                    parent: Some("root".to_string()),
9670                    protocol_binding: None,
9671                    role: None,
9672                },
9673            )]),
9674        };
9675
9676        assert!(
9677            exact_retained_root_owned_topology(
9678                &state,
9679                "store",
9680                DesiredCanisterKind::Store,
9681                "root",
9682                "store-principal",
9683                "root-principal",
9684            )
9685            .is_some()
9686        );
9687        assert!(
9688            exact_retained_root_owned_topology(
9689                &state,
9690                "store",
9691                DesiredCanisterKind::Store,
9692                "root",
9693                "foreign-store",
9694                "root-principal",
9695            )
9696            .is_none()
9697        );
9698        state
9699            .principals
9700            .insert("store".to_string(), "store-principal".to_string());
9701        state
9702            .principals
9703            .insert("root".to_string(), "foreign-root".to_string());
9704        assert!(
9705            exact_retained_root_owned_topology(
9706                &state,
9707                "store",
9708                DesiredCanisterKind::Store,
9709                "root",
9710                "store-principal",
9711                "root-principal",
9712            )
9713            .is_none()
9714        );
9715        state
9716            .principals
9717            .insert("root".to_string(), "root-principal".to_string());
9718        state
9719            .topology
9720            .get_mut("store")
9721            .expect("Store topology")
9722            .kind = DesiredCanisterKind::Pool;
9723        assert!(
9724            exact_retained_root_owned_topology(
9725                &state,
9726                "store",
9727                DesiredCanisterKind::Store,
9728                "root",
9729                "store-principal",
9730                "root-principal",
9731            )
9732            .is_none()
9733        );
9734        state
9735            .topology
9736            .get_mut("store")
9737            .expect("Store topology")
9738            .kind = DesiredCanisterKind::Store;
9739        state
9740            .topology
9741            .get_mut("store")
9742            .expect("Store topology")
9743            .parent = Some("foreign-root".to_string());
9744        assert!(
9745            exact_retained_root_owned_topology(
9746                &state,
9747                "store",
9748                DesiredCanisterKind::Store,
9749                "root",
9750                "store-principal",
9751                "root-principal",
9752            )
9753            .is_none()
9754        );
9755        state
9756            .topology
9757            .get_mut("store")
9758            .expect("Store topology")
9759            .parent = Some("root".to_string());
9760        state.principals.clear();
9761        state.pending_principals = BTreeMap::from([
9762            ("root".to_string(), "root-principal".to_string()),
9763            ("store".to_string(), "store-principal".to_string()),
9764        ]);
9765        assert!(
9766            exact_retained_root_owned_topology(
9767                &state,
9768                "store",
9769                DesiredCanisterKind::Store,
9770                "root",
9771                "store-principal",
9772                "root-principal",
9773            )
9774            .is_some()
9775        );
9776        state
9777            .principals
9778            .insert("store".to_string(), "foreign-store".to_string());
9779        assert!(
9780            exact_retained_root_owned_topology(
9781                &state,
9782                "store",
9783                DesiredCanisterKind::Store,
9784                "root",
9785                "store-principal",
9786                "root-principal",
9787            )
9788            .is_none()
9789        );
9790    }
9791
9792    #[test]
9793    fn completed_reinstall_requires_exact_continuity_when_ordinary_status_has_no_version() {
9794        let desired = "11".repeat(32);
9795        assert_eq!(
9796            completed_reinstall_continuity(7, true, true, Some(&desired), &desired, None,),
9797            Ok(())
9798        );
9799        assert_eq!(
9800            completed_reinstall_continuity(7, true, true, Some(&desired), &desired, Some(8),),
9801            Ok(())
9802        );
9803        for (rejected, field) in [
9804            (
9805                completed_reinstall_continuity(7, false, true, Some(&desired), &desired, None),
9806                "Principal",
9807            ),
9808            (
9809                completed_reinstall_continuity(7, true, false, Some(&desired), &desired, None),
9810                "Root/parent/kind topology",
9811            ),
9812            (
9813                completed_reinstall_continuity(
9814                    7,
9815                    true,
9816                    true,
9817                    Some(&"22".repeat(32)),
9818                    &desired,
9819                    None,
9820                ),
9821                "module SHA-256",
9822            ),
9823            (
9824                completed_reinstall_continuity(7, true, true, Some(&desired), &desired, Some(7)),
9825                "canister version",
9826            ),
9827        ] {
9828            assert_eq!(rejected, Err(field));
9829        }
9830    }
9831
9832    #[test]
9833    fn fresh_fleet_create_responses_retain_the_exact_requested_balance() {
9834        let canister = Principal::from_slice(&[9; 29]);
9835        let canister_text = canister.to_text();
9836        for (response, receipt) in [
9837            (
9838                Ok(CreateCanisterSuccess {
9839                    block_id: Nat::from(41_u8),
9840                    canister_id: canister,
9841                }),
9842                "41",
9843            ),
9844            (
9845                Err(CreateCanisterError::Duplicate {
9846                    duplicate_of: Nat::from(41_u8),
9847                    canister_id: Some(canister),
9848                }),
9849                "41",
9850            ),
9851        ] {
9852            let outcome = create_canister_response_outcome(response, 5_000)
9853                .expect("successful or duplicate-with-Principal Create response");
9854            assert_eq!(
9855                outcome.created_principal.as_deref(),
9856                Some(canister_text.as_str())
9857            );
9858            assert_eq!(outcome.post_cycles, Some(5_000));
9859            assert_eq!(outcome.receipt.as_deref(), Some(receipt));
9860        }
9861    }
9862
9863    #[test]
9864    fn root_funding_inspection_requires_exact_controller_and_pool_shape() {
9865        let root = Principal::from_slice(&[7; 29]);
9866        let root_text = root.to_text();
9867        let cycles = Nat::from(2_898_749_313_788_u128);
9868        assert_eq!(
9869            validate_root_controlled_inspection(
9870                "pool-0",
9871                InspectedModule::Empty,
9872                &root_text,
9873                &[root],
9874                None,
9875                &cycles,
9876                &[],
9877            )
9878            .expect("exact Root-authorized pool inspection"),
9879            2_898_749_313_788,
9880        );
9881
9882        assert_eq!(
9883            validate_root_controlled_inspection(
9884                "pending-reset",
9885                InspectedModule::Any,
9886                &root_text,
9887                &[root],
9888                Some(&[1]),
9889                &cycles,
9890                &[],
9891            )
9892            .expect("installed controlled asset remains part of conservation"),
9893            2_898_749_313_788,
9894        );
9895        let foreign = Principal::from_slice(&[8; 29]);
9896        for rejected in [
9897            validate_root_controlled_inspection(
9898                "pending-reset",
9899                InspectedModule::Any,
9900                &root_text,
9901                &[foreign],
9902                Some(&[1]),
9903                &cycles,
9904                &[],
9905            ),
9906            validate_root_controlled_inspection(
9907                "pool-0",
9908                InspectedModule::Empty,
9909                &root_text,
9910                &[foreign],
9911                None,
9912                &cycles,
9913                &[],
9914            ),
9915            validate_root_controlled_inspection(
9916                "pool-0",
9917                InspectedModule::Empty,
9918                &root_text,
9919                &[root],
9920                Some(&[1]),
9921                &cycles,
9922                &[],
9923            ),
9924        ] {
9925            assert!(matches!(
9926                rejected,
9927                Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { .. })
9928            ));
9929        }
9930    }
9931
9932    #[test]
9933    fn pending_fresh_pool_observation_accepts_only_its_reviewed_controller_transition() {
9934        let root = Principal::from_slice(&[7; 29]);
9935        let operator = Principal::from_slice(&[8; 29]);
9936        let foreign = Principal::from_slice(&[9; 29]);
9937        for controllers in [vec![root, operator], vec![root]] {
9938            assert_eq!(
9939                validate_pending_fresh_pool_inspection(
9940                    "pool",
9941                    &root.to_text(),
9942                    &operator.to_text(),
9943                    &controllers,
9944                    None,
9945                    &Nat::from(17_u8),
9946                    &[],
9947                )
9948                .expect("issued fresh controller transition"),
9949                17,
9950            );
9951        }
9952        for controllers in [
9953            vec![root, foreign],
9954            vec![operator],
9955            vec![root, operator, foreign],
9956            vec![root, root],
9957        ] {
9958            assert!(matches!(
9959                validate_pending_fresh_pool_inspection(
9960                    "pool",
9961                    &root.to_text(),
9962                    &operator.to_text(),
9963                    &controllers,
9964                    None,
9965                    &Nat::from(17_u8),
9966                    &[],
9967                ),
9968                Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { .. })
9969            ));
9970        }
9971    }
9972
9973    #[test]
9974    fn recovery_controller_is_required_on_ready_and_pending_fresh_pool_assets() {
9975        let root = Principal::from_slice(&[7; 29]);
9976        let operator = Principal::from_slice(&[8; 29]);
9977        let recovery = Principal::from_slice(&[9; 29]);
9978        let cycles = Nat::from(17_u8);
9979        assert!(
9980            validate_root_controlled_inspection(
9981                "ready",
9982                InspectedModule::Empty,
9983                &root.to_text(),
9984                &[root, recovery],
9985                None,
9986                &cycles,
9987                &[recovery],
9988            )
9989            .is_ok()
9990        );
9991        assert!(
9992            validate_root_controlled_inspection(
9993                "ready",
9994                InspectedModule::Empty,
9995                &root.to_text(),
9996                &[root],
9997                None,
9998                &cycles,
9999                &[recovery],
10000            )
10001            .is_err()
10002        );
10003        for controllers in [vec![root, recovery], vec![root, operator, recovery]] {
10004            assert!(
10005                validate_pending_fresh_pool_inspection(
10006                    "pending",
10007                    &root.to_text(),
10008                    &operator.to_text(),
10009                    &controllers,
10010                    None,
10011                    &cycles,
10012                    &[recovery],
10013                )
10014                .is_ok()
10015            );
10016        }
10017        assert!(
10018            validate_pending_fresh_pool_inspection(
10019                "pending",
10020                &root.to_text(),
10021                &operator.to_text(),
10022                &[root, operator],
10023                None,
10024                &cycles,
10025                &[recovery],
10026            )
10027            .is_err()
10028        );
10029    }
10030}