Skip to main content

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