1use std::path::{Path, PathBuf};
18
19use crate::broker::backend_lifecycle::verify_pid::process_is_alive;
20use crate::broker::server::service_def_loader::SERVICE_DEF_EXTENSION;
21use crate::client::paths;
22
23pub const EMERGENCY_RESERVE_FILE_NAME: &str = "emergency-reserve.bin";
28pub const EMERGENCY_RESERVE_BYTES: u64 = 32 * 1024 * 1024;
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum ArtifactStatus {
35 Clean,
37 Active,
39 Present,
42 Stale,
45 Orphaned,
47 Error,
49}
50
51impl ArtifactStatus {
52 pub fn as_str(self) -> &'static str {
54 match self {
55 ArtifactStatus::Clean => "CLEAN",
56 ArtifactStatus::Active => "ACTIVE",
57 ArtifactStatus::Present => "PRESENT",
58 ArtifactStatus::Stale => "STALE",
59 ArtifactStatus::Orphaned => "ORPHANED",
60 ArtifactStatus::Error => "ERROR",
61 }
62 }
63
64 pub fn is_finding(self) -> bool {
66 matches!(
67 self,
68 ArtifactStatus::Stale | ArtifactStatus::Orphaned | ArtifactStatus::Error
69 )
70 }
71}
72
73#[derive(Clone, Debug)]
75pub struct ArtifactCheck {
76 pub class: &'static str,
78 pub location: String,
80 pub status: ArtifactStatus,
82 pub detail: String,
84}
85
86impl ArtifactCheck {
87 fn new(
88 class: &'static str,
89 location: impl Into<String>,
90 status: ArtifactStatus,
91 detail: impl Into<String>,
92 ) -> Self {
93 Self {
94 class,
95 location: location.into(),
96 status,
97 detail: detail.into(),
98 }
99 }
100}
101
102#[derive(Clone, Debug, Default)]
104pub struct ArtifactReport {
105 pub checks: Vec<ArtifactCheck>,
107}
108
109impl ArtifactReport {
110 pub fn finding_count(&self) -> usize {
112 self.checks
113 .iter()
114 .filter(|check| check.status.is_finding())
115 .count()
116 }
117
118 pub fn exit_code(&self) -> i32 {
122 if self
123 .checks
124 .iter()
125 .any(|check| check.status == ArtifactStatus::Error)
126 {
127 1
128 } else {
129 0
130 }
131 }
132
133 pub fn to_json_value(&self) -> serde_json::Value {
135 let checks: Vec<serde_json::Value> = self
136 .checks
137 .iter()
138 .map(|check| {
139 serde_json::json!({
140 "class": check.class,
141 "location": check.location,
142 "status": check.status.as_str(),
143 "detail": check.detail,
144 })
145 })
146 .collect();
147 serde_json::json!({
148 "schema_version": 1,
149 "exit_code": self.exit_code(),
150 "findings": self.finding_count(),
151 "checks": checks,
152 })
153 }
154
155 pub fn render_text(&self) -> String {
157 let class_width = self
158 .checks
159 .iter()
160 .map(|check| check.class.len())
161 .max()
162 .unwrap_or(0);
163 let mut out = String::new();
164 for check in &self.checks {
165 out.push_str(&format!(
166 "{:<8} {:<class_width$} {} {}\n",
167 check.status.as_str(),
168 check.class,
169 check.location,
170 check.detail,
171 ));
172 }
173 out.push_str(&format!(
174 "cleanup verify: {} location(s) — {} finding(s)\n",
175 self.checks.len(),
176 self.finding_count()
177 ));
178 out
179 }
180}
181
182#[derive(Clone, Debug)]
184pub enum SocketLocation {
185 File(PathBuf),
187 NamedPipe(String),
189}
190
191#[derive(Clone, Debug)]
195pub struct ArtifactPaths {
196 pub socket: SocketLocation,
198 pub pid_file: PathBuf,
200 pub db: PathBuf,
202 pub data_dir: PathBuf,
204 pub emergency_reserve: PathBuf,
206 pub emergency_reserve_bytes: u64,
208 pub service_definition_dir: PathBuf,
210 pub shadow_dir: PathBuf,
212}
213
214impl ArtifactPaths {
215 pub fn from_environment(scope_hash: Option<&str>) -> Self {
217 let endpoint = paths::socket_path_view(scope_hash);
218 let socket = if cfg!(windows) {
219 SocketLocation::NamedPipe(endpoint)
220 } else {
221 SocketLocation::File(PathBuf::from(endpoint))
222 };
223 let data_dir = paths::data_dir();
224 Self {
225 socket,
226 pid_file: paths::pid_file_path_view(scope_hash),
227 db: paths::db_path_view(scope_hash),
228 emergency_reserve: data_dir.join(EMERGENCY_RESERVE_FILE_NAME),
229 emergency_reserve_bytes: EMERGENCY_RESERVE_BYTES,
230 data_dir,
231 service_definition_dir:
232 crate::broker::server::service_def_loader::service_definition_dir(),
233 shadow_dir: paths::shadow_dir_view(),
234 }
235 }
236}
237
238pub fn run(paths: &ArtifactPaths) -> ArtifactReport {
240 let connect =
241 |endpoint: &str| crate::broker::client::connect_local_socket(endpoint).map(|_stream| ());
242 run_with_probes(paths, &process_is_alive, &connect)
243}
244
245pub fn run_with_probes(
247 paths: &ArtifactPaths,
248 pid_is_alive: &dyn Fn(u32) -> bool,
249 connect: &dyn Fn(&str) -> std::io::Result<()>,
250) -> ArtifactReport {
251 let pid_check = check_pid_file(&paths.pid_file, pid_is_alive);
252 let daemon_alive = pid_check.status == ArtifactStatus::Active;
253 let mut checks = vec![check_socket(&paths.socket, connect), pid_check];
254 checks.extend(check_service_definitions(&paths.service_definition_dir));
255 checks.extend(check_database(&paths.db, daemon_alive));
256 checks.push(check_logs(&paths.data_dir));
257 checks.push(check_emergency_reserve(
258 &paths.emergency_reserve,
259 paths.emergency_reserve_bytes,
260 ));
261 checks.push(check_shadow_dir(&paths.shadow_dir, daemon_alive));
262 ArtifactReport { checks }
263}
264
265pub fn check_socket(
267 socket: &SocketLocation,
268 connect: &dyn Fn(&str) -> std::io::Result<()>,
269) -> ArtifactCheck {
270 const CLASS: &str = "socket";
271 match socket {
272 SocketLocation::NamedPipe(name) => match connect(name) {
273 Ok(()) => ArtifactCheck::new(
274 CLASS,
275 name.clone(),
276 ArtifactStatus::Active,
277 "named pipe accepts connections",
278 ),
279 Err(_) => ArtifactCheck::new(
280 CLASS,
281 name.clone(),
282 ArtifactStatus::Clean,
283 "no listener (named pipes leave no filesystem residue)",
284 ),
285 },
286 SocketLocation::File(path) => {
287 if !path.exists() {
288 return ArtifactCheck::new(
289 CLASS,
290 path.display().to_string(),
291 ArtifactStatus::Clean,
292 "socket file absent",
293 );
294 }
295 match connect(&path.to_string_lossy()) {
296 Ok(()) => ArtifactCheck::new(
297 CLASS,
298 path.display().to_string(),
299 ArtifactStatus::Active,
300 "socket file exists and accepts connections",
301 ),
302 Err(err) => ArtifactCheck::new(
303 CLASS,
304 path.display().to_string(),
305 ArtifactStatus::Stale,
306 format!("socket file exists but nothing is listening ({err})"),
307 ),
308 }
309 }
310 }
311}
312
313pub fn check_pid_file(path: &Path, pid_is_alive: &dyn Fn(u32) -> bool) -> ArtifactCheck {
315 const CLASS: &str = "pid-file";
316 let location = path.display().to_string();
317 let contents = match std::fs::read_to_string(path) {
318 Ok(contents) => contents,
319 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
320 return ArtifactCheck::new(CLASS, location, ArtifactStatus::Clean, "pid file absent");
321 }
322 Err(err) => {
323 return ArtifactCheck::new(
324 CLASS,
325 location,
326 ArtifactStatus::Error,
327 format!("cannot read pid file: {err}"),
328 );
329 }
330 };
331 match contents.trim().parse::<u32>() {
332 Ok(pid) if pid_is_alive(pid) => ArtifactCheck::new(
333 CLASS,
334 location,
335 ArtifactStatus::Active,
336 format!("daemon pid {pid} is alive"),
337 ),
338 Ok(pid) => ArtifactCheck::new(
339 CLASS,
340 location,
341 ArtifactStatus::Stale,
342 format!("pid {pid} is not alive"),
343 ),
344 Err(_) => ArtifactCheck::new(
345 CLASS,
346 location,
347 ArtifactStatus::Stale,
348 format!("unparsable pid file contents {:?}", contents.trim()),
349 ),
350 }
351}
352
353pub fn check_service_definitions(dir: &Path) -> Vec<ArtifactCheck> {
356 const CLASS: &str = "servicedef";
357 let location = dir.display().to_string();
358 if !dir.exists() {
359 return vec![ArtifactCheck::new(
360 CLASS,
361 location,
362 ArtifactStatus::Clean,
363 "service-definition directory absent (no services installed)",
364 )];
365 }
366 let entries = match std::fs::read_dir(dir) {
367 Ok(entries) => entries,
368 Err(err) => {
369 return vec![ArtifactCheck::new(
370 CLASS,
371 location,
372 ArtifactStatus::Error,
373 format!("cannot enumerate directory: {err}"),
374 )];
375 }
376 };
377 let mut paths: Vec<PathBuf> = entries
378 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
379 .collect();
380 paths.sort();
381 let mut definitions = 0usize;
382 let mut checks = Vec::new();
383 for path in &paths {
384 let is_definition = path
385 .extension()
386 .map(|ext| ext == SERVICE_DEF_EXTENSION)
387 .unwrap_or(false);
388 if is_definition {
389 definitions += 1;
390 } else {
391 checks.push(ArtifactCheck::new(
392 CLASS,
393 path.display().to_string(),
394 ArtifactStatus::Orphaned,
395 format!("unexpected non-.{SERVICE_DEF_EXTENSION} entry in service-definition dir"),
396 ));
397 }
398 }
399 checks.insert(
400 0,
401 ArtifactCheck::new(
402 CLASS,
403 location,
404 ArtifactStatus::Present,
405 format!("{definitions} .{SERVICE_DEF_EXTENSION} file(s) (persistent config, expected)"),
406 ),
407 );
408 checks
409}
410
411pub fn check_database(db: &Path, daemon_alive: bool) -> Vec<ArtifactCheck> {
413 const CLASS: &str = "database";
414 let mut checks = Vec::new();
415 let location = db.display().to_string();
416 if db.exists() {
417 let size = std::fs::metadata(db).map(|meta| meta.len()).unwrap_or(0);
418 checks.push(ArtifactCheck::new(
419 CLASS,
420 location,
421 ArtifactStatus::Present,
422 format!("registry database exists ({size} bytes; persists across daemon runs)"),
423 ));
424 } else {
425 checks.push(ArtifactCheck::new(
426 CLASS,
427 location,
428 ArtifactStatus::Clean,
429 "registry database absent (daemon never ran in this scope)",
430 ));
431 }
432 for suffix in ["-wal", "-shm"] {
433 let mut name = db.as_os_str().to_os_string();
434 name.push(suffix);
435 let sidecar = PathBuf::from(name);
436 if !sidecar.exists() {
437 continue;
438 }
439 let location = sidecar.display().to_string();
440 if daemon_alive {
441 checks.push(ArtifactCheck::new(
442 CLASS,
443 location,
444 ArtifactStatus::Active,
445 format!("sqlite {suffix} sidecar held by the live daemon"),
446 ));
447 } else {
448 checks.push(ArtifactCheck::new(
449 CLASS,
450 location,
451 ArtifactStatus::Stale,
452 format!(
453 "sqlite {suffix} sidecar left behind with no live daemon (unclean shutdown)"
454 ),
455 ));
456 }
457 }
458 checks
459}
460
461pub fn check_logs(data_dir: &Path) -> ArtifactCheck {
464 const CLASS: &str = "logs";
465 let location = data_dir.display().to_string();
466 if !data_dir.exists() {
467 return ArtifactCheck::new(
468 CLASS,
469 location,
470 ArtifactStatus::Clean,
471 "data directory absent (no log files)",
472 );
473 }
474 let entries = match std::fs::read_dir(data_dir) {
475 Ok(entries) => entries,
476 Err(err) => {
477 return ArtifactCheck::new(
478 CLASS,
479 location,
480 ArtifactStatus::Error,
481 format!("cannot enumerate data directory: {err}"),
482 );
483 }
484 };
485 let mut count = 0usize;
486 let mut bytes = 0u64;
487 for path in entries.filter_map(|entry| entry.ok().map(|entry| entry.path())) {
488 if path.extension().map(|ext| ext == "log").unwrap_or(false) {
489 count += 1;
490 bytes += std::fs::metadata(&path).map(|meta| meta.len()).unwrap_or(0);
491 }
492 }
493 if count == 0 {
494 ArtifactCheck::new(CLASS, location, ArtifactStatus::Clean, "no *.log files")
495 } else {
496 ArtifactCheck::new(
497 CLASS,
498 location,
499 ArtifactStatus::Present,
500 format!("{count} *.log file(s), {bytes} bytes total (reported, not deleted)"),
501 )
502 }
503}
504
505pub fn check_emergency_reserve(path: &Path, expected_bytes: u64) -> ArtifactCheck {
507 const CLASS: &str = "emergency-reserve";
508 let location = path.display().to_string();
509 match std::fs::metadata(path) {
510 Ok(meta) if meta.len() == expected_bytes => ArtifactCheck::new(
511 CLASS,
512 location,
513 ArtifactStatus::Present,
514 format!("armed at {expected_bytes} bytes (recreated at every daemon startup)"),
515 ),
516 Ok(meta) => ArtifactCheck::new(
517 CLASS,
518 location,
519 ArtifactStatus::Stale,
520 format!(
521 "unexpected size {} bytes (expected {expected_bytes}); partial pre-allocation \
522 from a crashed startup",
523 meta.len()
524 ),
525 ),
526 Err(err) if err.kind() == std::io::ErrorKind::NotFound => ArtifactCheck::new(
527 CLASS,
528 location,
529 ArtifactStatus::Clean,
530 "absent (released after ENOSPC or daemon never ran; re-armed at next startup)",
531 ),
532 Err(err) => ArtifactCheck::new(
533 CLASS,
534 location,
535 ArtifactStatus::Error,
536 format!("cannot inspect reserve file: {err}"),
537 ),
538 }
539}
540
541pub fn check_shadow_dir(dir: &Path, daemon_alive: bool) -> ArtifactCheck {
543 const CLASS: &str = "shadow";
544 let location = dir.display().to_string();
545 if !dir.exists() {
546 return ArtifactCheck::new(
547 CLASS,
548 location,
549 ArtifactStatus::Clean,
550 "shadow directory absent",
551 );
552 }
553 let entries = match std::fs::read_dir(dir) {
554 Ok(entries) => entries,
555 Err(err) => {
556 return ArtifactCheck::new(
557 CLASS,
558 location,
559 ArtifactStatus::Error,
560 format!("cannot enumerate shadow directory: {err}"),
561 );
562 }
563 };
564 let count = entries.filter_map(|entry| entry.ok()).count();
565 if count == 0 {
566 ArtifactCheck::new(CLASS, location, ArtifactStatus::Clean, "empty")
567 } else if daemon_alive {
568 ArtifactCheck::new(
569 CLASS,
570 location,
571 ArtifactStatus::Active,
572 format!(
573 "{count} entr{} (may include the running daemon's shadow copy)",
574 if count == 1 { "y" } else { "ies" }
575 ),
576 )
577 } else {
578 ArtifactCheck::new(
579 CLASS,
580 location,
581 ArtifactStatus::Present,
582 format!(
583 "{count} entr{} with no live daemon (shadow copies persist by design; \
584 prune manually if disk space matters)",
585 if count == 1 { "y" } else { "ies" }
586 ),
587 )
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 fn temp_dir(label: &str) -> PathBuf {
596 let dir = std::env::temp_dir().join(format!(
597 "rp-verify-artifacts-{label}-{}-{:?}",
598 std::process::id(),
599 std::thread::current().id()
600 ));
601 let _ = std::fs::remove_dir_all(&dir);
602 std::fs::create_dir_all(&dir).unwrap();
603 dir
604 }
605
606 #[test]
607 fn pid_file_absent_is_clean() {
608 let dir = temp_dir("pid-clean");
609 let check = check_pid_file(&dir.join("daemon.pid"), &|_| true);
610 assert_eq!(check.status, ArtifactStatus::Clean);
611 let _ = std::fs::remove_dir_all(&dir);
612 }
613
614 #[test]
615 fn pid_file_live_pid_is_active_dead_pid_is_stale() {
616 let dir = temp_dir("pid-live");
617 let path = dir.join("daemon.pid");
618 std::fs::write(&path, "4242\n").unwrap();
619 assert_eq!(
620 check_pid_file(&path, &|pid| pid == 4242).status,
621 ArtifactStatus::Active
622 );
623 assert_eq!(
624 check_pid_file(&path, &|_| false).status,
625 ArtifactStatus::Stale
626 );
627 let _ = std::fs::remove_dir_all(&dir);
628 }
629
630 #[test]
631 fn pid_file_garbage_is_stale() {
632 let dir = temp_dir("pid-garbage");
633 let path = dir.join("daemon.pid");
634 std::fs::write(&path, "not-a-pid").unwrap();
635 let check = check_pid_file(&path, &|_| true);
636 assert_eq!(check.status, ArtifactStatus::Stale);
637 assert!(check.detail.contains("unparsable"));
638 let _ = std::fs::remove_dir_all(&dir);
639 }
640
641 #[test]
642 fn socket_file_states() {
643 let dir = temp_dir("socket");
644 let path = dir.join("daemon.sock");
645 let absent = SocketLocation::File(path.clone());
646 assert_eq!(
647 check_socket(&absent, &|_| Ok(())).status,
648 ArtifactStatus::Clean
649 );
650
651 std::fs::write(&path, b"").unwrap();
652 assert_eq!(
653 check_socket(&SocketLocation::File(path.clone()), &|_| Ok(())).status,
654 ArtifactStatus::Active
655 );
656 let refused = |_endpoint: &str| -> std::io::Result<()> {
657 Err(std::io::Error::from(std::io::ErrorKind::ConnectionRefused))
658 };
659 let check = check_socket(&SocketLocation::File(path), &refused);
660 assert_eq!(check.status, ArtifactStatus::Stale);
661 assert!(check.detail.contains("nothing is listening"));
662 let _ = std::fs::remove_dir_all(&dir);
663 }
664
665 #[test]
666 fn named_pipe_states() {
667 let pipe = SocketLocation::NamedPipe(r"\\.\pipe\rp-test".into());
668 assert_eq!(
669 check_socket(&pipe, &|_| Ok(())).status,
670 ArtifactStatus::Active
671 );
672 let gone = |_endpoint: &str| -> std::io::Result<()> {
673 Err(std::io::Error::from(std::io::ErrorKind::NotFound))
674 };
675 assert_eq!(check_socket(&pipe, &gone).status, ArtifactStatus::Clean);
676 }
677
678 #[test]
679 fn service_definitions_report_files_and_orphans() {
680 let dir = temp_dir("servicedef");
681 std::fs::write(dir.join("svc.servicedef"), b"x").unwrap();
682 std::fs::write(dir.join("stray.txt"), b"x").unwrap();
683 let checks = check_service_definitions(&dir);
684 assert_eq!(checks[0].status, ArtifactStatus::Present);
685 assert!(checks[0].detail.contains("1 .servicedef"));
686 let orphan = checks
687 .iter()
688 .find(|check| check.status == ArtifactStatus::Orphaned)
689 .expect("stray file flagged");
690 assert!(orphan.location.contains("stray.txt"));
691 let _ = std::fs::remove_dir_all(&dir);
692 }
693
694 #[test]
695 fn service_definition_dir_absent_is_clean() {
696 let dir = temp_dir("servicedef-absent");
697 let checks = check_service_definitions(&dir.join("missing"));
698 assert_eq!(checks.len(), 1);
699 assert_eq!(checks[0].status, ArtifactStatus::Clean);
700 let _ = std::fs::remove_dir_all(&dir);
701 }
702
703 #[test]
704 fn database_and_sidecars_reconcile_against_liveness() {
705 let dir = temp_dir("db");
706 let db = dir.join("tracked-pids.sqlite3");
707 assert_eq!(check_database(&db, false)[0].status, ArtifactStatus::Clean);
708
709 std::fs::write(&db, b"db").unwrap();
710 std::fs::write(dir.join("tracked-pids.sqlite3-wal"), b"wal").unwrap();
711 std::fs::write(dir.join("tracked-pids.sqlite3-shm"), b"shm").unwrap();
712
713 let dead = check_database(&db, false);
714 assert_eq!(dead[0].status, ArtifactStatus::Present);
715 assert_eq!(dead.len(), 3);
716 assert!(dead[1..]
717 .iter()
718 .all(|check| check.status == ArtifactStatus::Stale));
719
720 let alive = check_database(&db, true);
721 assert!(alive[1..]
722 .iter()
723 .all(|check| check.status == ArtifactStatus::Active));
724 let _ = std::fs::remove_dir_all(&dir);
725 }
726
727 #[test]
728 fn logs_counted_not_deleted() {
729 let dir = temp_dir("logs");
730 assert_eq!(check_logs(&dir).status, ArtifactStatus::Clean);
731 std::fs::write(dir.join("daemon.log"), b"0123456789").unwrap();
732 let check = check_logs(&dir);
733 assert_eq!(check.status, ArtifactStatus::Present);
734 assert!(check.detail.contains("1 *.log file(s), 10 bytes"));
735 assert!(dir.join("daemon.log").exists());
736 let _ = std::fs::remove_dir_all(&dir);
737 }
738
739 #[test]
740 fn emergency_reserve_states() {
741 let dir = temp_dir("reserve");
742 let path = dir.join(EMERGENCY_RESERVE_FILE_NAME);
743 assert_eq!(
744 check_emergency_reserve(&path, 1024).status,
745 ArtifactStatus::Clean
746 );
747 std::fs::write(&path, vec![0u8; 1024]).unwrap();
748 assert_eq!(
749 check_emergency_reserve(&path, 1024).status,
750 ArtifactStatus::Present
751 );
752 let check = check_emergency_reserve(&path, 2048);
753 assert_eq!(check.status, ArtifactStatus::Stale);
754 assert!(check.detail.contains("1024"));
755 let _ = std::fs::remove_dir_all(&dir);
756 }
757
758 #[test]
759 fn shadow_dir_states() {
760 let dir = temp_dir("shadow");
761 assert_eq!(
762 check_shadow_dir(&dir.join("missing"), false).status,
763 ArtifactStatus::Clean
764 );
765 assert_eq!(check_shadow_dir(&dir, false).status, ArtifactStatus::Clean);
766 std::fs::write(dir.join("daemon-abc123.exe"), b"x").unwrap();
767 assert_eq!(
768 check_shadow_dir(&dir, false).status,
769 ArtifactStatus::Present
770 );
771 assert_eq!(check_shadow_dir(&dir, true).status, ArtifactStatus::Active);
772 let _ = std::fs::remove_dir_all(&dir);
773 }
774
775 #[test]
776 fn report_exit_code_and_findings() {
777 let mut report = ArtifactReport::default();
778 report.checks.push(ArtifactCheck::new(
779 "pid-file",
780 "p",
781 ArtifactStatus::Stale,
782 "d",
783 ));
784 assert_eq!(report.finding_count(), 1);
785 assert_eq!(report.exit_code(), 0);
786 report
787 .checks
788 .push(ArtifactCheck::new("logs", "p", ArtifactStatus::Error, "d"));
789 assert_eq!(report.exit_code(), 1);
790 let json = report.to_json_value();
791 assert_eq!(json["schema_version"], 1);
792 assert_eq!(json["findings"], 2);
793 assert_eq!(json["checks"].as_array().unwrap().len(), 2);
794 let text = report.render_text();
795 assert!(text.contains("cleanup verify: 2 location(s) — 2 finding(s)"));
796 }
797
798 #[test]
799 fn from_environment_creates_nothing() {
800 let paths = ArtifactPaths::from_environment(Some("0123456789abcdef"));
802 assert!(paths
803 .pid_file
804 .to_string_lossy()
805 .contains("0123456789abcdef"));
806 assert!(paths.db.to_string_lossy().contains("0123456789abcdef"));
807 }
808
809 #[cfg(feature = "daemon")]
810 #[test]
811 fn reserve_constants_match_daemon_module() {
812 assert_eq!(
813 EMERGENCY_RESERVE_FILE_NAME,
814 crate::daemon::emergency_reserve::EMERGENCY_RESERVE_FILE_NAME
815 );
816 assert_eq!(
817 EMERGENCY_RESERVE_BYTES,
818 crate::daemon::emergency_reserve::EMERGENCY_RESERVE_BYTES
819 );
820 }
821}