Skip to main content

canic_host/fleet_ensure/ops/
platform.rs

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