1use std::process::ExitCode;
29use std::sync::Arc;
30
31use serde::Serialize;
32use serde_json::{Value, json};
33
34use crate::api::ApiClient;
35use crate::config::Config;
36use crate::durability;
37use crate::manifest_walker::{self, InspectResult};
38use crate::stamp_preview;
39use crate::utility_verbs;
40
41#[derive(Debug, Serialize)]
44pub struct OnceResult {
45 pub verb: String,
46 pub status: OnceStatus,
47 pub message: String,
48 #[serde(skip_serializing_if = "Value::is_null")]
49 pub data: Value,
50}
51
52#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)]
53#[serde(rename_all = "snake_case")]
54pub enum OnceStatus {
55 Ok,
56 Unhealthy,
57 Error,
58 UsageError,
59}
60
61impl OnceStatus {
62 pub fn exit_code(self) -> ExitCode {
63 match self {
64 Self::Ok => ExitCode::SUCCESS,
65 Self::Unhealthy | Self::Error => ExitCode::from(1),
66 Self::UsageError => ExitCode::from(2),
67 }
68 }
69}
70
71impl OnceResult {
72 pub fn ok(verb: &str, message: impl Into<String>) -> Self {
73 Self {
74 verb: verb.into(),
75 status: OnceStatus::Ok,
76 message: message.into(),
77 data: Value::Null,
78 }
79 }
80 pub fn ok_with_data(verb: &str, message: impl Into<String>, data: Value) -> Self {
81 Self {
82 verb: verb.into(),
83 status: OnceStatus::Ok,
84 message: message.into(),
85 data,
86 }
87 }
88 pub fn unhealthy(verb: &str, message: impl Into<String>, data: Value) -> Self {
89 Self {
90 verb: verb.into(),
91 status: OnceStatus::Unhealthy,
92 message: message.into(),
93 data,
94 }
95 }
96 pub fn error(verb: &str, message: impl Into<String>) -> Self {
97 Self {
98 verb: verb.into(),
99 status: OnceStatus::Error,
100 message: message.into(),
101 data: Value::Null,
102 }
103 }
104 pub fn usage(verb: &str, message: impl Into<String>) -> Self {
105 Self {
106 verb: verb.into(),
107 status: OnceStatus::UsageError,
108 message: message.into(),
109 data: Value::Null,
110 }
111 }
112}
113
114pub async fn run(verb: &str, args: &[String], json_output: bool) -> ExitCode {
118 let result = dispatch(verb, args).await;
119 print_result(&result, json_output);
120 result.status.exit_code()
121}
122
123async fn dispatch(verb: &str, args: &[String]) -> OnceResult {
124 match verb {
125 "hash" => once_hash(args),
127 "cid" => once_cid(args),
128 "depth-table" => once_depth_table(),
129 "pss-target" => once_pss_target(args),
130 "gsoc-mine" => once_gsoc_mine(args),
131
132 "readiness" => once_readiness().await,
134 "version-check" => once_version_check().await,
135 "inspect" => once_inspect(args).await,
136 "durability-check" => once_durability_check(args).await,
137
138 "buy-preview" => once_buy_preview(args).await,
141 "buy-suggest" => once_buy_suggest(args).await,
142 "topup-preview" => once_topup_preview(args).await,
143 "dilute-preview" => once_dilute_preview(args).await,
144 "extend-preview" => once_extend_preview(args).await,
145 "plan-batch" => once_plan_batch(args).await,
146
147 other => OnceResult::usage(
149 other,
150 format!(
151 "unknown --once verb {other:?}. Supported: hash, cid, depth-table, pss-target, gsoc-mine, readiness, version-check, inspect, durability-check, buy-preview, buy-suggest, topup-preview, dilute-preview, extend-preview, plan-batch"
152 ),
153 ),
154 }
155}
156
157fn once_hash(args: &[String]) -> OnceResult {
160 let path = match args.first() {
161 Some(p) => p.as_str(),
162 None => {
163 return OnceResult::usage("hash", "usage: --once hash <path>");
164 }
165 };
166 match utility_verbs::hash_path(path) {
167 Ok(r) => OnceResult::ok_with_data(
168 "hash",
169 format!("hash {path}: {r}"),
170 json!({ "path": path, "reference": r }),
171 ),
172 Err(e) => OnceResult::error("hash", format!("hash failed: {e}")),
173 }
174}
175
176fn once_cid(args: &[String]) -> OnceResult {
177 let ref_arg = match args.first() {
178 Some(r) => r.as_str(),
179 None => return OnceResult::usage("cid", "usage: --once cid <ref> [manifest|feed]"),
180 };
181 let kind_arg = args.get(1).map(String::as_str);
182 let kind = match utility_verbs::parse_cid_kind(kind_arg) {
183 Ok(k) => k,
184 Err(e) => return OnceResult::usage("cid", e),
185 };
186 match utility_verbs::cid_for_ref(ref_arg, kind) {
187 Ok(cid) => {
188 OnceResult::ok_with_data("cid", format!("cid: {cid}"), json!({ "cid": cid }))
189 }
190 Err(e) => OnceResult::error("cid", format!("cid failed: {e}")),
191 }
192}
193
194fn once_depth_table() -> OnceResult {
195 OnceResult::ok_with_data(
196 "depth-table",
197 utility_verbs::depth_table(),
198 json!({ "table": utility_verbs::depth_table() }),
199 )
200}
201
202fn once_pss_target(args: &[String]) -> OnceResult {
203 let overlay = match args.first() {
204 Some(o) => o.as_str(),
205 None => return OnceResult::usage("pss-target", "usage: --once pss-target <overlay>"),
206 };
207 match utility_verbs::pss_target_for(overlay) {
208 Ok(prefix) => OnceResult::ok_with_data(
209 "pss-target",
210 format!("pss target prefix: {prefix}"),
211 json!({ "prefix": prefix }),
212 ),
213 Err(e) => OnceResult::error("pss-target", format!("pss-target failed: {e}")),
214 }
215}
216
217fn once_gsoc_mine(args: &[String]) -> OnceResult {
218 let overlay = args.first().map(String::as_str);
219 let ident = args.get(1).map(String::as_str);
220 let (overlay, ident) = match (overlay, ident) {
221 (Some(o), Some(i)) => (o, i),
222 _ => {
223 return OnceResult::usage(
224 "gsoc-mine",
225 "usage: --once gsoc-mine <overlay> <identifier>",
226 );
227 }
228 };
229 match utility_verbs::gsoc_mine_for(overlay, ident) {
230 Ok(out) => OnceResult::ok_with_data(
231 "gsoc-mine",
232 out.replace('\n', " · "),
233 json!({ "result": out }),
234 ),
235 Err(e) => OnceResult::error("gsoc-mine", format!("gsoc-mine failed: {e}")),
236 }
237}
238
239fn build_api() -> Result<Arc<ApiClient>, OnceResult> {
245 let config = match Config::new() {
246 Ok(c) => c,
247 Err(e) => {
248 return Err(OnceResult::usage(
249 "_config",
250 format!("could not load config: {e}"),
251 ));
252 }
253 };
254 let node = match config.active_node() {
255 Some(n) => n,
256 None => {
257 return Err(OnceResult::usage(
258 "_config",
259 "no Bee node configured (config.nodes is empty)",
260 ));
261 }
262 };
263 let api = match ApiClient::from_node(node) {
264 Ok(a) => Arc::new(a),
265 Err(e) => {
266 return Err(OnceResult::usage(
267 "_config",
268 format!("could not build api client: {e}"),
269 ));
270 }
271 };
272 Ok(api)
273}
274
275async fn once_readiness() -> OnceResult {
279 let api = match build_api() {
280 Ok(a) => a,
281 Err(r) => return r,
282 };
283 let bee = api.bee();
284 let debug = bee.debug();
285 let (health, topology) = tokio::join!(debug.health(), debug.topology());
286 let health = match health {
287 Ok(h) => h,
288 Err(e) => {
289 return OnceResult::error("readiness", format!("/health failed: {e}"));
290 }
291 };
292 let topology = match topology {
293 Ok(t) => t,
294 Err(e) => {
295 return OnceResult::error("readiness", format!("/topology failed: {e}"));
296 }
297 };
298 let depth = topology.depth as u32;
299 let depth_ok = (1..=30).contains(&depth);
300 let status_ok = health.status == "ok";
301 let data = json!({
302 "health_status": health.status,
303 "version": health.version,
304 "api_version": health.api_version,
305 "depth": depth,
306 "depth_ok": depth_ok,
307 "status_ok": status_ok,
308 });
309 if status_ok && depth_ok {
310 OnceResult::ok_with_data(
311 "readiness",
312 format!(
313 "READY · status={} · depth={depth} · version={}",
314 health.status, health.version
315 ),
316 data,
317 )
318 } else {
319 OnceResult::unhealthy(
320 "readiness",
321 format!(
322 "NOT READY · status={} · depth={depth} (need [1,30]) · version={}",
323 health.status, health.version
324 ),
325 data,
326 )
327 }
328}
329
330async fn once_version_check() -> OnceResult {
333 let api = match build_api() {
334 Ok(a) => a,
335 Err(r) => return r,
336 };
337 match api.bee().debug().health().await {
338 Ok(h) => OnceResult::ok_with_data(
339 "version-check",
340 format!("bee {} · api {}", h.version, h.api_version),
341 json!({
342 "version": h.version,
343 "api_version": h.api_version,
344 }),
345 ),
346 Err(e) => OnceResult::error("version-check", format!("/health failed: {e}")),
347 }
348}
349
350async fn once_inspect(args: &[String]) -> OnceResult {
353 let ref_arg = match args.first() {
354 Some(r) => r.as_str(),
355 None => return OnceResult::usage("inspect", "usage: --once inspect <ref>"),
356 };
357 let reference = match bee::swarm::Reference::from_hex(ref_arg.trim()) {
358 Ok(r) => r,
359 Err(e) => return OnceResult::usage("inspect", format!("bad ref: {e}")),
360 };
361 let api = match build_api() {
362 Ok(a) => a,
363 Err(r) => return r,
364 };
365 match manifest_walker::inspect(api, reference).await {
366 InspectResult::Manifest { node, bytes_len } => OnceResult::ok_with_data(
367 "inspect",
368 format!(
369 "manifest · {bytes_len} bytes · {} forks",
370 node.forks.len()
371 ),
372 json!({
373 "kind": "manifest",
374 "bytes": bytes_len,
375 "forks": node.forks.len(),
376 }),
377 ),
378 InspectResult::RawChunk { bytes_len } => OnceResult::ok_with_data(
379 "inspect",
380 format!("raw chunk · {bytes_len} bytes"),
381 json!({
382 "kind": "raw_chunk",
383 "bytes": bytes_len,
384 }),
385 ),
386 InspectResult::Error(e) => OnceResult::error("inspect", format!("inspect failed: {e}")),
387 }
388}
389
390async fn once_buy_preview(args: &[String]) -> OnceResult {
395 let (depth_str, amount_str) = match (args.first(), args.get(1)) {
396 (Some(d), Some(a)) => (d.as_str(), a.as_str()),
397 _ => {
398 return OnceResult::usage(
399 "buy-preview",
400 "usage: --once buy-preview <depth> <amount-plur>",
401 );
402 }
403 };
404 let depth: u8 = match depth_str.parse() {
405 Ok(d) => d,
406 Err(_) => return OnceResult::usage("buy-preview", format!("invalid depth: {depth_str}")),
407 };
408 let amount = match stamp_preview::parse_plur_amount(amount_str) {
409 Ok(a) => a,
410 Err(e) => return OnceResult::usage("buy-preview", e),
411 };
412 let api = match build_api() {
413 Ok(a) => a,
414 Err(r) => return r,
415 };
416 let chain = match api.bee().debug().chain_state().await {
417 Ok(c) => c,
418 Err(e) => {
419 return OnceResult::error("buy-preview", format!("/chainstate failed: {e}"));
420 }
421 };
422 match stamp_preview::buy_preview(depth, amount, &chain) {
423 Ok(p) => OnceResult::ok_with_data(
424 "buy-preview",
425 p.summary(),
426 json!({
427 "depth": p.depth,
428 "amount_plur": p.amount_plur.to_string(),
429 "ttl_seconds": p.ttl_seconds,
430 "cost_bzz": p.cost_bzz,
431 }),
432 ),
433 Err(e) => OnceResult::error("buy-preview", e),
434 }
435}
436
437async fn once_buy_suggest(args: &[String]) -> OnceResult {
441 let (size_str, duration_str) = match (args.first(), args.get(1)) {
442 (Some(s), Some(d)) => (s.as_str(), d.as_str()),
443 _ => {
444 return OnceResult::usage(
445 "buy-suggest",
446 "usage: --once buy-suggest <size> <duration> (e.g. 5GiB 30d)",
447 );
448 }
449 };
450 let target_bytes = match stamp_preview::parse_size_bytes(size_str) {
451 Ok(b) => b,
452 Err(e) => return OnceResult::usage("buy-suggest", e),
453 };
454 let target_seconds = match stamp_preview::parse_duration_seconds(duration_str) {
455 Ok(s) => s,
456 Err(e) => return OnceResult::usage("buy-suggest", e),
457 };
458 let api = match build_api() {
459 Ok(a) => a,
460 Err(r) => return r,
461 };
462 let chain = match api.bee().debug().chain_state().await {
463 Ok(c) => c,
464 Err(e) => {
465 return OnceResult::error("buy-suggest", format!("/chainstate failed: {e}"));
466 }
467 };
468 match stamp_preview::buy_suggest(target_bytes, target_seconds, &chain) {
469 Ok(p) => OnceResult::ok_with_data(
470 "buy-suggest",
471 p.summary(),
472 json!({
473 "target_bytes": p.target_bytes.to_string(),
474 "target_seconds": p.target_seconds,
475 "depth": p.depth,
476 "amount_plur": p.amount_plur.to_string(),
477 "capacity_bytes": p.capacity_bytes.to_string(),
478 "ttl_seconds": p.ttl_seconds,
479 "cost_bzz": p.cost_bzz,
480 }),
481 ),
482 Err(e) => OnceResult::error("buy-suggest", e),
483 }
484}
485
486async fn once_topup_preview(args: &[String]) -> OnceResult {
489 let (prefix, amount_str) = match (args.first(), args.get(1)) {
490 (Some(p), Some(a)) => (p.as_str(), a.as_str()),
491 _ => {
492 return OnceResult::usage(
493 "topup-preview",
494 "usage: --once topup-preview <batch-prefix> <amount-plur>",
495 );
496 }
497 };
498 let amount = match stamp_preview::parse_plur_amount(amount_str) {
499 Ok(a) => a,
500 Err(e) => return OnceResult::usage("topup-preview", e),
501 };
502 let api = match build_api() {
503 Ok(a) => a,
504 Err(r) => return r,
505 };
506 let (batches, chain) = match fetch_stamps_and_chain(&api).await {
507 Ok(p) => p,
508 Err(e) => return OnceResult::error("topup-preview", e),
509 };
510 let batch = match stamp_preview::match_batch_prefix(&batches, prefix) {
511 Ok(b) => b.clone(),
512 Err(e) => return OnceResult::usage("topup-preview", e),
513 };
514 match stamp_preview::topup_preview(&batch, amount, &chain) {
515 Ok(p) => OnceResult::ok_with_data(
516 "topup-preview",
517 p.summary(),
518 json!({
519 "batch_id": batch.batch_id.to_hex(),
520 "current_depth": p.current_depth,
521 "current_ttl_seconds": p.current_ttl_seconds,
522 "delta_amount_plur": p.delta_amount.to_string(),
523 "extra_ttl_seconds": p.extra_ttl_seconds,
524 "new_ttl_seconds": p.new_ttl_seconds,
525 "cost_bzz": p.cost_bzz,
526 }),
527 ),
528 Err(e) => OnceResult::error("topup-preview", e),
529 }
530}
531
532async fn once_dilute_preview(args: &[String]) -> OnceResult {
536 let (prefix, depth_str) = match (args.first(), args.get(1)) {
537 (Some(p), Some(d)) => (p.as_str(), d.as_str()),
538 _ => {
539 return OnceResult::usage(
540 "dilute-preview",
541 "usage: --once dilute-preview <batch-prefix> <new-depth>",
542 );
543 }
544 };
545 let new_depth: u8 = match depth_str.parse() {
546 Ok(d) => d,
547 Err(_) => {
548 return OnceResult::usage(
549 "dilute-preview",
550 format!("invalid depth: {depth_str}"),
551 );
552 }
553 };
554 let api = match build_api() {
555 Ok(a) => a,
556 Err(r) => return r,
557 };
558 let batches = match api.bee().postage().get_postage_batches().await {
559 Ok(b) => b,
560 Err(e) => return OnceResult::error("dilute-preview", format!("/stamps failed: {e}")),
561 };
562 let batch = match stamp_preview::match_batch_prefix(&batches, prefix) {
563 Ok(b) => b.clone(),
564 Err(e) => return OnceResult::usage("dilute-preview", e),
565 };
566 match stamp_preview::dilute_preview(&batch, new_depth) {
567 Ok(p) => OnceResult::ok_with_data(
568 "dilute-preview",
569 p.summary(),
570 json!({
571 "batch_id": batch.batch_id.to_hex(),
572 "old_depth": p.old_depth,
573 "new_depth": p.new_depth,
574 "old_ttl_seconds": p.old_ttl_seconds,
575 "new_ttl_seconds": p.new_ttl_seconds,
576 }),
577 ),
578 Err(e) => OnceResult::error("dilute-preview", e),
579 }
580}
581
582async fn once_extend_preview(args: &[String]) -> OnceResult {
586 let (prefix, duration_str) = match (args.first(), args.get(1)) {
587 (Some(p), Some(d)) => (p.as_str(), d.as_str()),
588 _ => {
589 return OnceResult::usage(
590 "extend-preview",
591 "usage: --once extend-preview <batch-prefix> <duration>",
592 );
593 }
594 };
595 let extension_seconds = match stamp_preview::parse_duration_seconds(duration_str) {
596 Ok(s) => s,
597 Err(e) => return OnceResult::usage("extend-preview", e),
598 };
599 let api = match build_api() {
600 Ok(a) => a,
601 Err(r) => return r,
602 };
603 let (batches, chain) = match fetch_stamps_and_chain(&api).await {
604 Ok(p) => p,
605 Err(e) => return OnceResult::error("extend-preview", e),
606 };
607 let batch = match stamp_preview::match_batch_prefix(&batches, prefix) {
608 Ok(b) => b.clone(),
609 Err(e) => return OnceResult::usage("extend-preview", e),
610 };
611 match stamp_preview::extend_preview(&batch, extension_seconds, &chain) {
612 Ok(p) => OnceResult::ok_with_data(
613 "extend-preview",
614 p.summary(),
615 json!({
616 "batch_id": batch.batch_id.to_hex(),
617 "depth": p.depth,
618 "current_ttl_seconds": p.current_ttl_seconds,
619 "needed_amount_plur": p.needed_amount_plur.to_string(),
620 "cost_bzz": p.cost_bzz,
621 "new_ttl_seconds": p.new_ttl_seconds,
622 }),
623 ),
624 Err(e) => OnceResult::error("extend-preview", e),
625 }
626}
627
628async fn once_plan_batch(args: &[String]) -> OnceResult {
633 let prefix = match args.first() {
634 Some(p) => p.as_str(),
635 None => {
636 return OnceResult::usage(
637 "plan-batch",
638 "usage: --once plan-batch <batch-prefix> [usage-thr] [ttl-thr] [extra-depth]",
639 );
640 }
641 };
642 let usage_thr = match args.get(1) {
643 Some(s) => match s.parse::<f64>() {
644 Ok(v) => v,
645 Err(_) => {
646 return OnceResult::usage(
647 "plan-batch",
648 format!("invalid usage-thr {s:?} (expected float in [0,1])"),
649 );
650 }
651 },
652 None => stamp_preview::DEFAULT_USAGE_THRESHOLD,
653 };
654 let ttl_thr = match args.get(2) {
655 Some(s) => match stamp_preview::parse_duration_seconds(s) {
656 Ok(v) => v,
657 Err(e) => return OnceResult::usage("plan-batch", format!("ttl-thr: {e}")),
658 },
659 None => stamp_preview::DEFAULT_TTL_THRESHOLD_SECONDS,
660 };
661 let extra_depth = match args.get(3) {
662 Some(s) => match s.parse::<u8>() {
663 Ok(v) => v,
664 Err(_) => {
665 return OnceResult::usage(
666 "plan-batch",
667 format!("invalid extra-depth {s:?}"),
668 );
669 }
670 },
671 None => stamp_preview::DEFAULT_EXTRA_DEPTH,
672 };
673 let api = match build_api() {
674 Ok(a) => a,
675 Err(r) => return r,
676 };
677 let (batches, chain) = match fetch_stamps_and_chain(&api).await {
678 Ok(p) => p,
679 Err(e) => return OnceResult::error("plan-batch", e),
680 };
681 let batch = match stamp_preview::match_batch_prefix(&batches, prefix) {
682 Ok(b) => b.clone(),
683 Err(e) => return OnceResult::usage("plan-batch", e),
684 };
685 match stamp_preview::plan_batch(&batch, &chain, usage_thr, ttl_thr, extra_depth) {
686 Ok(p) => {
687 let action_kind = match &p.action {
688 stamp_preview::PlanAction::None => "none",
689 stamp_preview::PlanAction::Topup { .. } => "topup",
690 stamp_preview::PlanAction::Dilute { .. } => "dilute",
691 stamp_preview::PlanAction::TopupThenDilute { .. } => "topup_then_dilute",
692 };
693 let data = json!({
694 "batch_id": batch.batch_id.to_hex(),
695 "current_depth": p.current_depth,
696 "current_usage_pct": p.current_usage_pct,
697 "current_ttl_seconds": p.current_ttl_seconds,
698 "usage_threshold_pct": p.usage_threshold_pct,
699 "ttl_threshold_seconds": p.ttl_threshold_seconds,
700 "extra_depth": p.extra_depth,
701 "action": action_kind,
702 "total_cost_bzz": p.total_cost_bzz,
703 "reason": p.reason.clone(),
704 });
705 if matches!(p.action, stamp_preview::PlanAction::None) {
709 OnceResult::ok_with_data("plan-batch", p.summary(), data)
710 } else {
711 OnceResult::unhealthy("plan-batch", p.summary(), data)
712 }
713 }
714 Err(e) => OnceResult::error("plan-batch", e),
715 }
716}
717
718async fn fetch_stamps_and_chain(
721 api: &Arc<ApiClient>,
722) -> Result<
723 (
724 Vec<bee::postage::PostageBatch>,
725 bee::debug::ChainState,
726 ),
727 String,
728> {
729 let bee = api.bee();
730 let postage = bee.postage();
731 let debug = bee.debug();
732 let (batches, chain) = tokio::join!(postage.get_postage_batches(), debug.chain_state());
733 let batches = batches.map_err(|e| format!("/stamps failed: {e}"))?;
734 let chain = chain.map_err(|e| format!("/chainstate failed: {e}"))?;
735 Ok((batches, chain))
736}
737
738async fn once_durability_check(args: &[String]) -> OnceResult {
741 let ref_arg = match args.first() {
742 Some(r) => r.as_str(),
743 None => {
744 return OnceResult::usage(
745 "durability-check",
746 "usage: --once durability-check <ref>",
747 );
748 }
749 };
750 let reference = match bee::swarm::Reference::from_hex(ref_arg.trim()) {
751 Ok(r) => r,
752 Err(e) => return OnceResult::usage("durability-check", format!("bad ref: {e}")),
753 };
754 let api = match build_api() {
755 Ok(a) => a,
756 Err(r) => return r,
757 };
758 let result = durability::check(api, reference).await;
759 let data = json!({
760 "chunks_total": result.chunks_total,
761 "chunks_lost": result.chunks_lost,
762 "chunks_errors": result.chunks_errors,
763 "duration_ms": result.duration_ms,
764 "root_is_manifest": result.root_is_manifest,
765 "truncated": result.truncated,
766 });
767 if result.is_healthy() {
768 OnceResult::ok_with_data("durability-check", result.summary(), data)
769 } else {
770 OnceResult::unhealthy("durability-check", result.summary(), data)
771 }
772}
773
774fn print_result(result: &OnceResult, json_output: bool) {
777 if json_output {
778 match serde_json::to_string(result) {
779 Ok(s) => println!("{s}"),
780 Err(e) => eprintln!("(failed to serialize result: {e})"),
781 }
782 return;
783 }
784 let prefix = match result.status {
785 OnceStatus::Ok => "OK",
786 OnceStatus::Unhealthy => "UNHEALTHY",
787 OnceStatus::Error => "ERROR",
788 OnceStatus::UsageError => "USAGE",
789 };
790 println!("[{prefix}] {}", result.message);
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796
797 fn args(s: &[&str]) -> Vec<String> {
798 s.iter().map(|x| x.to_string()).collect()
799 }
800
801 #[test]
802 fn unknown_verb_returns_usage_error() {
803 let r = once_pss_target(&[]);
804 assert!(matches!(r.status, OnceStatus::UsageError));
805 assert!(r.message.contains("usage"), "{}", r.message);
806 }
807
808 #[test]
809 fn cid_handler_round_trips() {
810 let r = once_cid(&args(&[&"0".repeat(64), "feed"]));
811 assert!(matches!(r.status, OnceStatus::Ok));
812 assert!(r.message.contains("cid:"), "{}", r.message);
813 assert!(r.data["cid"].is_string());
815 }
816
817 #[test]
818 fn cid_handler_rejects_garbage() {
819 let r = once_cid(&args(&["not-hex"]));
820 assert!(matches!(r.status, OnceStatus::Error));
821 }
822
823 #[test]
824 fn cid_handler_no_args_is_usage_error() {
825 let r = once_cid(&[]);
826 assert!(matches!(r.status, OnceStatus::UsageError));
827 }
828
829 #[test]
830 fn pss_target_extracts_prefix() {
831 let r = once_pss_target(&args(&[
832 "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
833 ]));
834 assert!(matches!(r.status, OnceStatus::Ok));
835 assert!(r.message.contains("abcd"), "{}", r.message);
836 }
837
838 #[test]
839 fn depth_table_renders_full_table() {
840 let r = once_depth_table();
841 assert!(matches!(r.status, OnceStatus::Ok));
842 assert!(r.message.contains("depth"));
843 assert!(r.message.contains("17"));
844 assert!(r.message.contains("34"));
845 }
846
847 #[test]
848 fn exit_codes_map_correctly() {
849 assert_eq!(
850 OnceStatus::Ok.exit_code(),
851 std::process::ExitCode::SUCCESS
852 );
853 let _ = OnceStatus::Unhealthy.exit_code();
857 let _ = OnceStatus::Error.exit_code();
858 let _ = OnceStatus::UsageError.exit_code();
859 }
860
861 #[test]
862 fn ok_helpers_compose_the_expected_shape() {
863 let r = OnceResult::ok("v", "all good");
864 assert_eq!(r.verb, "v");
865 assert!(matches!(r.status, OnceStatus::Ok));
866 assert_eq!(r.message, "all good");
867 assert!(r.data.is_null());
868
869 let r2 = OnceResult::unhealthy("v", "broken", json!({"x": 1}));
870 assert!(matches!(r2.status, OnceStatus::Unhealthy));
871 assert_eq!(r2.data["x"], 1);
872 }
873
874 #[test]
875 fn print_result_json_output_is_one_line() {
876 let r = OnceResult::ok("hash", "hash X: abc");
879 print_result(&r, true);
880 print_result(&r, false);
881 }
882}