canic-host 0.63.1

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
use super::*;
use crate::{
    nns_data_center::NnsDataCenterRow, nns_node::NnsNodeRow, nns_node_operator::NnsNodeOperatorRow,
    nns_node_provider::NnsNodeProviderRow, subnet_catalog::SubnetCatalogSubnetRow,
};
use canic_subnet_catalog::{
    ClassificationSource, GeographicScope, MAINNET_NETWORK, MAINNET_REGISTRY_CANISTER_ID,
    SubnetKind, SubnetSpecialization,
};

#[test]
fn topology_summary_counts_existing_reports() {
    let report = topology_summary_report_from_reports(
        MAINNET_NETWORK.to_string(),
        "https://icp-api.io".to_string(),
        subnet_report_fixture(),
        node_report_fixture(),
        node_provider_report_fixture(),
        node_operator_report_fixture(),
        data_center_report_fixture(),
    );

    assert_eq!(report.schema_version, 1);
    assert_eq!(report.subnet_count, 2);
    assert_eq!(report.application_subnet_count, 1);
    assert_eq!(report.system_subnet_count, 1);
    assert_eq!(report.routing_range_count, 3);
    assert_eq!(report.node_count, 3);
    assert_eq!(report.application_node_count, 2);
    assert_eq!(report.system_node_count, 1);
    assert_eq!(report.node_provider_count, 1);
    assert_eq!(report.node_operator_count, 2);
    assert_eq!(report.data_center_count, 1);
    assert_eq!(report.registry_versions.len(), 5);
}

#[test]
fn topology_summary_text_renders_count_and_version_tables() {
    let report = topology_summary_report_from_reports(
        MAINNET_NETWORK.to_string(),
        "https://icp-api.io".to_string(),
        subnet_report_fixture(),
        node_report_fixture(),
        node_provider_report_fixture(),
        node_operator_report_fixture(),
        data_center_report_fixture(),
    );

    let text = nns_topology_summary_report_text(&report);

    assert!(text.contains("topology: ic subnets 2 nodes 3"));
    assert!(text.contains("routing_ranges"));
    assert!(text.contains("subnet_kinds:"));
    assert!(text.contains("registry_versions:"));
    assert!(text.contains("subnet_catalog"));
}

#[test]
fn topology_summary_rejects_local_network_with_topology_hint() {
    let request = NnsTopologySummaryRequest {
        icp_root: std::env::temp_dir(),
        network: "local".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        now_unix_secs: 1_780_531_200,
    };

    let err = build_nns_topology_summary_report(&request).expect_err("local rejected");
    let message = err.to_string();

    assert!(message.contains("supports only the mainnet `ic` network"));
    assert!(message.contains("canic --network ic nns topology summary"));
}

#[test]
fn topology_refresh_counts_component_reports() {
    let report = topology_refresh_report_from_reports(
        MAINNET_NETWORK.to_string(),
        "https://icp-api.io".to_string(),
        false,
        NnsTopologyRefreshComponentReports {
            subnet: subnet_refresh_report_fixture(),
            node: node_refresh_report_fixture(),
            node_provider: node_provider_refresh_report_fixture(),
            node_operator: node_operator_refresh_report_fixture(),
            data_center: data_center_refresh_report_fixture(),
        },
    );

    assert_eq!(report.schema_version, 1);
    assert_eq!(report.component_count, 5);
    assert_eq!(report.wrote_cache_count, 5);
    assert_eq!(report.replaced_existing_cache_count, 1);
    assert_eq!(report.components[0].source, "subnet_catalog");
    assert_eq!(report.components[0].item_count, 2);
    assert_eq!(report.components[1].source, "nodes");
    assert_eq!(report.components[1].item_count, 3);
}

#[test]
fn topology_refresh_text_renders_component_table() {
    let report = topology_refresh_report_from_reports(
        MAINNET_NETWORK.to_string(),
        "https://icp-api.io".to_string(),
        true,
        NnsTopologyRefreshComponentReports {
            subnet: dry_run_subnet_refresh_report_fixture(),
            node: dry_run_node_refresh_report_fixture(),
            node_provider: dry_run_node_provider_refresh_report_fixture(),
            node_operator: dry_run_node_operator_refresh_report_fixture(),
            data_center: dry_run_data_center_refresh_report_fixture(),
        },
    );

    let text = nns_topology_refresh_report_text(&report);

    assert!(text.contains("topology_refresh: ic components 5 wrote 0 replaced 1 dry_run yes"));
    assert!(text.contains("subnet_catalog"));
    assert!(text.contains("node_operators"));
    assert!(text.contains("data_centers"));
}

fn subnet_report_fixture() -> SubnetCatalogListReport {
    SubnetCatalogListReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        catalog_path: "catalog.json".to_string(),
        catalog_schema_version: 1,
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 42,
        fetched_at: "2026-06-04T00:00:00Z".to_string(),
        catalog_stale: false,
        stale_reason: "fresh".to_string(),
        resolver_backend: "local-nns-subnet-catalog".to_string(),
        subnets: vec![
            subnet_row("pzp6e", SubnetKind::Application, 2, 2),
            subnet_row("tdb26", SubnetKind::System, 1, 1),
        ],
    }
}

fn subnet_refresh_report_fixture() -> SubnetCatalogRefreshReport {
    SubnetCatalogRefreshReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        catalog_path: ".canic/subnet-catalog/ic/catalog.json".to_string(),
        refresh_lock_path: ".canic/subnet-catalog/ic/refresh.lock".to_string(),
        output_path: None,
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 42,
        fetched_at: "2026-06-04T00:00:00Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        fetched_by: "test".to_string(),
        dry_run: false,
        wrote_catalog: true,
        replaced_existing_catalog: true,
        subnet_count: 2,
        routing_range_count: 3,
    }
}

fn dry_run_subnet_refresh_report_fixture() -> SubnetCatalogRefreshReport {
    SubnetCatalogRefreshReport {
        dry_run: true,
        wrote_catalog: false,
        ..subnet_refresh_report_fixture()
    }
}

fn node_refresh_report_fixture() -> NnsNodeRefreshReport {
    NnsNodeRefreshReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        cache_path: ".canic/node/ic/nodes.json".to_string(),
        refresh_lock_path: ".canic/node/ic/refresh.lock".to_string(),
        output_path: None,
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 43,
        fetched_at: "2026-06-04T00:01:00Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        fetched_by: "test".to_string(),
        dry_run: false,
        wrote_cache: true,
        replaced_existing_cache: false,
        node_count: 3,
    }
}

fn dry_run_node_refresh_report_fixture() -> NnsNodeRefreshReport {
    NnsNodeRefreshReport {
        dry_run: true,
        wrote_cache: false,
        ..node_refresh_report_fixture()
    }
}

fn node_provider_refresh_report_fixture() -> NnsNodeProviderRefreshReport {
    NnsNodeProviderRefreshReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        cache_path: ".canic/node-provider/ic/providers.json".to_string(),
        refresh_lock_path: ".canic/node-provider/ic/refresh.lock".to_string(),
        output_path: None,
        governance_canister_id: "rrkah-fqaaa-aaaaa-aaaaq-cai".to_string(),
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 44,
        fetched_at: "2026-06-04T00:02:00Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        fetched_by: "test".to_string(),
        dry_run: false,
        wrote_cache: true,
        replaced_existing_cache: false,
        node_provider_count: 1,
    }
}

fn dry_run_node_provider_refresh_report_fixture() -> NnsNodeProviderRefreshReport {
    NnsNodeProviderRefreshReport {
        dry_run: true,
        wrote_cache: false,
        ..node_provider_refresh_report_fixture()
    }
}

fn node_operator_refresh_report_fixture() -> NnsNodeOperatorRefreshReport {
    NnsNodeOperatorRefreshReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        cache_path: ".canic/node-operator/ic/operators.json".to_string(),
        refresh_lock_path: ".canic/node-operator/ic/refresh.lock".to_string(),
        output_path: None,
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 45,
        fetched_at: "2026-06-04T00:03:00Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        fetched_by: "test".to_string(),
        dry_run: false,
        wrote_cache: true,
        replaced_existing_cache: false,
        node_operator_count: 2,
    }
}

fn dry_run_node_operator_refresh_report_fixture() -> NnsNodeOperatorRefreshReport {
    NnsNodeOperatorRefreshReport {
        dry_run: true,
        wrote_cache: false,
        ..node_operator_refresh_report_fixture()
    }
}

fn data_center_refresh_report_fixture() -> NnsDataCenterRefreshReport {
    NnsDataCenterRefreshReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        cache_path: ".canic/data-center/ic/data-centers.json".to_string(),
        refresh_lock_path: ".canic/data-center/ic/refresh.lock".to_string(),
        output_path: None,
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 46,
        fetched_at: "2026-06-04T00:04:00Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        fetched_by: "test".to_string(),
        dry_run: false,
        wrote_cache: true,
        replaced_existing_cache: false,
        data_center_count: 1,
    }
}

fn dry_run_data_center_refresh_report_fixture() -> NnsDataCenterRefreshReport {
    NnsDataCenterRefreshReport {
        dry_run: true,
        wrote_cache: false,
        ..data_center_refresh_report_fixture()
    }
}

fn subnet_row(
    subnet_principal: &str,
    subnet_kind: SubnetKind,
    node_count: u32,
    range_count: usize,
) -> SubnetCatalogSubnetRow {
    SubnetCatalogSubnetRow {
        subnet_principal: subnet_principal.to_string(),
        subnet_kind,
        subnet_kind_source: ClassificationSource::Registry,
        subnet_specialization: SubnetSpecialization::None,
        subnet_specialization_source: ClassificationSource::Computed,
        geographic_scope: GeographicScope::Global,
        geographic_scope_source: ClassificationSource::Computed,
        subnet_label: subnet_kind.as_str().to_string(),
        subnet_label_source: ClassificationSource::Computed,
        node_count: Some(node_count),
        charges_apply_by_default: subnet_kind == SubnetKind::Application,
        range_count,
        ranges_shown: 0,
        range_offset: 0,
        range_limit: 1,
        ranges: Vec::new(),
    }
}

fn node_report_fixture() -> NnsNodeListReport {
    NnsNodeListReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 43,
        fetched_at: "2026-06-04T00:01:00Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        fetched_by: "test".to_string(),
        node_count: 3,
        nodes: vec![
            node_row("node-a", "application"),
            node_row("node-b", "application"),
            node_row("node-c", "system"),
        ],
    }
}

fn node_row(node_principal: &str, subnet_kind: &str) -> NnsNodeRow {
    NnsNodeRow {
        node_principal: node_principal.to_string(),
        node_operator_principal: "operator-a".to_string(),
        node_provider_principal: "provider-a".to_string(),
        subnet_principal: "subnet-a".to_string(),
        subnet_kind: subnet_kind.to_string(),
        data_center_id: "dc1".to_string(),
    }
}

fn node_provider_report_fixture() -> NnsNodeProviderListReport {
    NnsNodeProviderListReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        governance_canister_id: "rrkah-fqaaa-aaaaa-aaaaq-cai".to_string(),
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 44,
        fetched_at: "2026-06-04T00:02:00Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        fetched_by: "test".to_string(),
        node_provider_count: 1,
        node_providers: vec![NnsNodeProviderRow {
            node_provider_principal: "provider-a".to_string(),
            name: None,
            node_count: Some(3),
            reward_account_hex: None,
        }],
    }
}

fn node_operator_report_fixture() -> NnsNodeOperatorListReport {
    NnsNodeOperatorListReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 45,
        fetched_at: "2026-06-04T00:03:00Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        fetched_by: "test".to_string(),
        node_operator_count: 2,
        node_operators: vec![
            NnsNodeOperatorRow {
                node_operator_principal: "operator-a".to_string(),
                node_provider_principal: "provider-a".to_string(),
                node_allowance: 1,
                data_center_id: "dc1".to_string(),
                node_count: Some(2),
            },
            NnsNodeOperatorRow {
                node_operator_principal: "operator-b".to_string(),
                node_provider_principal: "provider-a".to_string(),
                node_allowance: 1,
                data_center_id: "dc1".to_string(),
                node_count: Some(1),
            },
        ],
    }
}

fn data_center_report_fixture() -> NnsDataCenterListReport {
    NnsDataCenterListReport {
        schema_version: 1,
        network: MAINNET_NETWORK.to_string(),
        registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
        registry_version: 46,
        fetched_at: "2026-06-04T00:04:00Z".to_string(),
        source_endpoint: "https://icp-api.io".to_string(),
        fetched_by: "test".to_string(),
        data_center_count: 1,
        data_centers: vec![NnsDataCenterRow {
            data_center_id: "dc1".to_string(),
            region: "eu-west".to_string(),
            owner: "example".to_string(),
            latitude: None,
            longitude: None,
            node_operator_count: 2,
            node_provider_count: 1,
            node_count: 3,
        }],
    }
}