service.rs (23473B)
1 use std::{ffi::OsString, path::PathBuf}; 2 3 use serde::Serialize; 4 use thiserror::Error; 5 6 use crate::{ 7 RadrootsMigrationReport, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, 8 RadrootsPaths, RadrootsRuntimeNamespace, RadrootsRuntimePathsError, 9 }; 10 11 #[derive(Debug, Clone, PartialEq, Eq)] 12 pub struct RadrootsRuntimePathSelection { 13 pub profile: RadrootsPathProfile, 14 pub profile_source: String, 15 pub repo_local_root: Option<PathBuf>, 16 pub repo_local_root_source: Option<String>, 17 } 18 19 #[derive(Debug, Clone, PartialEq, Eq)] 20 pub struct RadrootsRuntimePathConfigEntry { 21 pub key: String, 22 pub value: String, 23 pub source_label: String, 24 } 25 26 impl RadrootsRuntimePathConfigEntry { 27 #[must_use] 28 pub fn new( 29 key: impl Into<String>, 30 value: impl Into<String>, 31 source_label: impl Into<String>, 32 ) -> Self { 33 Self { 34 key: key.into(), 35 value: value.into(), 36 source_label: source_label.into(), 37 } 38 } 39 } 40 41 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 42 pub struct RadrootsRuntimeSelectionContract { 43 pub active_profile: String, 44 pub allowed_profiles: Vec<String>, 45 pub path_overrides: RadrootsRuntimeSelectionOverrideContract, 46 } 47 48 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 49 pub struct RadrootsRuntimeSelectionOverrideContract { 50 pub profile_source: String, 51 pub root_source: String, 52 #[serde(skip_serializing_if = "Option::is_none")] 53 pub repo_local_root: Option<PathBuf>, 54 #[serde(skip_serializing_if = "Option::is_none")] 55 pub repo_local_root_source: Option<String>, 56 pub subordinate_path_override_source: String, 57 pub subordinate_path_override_keys: Vec<String>, 58 } 59 60 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 61 pub struct RadrootsRuntimePathPolicyContract { 62 pub canonical_root_selection: String, 63 pub canonical_subordinate_path_override: String, 64 pub leaf_path_env_posture: String, 65 pub compatibility_leaf_path_keys: Vec<String>, 66 } 67 68 impl RadrootsRuntimePathPolicyContract { 69 pub fn new( 70 canonical_root_selection: &str, 71 canonical_subordinate_path_override: &str, 72 leaf_path_env_posture: &str, 73 compatibility_leaf_path_keys: &[&str], 74 ) -> Self { 75 Self { 76 canonical_root_selection: canonical_root_selection.to_owned(), 77 canonical_subordinate_path_override: canonical_subordinate_path_override.to_owned(), 78 leaf_path_env_posture: leaf_path_env_posture.to_owned(), 79 compatibility_leaf_path_keys: compatibility_leaf_path_keys 80 .iter() 81 .map(|entry| (*entry).to_owned()) 82 .collect(), 83 } 84 } 85 } 86 87 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 88 pub struct RadrootsRuntimeMigrationContract { 89 pub posture: String, 90 pub state: String, 91 pub silent_startup_relocation: bool, 92 pub compatibility_window: String, 93 pub detected_legacy_paths: Vec<RadrootsRuntimeLegacyPathContract>, 94 } 95 96 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 97 pub struct RadrootsRuntimeLegacyPathContract { 98 pub id: String, 99 pub description: String, 100 pub path: PathBuf, 101 #[serde(skip_serializing_if = "Option::is_none")] 102 pub destination: Option<PathBuf>, 103 pub import_hint: String, 104 } 105 106 pub fn runtime_migration_contract( 107 report: RadrootsMigrationReport, 108 ) -> RadrootsRuntimeMigrationContract { 109 RadrootsRuntimeMigrationContract { 110 posture: report.posture.to_owned(), 111 state: report.state.to_owned(), 112 silent_startup_relocation: report.silent_startup_relocation, 113 compatibility_window: report.compatibility_window.to_owned(), 114 detected_legacy_paths: report 115 .detected_legacy_paths 116 .into_iter() 117 .map(|path| RadrootsRuntimeLegacyPathContract { 118 id: path.id, 119 description: path.description, 120 path: path.path, 121 destination: path.destination, 122 import_hint: path.import_hint, 123 }) 124 .collect(), 125 } 126 } 127 128 #[derive(Debug, Error, Clone, PartialEq, Eq)] 129 pub enum RadrootsRuntimePathSelectionError { 130 #[error("{env_var} must be valid utf-8 when set")] 131 NonUnicodeEnv { env_var: String }, 132 133 #[error( 134 "{env_var} must be `interactive_user`, `service_host`, or `repo_local`; found `{value}`" 135 )] 136 InvalidProfileEnv { env_var: String, value: String }, 137 138 #[error( 139 "profile must be `interactive_user`, `service_host`, `repo_local`, or `mobile_native`; found `{value}`" 140 )] 141 InvalidProfileValue { value: String }, 142 143 #[error("{repo_local_root_env} must be set when {profile_env}=repo_local")] 144 MissingRepoLocalRoot { 145 profile_env: String, 146 repo_local_root_env: String, 147 }, 148 149 #[error(transparent)] 150 Paths(#[from] RadrootsRuntimePathsError), 151 } 152 153 impl RadrootsRuntimePathSelection { 154 pub fn caller(profile: RadrootsPathProfile, repo_local_root: Option<PathBuf>) -> Self { 155 Self { 156 profile, 157 profile_source: "caller".to_owned(), 158 repo_local_root_source: repo_local_root.as_ref().map(|_| "caller".to_owned()), 159 repo_local_root, 160 } 161 } 162 163 pub fn from_profile_value( 164 profile: &str, 165 repo_local_root: Option<PathBuf>, 166 ) -> Result<Self, RadrootsRuntimePathSelectionError> { 167 Ok(Self::caller(parse_profile_value(profile)?, repo_local_root)) 168 } 169 170 pub fn from_config_entries( 171 profile_entry: Option<RadrootsRuntimePathConfigEntry>, 172 repo_local_root_entry: Option<RadrootsRuntimePathConfigEntry>, 173 default_profile: RadrootsPathProfile, 174 ) -> Result<Self, RadrootsRuntimePathSelectionError> { 175 let (profile, profile_source) = match profile_entry { 176 Some(entry) => ( 177 parse_profile(entry.key.as_str(), entry.value.as_str())?, 178 entry.source_label, 179 ), 180 None => (default_profile, "default".to_owned()), 181 }; 182 let (repo_local_root, repo_local_root_source) = match repo_local_root_entry { 183 Some(entry) => (Some(PathBuf::from(entry.value)), Some(entry.source_label)), 184 None => (None, None), 185 }; 186 Ok(Self { 187 profile, 188 profile_source, 189 repo_local_root, 190 repo_local_root_source, 191 }) 192 } 193 194 pub fn from_env( 195 profile_env: &'static str, 196 repo_local_root_env: &'static str, 197 default_profile: RadrootsPathProfile, 198 ) -> Result<Self, RadrootsRuntimePathSelectionError> { 199 Self::from_env_values( 200 profile_env, 201 std::env::var(profile_env), 202 repo_local_root_env, 203 std::env::var_os(repo_local_root_env), 204 default_profile, 205 ) 206 } 207 208 fn from_env_values( 209 profile_env: &'static str, 210 profile_value: Result<String, std::env::VarError>, 211 repo_local_root_env: &'static str, 212 repo_local_root_raw: Option<OsString>, 213 default_profile: RadrootsPathProfile, 214 ) -> Result<Self, RadrootsRuntimePathSelectionError> { 215 let (profile, profile_source) = match profile_value { 216 Ok(value) => ( 217 parse_profile(profile_env, value.as_str())?, 218 format!("process_env:{profile_env}"), 219 ), 220 Err(std::env::VarError::NotPresent) => (default_profile, "default".to_owned()), 221 Err(std::env::VarError::NotUnicode(_)) => { 222 return Err(RadrootsRuntimePathSelectionError::NonUnicodeEnv { 223 env_var: profile_env.to_owned(), 224 }); 225 } 226 }; 227 let repo_local_root = repo_local_root_raw.as_ref().map(PathBuf::from); 228 Ok(Self { 229 profile, 230 profile_source, 231 repo_local_root, 232 repo_local_root_source: repo_local_root_raw 233 .as_ref() 234 .map(|_| format!("process_env:{repo_local_root_env}")), 235 }) 236 } 237 238 pub fn root_source(&self) -> &'static str { 239 match self.profile { 240 RadrootsPathProfile::InteractiveUser => "host_defaults", 241 RadrootsPathProfile::ServiceHost => "service_host_defaults", 242 RadrootsPathProfile::RepoLocal => "repo_local_root", 243 RadrootsPathProfile::MobileNative => "mobile_native_defaults", 244 } 245 } 246 247 pub fn overrides( 248 &self, 249 profile_env: &'static str, 250 repo_local_root_env: &'static str, 251 ) -> Result<RadrootsPathOverrides, RadrootsRuntimePathSelectionError> { 252 self.overrides_with_labels(profile_env, repo_local_root_env) 253 } 254 255 pub fn caller_overrides( 256 &self, 257 ) -> Result<RadrootsPathOverrides, RadrootsRuntimePathSelectionError> { 258 self.overrides_with_labels("caller_profile", "caller_repo_local_root") 259 } 260 261 pub fn contract( 262 &self, 263 allowed_profiles: &[&str], 264 subordinate_path_override_source: &str, 265 subordinate_path_override_keys: &[&str], 266 ) -> RadrootsRuntimeSelectionContract { 267 RadrootsRuntimeSelectionContract { 268 active_profile: self.profile.to_string(), 269 allowed_profiles: allowed_profiles 270 .iter() 271 .map(|entry| (*entry).to_owned()) 272 .collect(), 273 path_overrides: RadrootsRuntimeSelectionOverrideContract { 274 profile_source: self.profile_source.clone(), 275 root_source: self.root_source().to_owned(), 276 repo_local_root: self.repo_local_root.clone(), 277 repo_local_root_source: self.repo_local_root_source.clone(), 278 subordinate_path_override_source: subordinate_path_override_source.to_owned(), 279 subordinate_path_override_keys: subordinate_path_override_keys 280 .iter() 281 .map(|entry| (*entry).to_owned()) 282 .collect(), 283 }, 284 } 285 } 286 287 fn overrides_with_labels( 288 &self, 289 profile_label: &str, 290 repo_local_root_label: &str, 291 ) -> Result<RadrootsPathOverrides, RadrootsRuntimePathSelectionError> { 292 match self.profile { 293 RadrootsPathProfile::RepoLocal => { 294 let Some(repo_local_root) = self.repo_local_root.as_ref() else { 295 return Err(RadrootsRuntimePathSelectionError::MissingRepoLocalRoot { 296 profile_env: profile_label.to_owned(), 297 repo_local_root_env: repo_local_root_label.to_owned(), 298 }); 299 }; 300 Ok(RadrootsPathOverrides::repo_local(repo_local_root)) 301 } 302 _ => Ok(RadrootsPathOverrides::default()), 303 } 304 } 305 306 pub fn resolve_service_roots( 307 &self, 308 resolver: &RadrootsPathResolver, 309 service_id: &str, 310 profile_env: &'static str, 311 repo_local_root_env: &'static str, 312 ) -> Result<RadrootsPaths, RadrootsRuntimePathSelectionError> { 313 let namespace = RadrootsRuntimeNamespace::service(service_id)?; 314 let overrides = self.overrides(profile_env, repo_local_root_env)?; 315 let roots = resolver.resolve(self.profile, &overrides)?; 316 Ok(roots.namespaced(&namespace)) 317 } 318 } 319 320 fn parse_profile( 321 env_var: &str, 322 value: &str, 323 ) -> Result<RadrootsPathProfile, RadrootsRuntimePathSelectionError> { 324 parse_profile_value(value).map_err(|_| RadrootsRuntimePathSelectionError::InvalidProfileEnv { 325 env_var: env_var.to_owned(), 326 value: value.to_owned(), 327 }) 328 } 329 330 fn parse_profile_value( 331 value: &str, 332 ) -> Result<RadrootsPathProfile, RadrootsRuntimePathSelectionError> { 333 match value { 334 "interactive_user" => Ok(RadrootsPathProfile::InteractiveUser), 335 "service_host" => Ok(RadrootsPathProfile::ServiceHost), 336 "repo_local" => Ok(RadrootsPathProfile::RepoLocal), 337 "mobile_native" => Ok(RadrootsPathProfile::MobileNative), 338 other => Err(RadrootsRuntimePathSelectionError::InvalidProfileValue { 339 value: other.to_owned(), 340 }), 341 } 342 } 343 344 #[cfg(test)] 345 mod tests { 346 use std::path::PathBuf; 347 348 use crate::{ 349 RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, 350 }; 351 352 use super::{ 353 RadrootsRuntimePathConfigEntry, RadrootsRuntimePathPolicyContract, 354 RadrootsRuntimePathSelection, RadrootsRuntimePathSelectionError, 355 runtime_migration_contract, 356 }; 357 use crate::{RadrootsLegacyPathDetection, RadrootsMigrationReport}; 358 359 #[test] 360 fn caller_selection_preserves_profile_and_sources() { 361 let selection = 362 RadrootsRuntimePathSelection::caller(RadrootsPathProfile::InteractiveUser, None); 363 assert_eq!(selection.profile, RadrootsPathProfile::InteractiveUser); 364 assert_eq!(selection.profile_source, "caller"); 365 assert_eq!(selection.repo_local_root, None); 366 assert_eq!(selection.repo_local_root_source, None); 367 assert_eq!(selection.root_source(), "host_defaults"); 368 369 let overrides = selection 370 .caller_overrides() 371 .expect("non-repo-local caller overrides should be empty"); 372 assert_eq!(overrides.repo_local_root, None); 373 374 let service_selection = 375 RadrootsRuntimePathSelection::caller(RadrootsPathProfile::ServiceHost, None); 376 assert_eq!(service_selection.root_source(), "service_host_defaults"); 377 } 378 379 #[test] 380 fn caller_selection_marks_repo_local_source() { 381 let selection = RadrootsRuntimePathSelection::caller( 382 RadrootsPathProfile::RepoLocal, 383 Some(PathBuf::from("/repo/.local/radroots")), 384 ); 385 386 assert_eq!(selection.profile_source, "caller"); 387 assert_eq!(selection.repo_local_root_source.as_deref(), Some("caller")); 388 assert_eq!(selection.root_source(), "repo_local_root"); 389 390 let overrides = selection 391 .caller_overrides() 392 .expect("caller overrides should use repo-local root"); 393 assert_eq!( 394 overrides.repo_local_root, 395 Some(PathBuf::from("/repo/.local/radroots")) 396 ); 397 } 398 399 #[test] 400 fn resolve_service_roots_uses_repo_local_override() { 401 let selection = RadrootsRuntimePathSelection::caller( 402 RadrootsPathProfile::RepoLocal, 403 Some(PathBuf::from("/repo/.local/radroots")), 404 ); 405 let resolver = 406 RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); 407 408 let roots = selection 409 .resolve_service_roots(&resolver, "radrootsd", "PROFILE_ENV", "ROOT_ENV") 410 .expect("service roots"); 411 412 assert_eq!( 413 roots.config, 414 PathBuf::from("/repo/.local/radroots/config/services/radrootsd") 415 ); 416 assert_eq!( 417 roots.data, 418 PathBuf::from("/repo/.local/radroots/data/services/radrootsd") 419 ); 420 assert_eq!( 421 roots.logs, 422 PathBuf::from("/repo/.local/radroots/logs/services/radrootsd") 423 ); 424 assert_eq!( 425 roots.run, 426 PathBuf::from("/repo/.local/radroots/run/services/radrootsd") 427 ); 428 assert_eq!( 429 roots.secrets, 430 PathBuf::from("/repo/.local/radroots/secrets/services/radrootsd") 431 ); 432 } 433 434 #[test] 435 fn overrides_require_repo_local_root_for_repo_local_profile() { 436 let selection = RadrootsRuntimePathSelection::caller(RadrootsPathProfile::RepoLocal, None); 437 let err = selection 438 .overrides("RADROOTS_TEST_PROFILE", "RADROOTS_TEST_ROOT") 439 .expect_err("repo local root"); 440 441 assert_eq!( 442 err, 443 RadrootsRuntimePathSelectionError::MissingRepoLocalRoot { 444 profile_env: "RADROOTS_TEST_PROFILE".to_owned(), 445 repo_local_root_env: "RADROOTS_TEST_ROOT".to_owned(), 446 } 447 ); 448 } 449 450 #[test] 451 fn profile_value_selection_accepts_mobile_native() { 452 let selection = RadrootsRuntimePathSelection::from_profile_value("mobile_native", None) 453 .expect("mobile native profile"); 454 455 assert_eq!(selection.profile, RadrootsPathProfile::MobileNative); 456 assert_eq!(selection.profile_source, "caller"); 457 assert_eq!(selection.root_source(), "mobile_native_defaults"); 458 459 let selection = RadrootsRuntimePathSelection::from_profile_value("interactive_user", None) 460 .expect("interactive profile"); 461 assert_eq!(selection.profile, RadrootsPathProfile::InteractiveUser); 462 463 let selection = RadrootsRuntimePathSelection::from_profile_value("service_host", None) 464 .expect("service-host profile"); 465 assert_eq!(selection.profile, RadrootsPathProfile::ServiceHost); 466 } 467 468 #[test] 469 fn env_selection_covers_absent_present_and_error_paths() { 470 let selection = RadrootsRuntimePathSelection::from_env( 471 "RADROOTS_RUNTIME_PATHS_TEST_UNSET_PROFILE_DFA3ED5D", 472 "RADROOTS_RUNTIME_PATHS_TEST_UNSET_ROOT_DFA3ED5D", 473 RadrootsPathProfile::ServiceHost, 474 ) 475 .expect("absent env selection should use default profile"); 476 assert_eq!(selection.profile, RadrootsPathProfile::ServiceHost); 477 assert_eq!(selection.profile_source, "default"); 478 assert_eq!(selection.repo_local_root, None); 479 assert_eq!(selection.repo_local_root_source, None); 480 481 let selection = RadrootsRuntimePathSelection::from_env_values( 482 "RADROOTS_TEST_PROFILE", 483 Ok("repo_local".to_owned()), 484 "RADROOTS_TEST_ROOT", 485 Some(std::ffi::OsString::from("/repo/.local/radroots")), 486 RadrootsPathProfile::InteractiveUser, 487 ) 488 .expect("present env values should select repo-local profile"); 489 assert_eq!(selection.profile, RadrootsPathProfile::RepoLocal); 490 assert_eq!( 491 selection.profile_source, 492 "process_env:RADROOTS_TEST_PROFILE" 493 ); 494 assert_eq!( 495 selection.repo_local_root, 496 Some(PathBuf::from("/repo/.local/radroots")) 497 ); 498 assert_eq!( 499 selection.repo_local_root_source.as_deref(), 500 Some("process_env:RADROOTS_TEST_ROOT") 501 ); 502 503 let err = RadrootsRuntimePathSelection::from_env_values( 504 "RADROOTS_TEST_PROFILE", 505 Err(std::env::VarError::NotUnicode(std::ffi::OsString::from( 506 "not-unicode", 507 ))), 508 "RADROOTS_TEST_ROOT", 509 None, 510 RadrootsPathProfile::InteractiveUser, 511 ) 512 .expect_err("non-unicode profile env should fail"); 513 assert_eq!( 514 err, 515 RadrootsRuntimePathSelectionError::NonUnicodeEnv { 516 env_var: "RADROOTS_TEST_PROFILE".to_owned() 517 } 518 ); 519 520 let err = RadrootsRuntimePathSelection::from_env_values( 521 "RADROOTS_TEST_PROFILE", 522 Ok("unknown".to_owned()), 523 "RADROOTS_TEST_ROOT", 524 None, 525 RadrootsPathProfile::InteractiveUser, 526 ) 527 .expect_err("invalid profile env should fail"); 528 assert_eq!( 529 err, 530 RadrootsRuntimePathSelectionError::InvalidProfileEnv { 531 env_var: "RADROOTS_TEST_PROFILE".to_owned(), 532 value: "unknown".to_owned() 533 } 534 ); 535 } 536 537 #[test] 538 fn config_entry_selection_preserves_sources() { 539 let selection = RadrootsRuntimePathSelection::from_config_entries( 540 Some(RadrootsRuntimePathConfigEntry::new( 541 "RADROOTS_CLI_PATHS_PROFILE", 542 "repo_local", 543 "env_file:RADROOTS_CLI_PATHS_PROFILE", 544 )), 545 Some(RadrootsRuntimePathConfigEntry::new( 546 "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", 547 ".local/radroots", 548 "env_file:RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", 549 )), 550 RadrootsPathProfile::InteractiveUser, 551 ) 552 .expect("config entries should select paths"); 553 554 assert_eq!(selection.profile, RadrootsPathProfile::RepoLocal); 555 assert_eq!( 556 selection.profile_source, 557 "env_file:RADROOTS_CLI_PATHS_PROFILE" 558 ); 559 assert_eq!( 560 selection.repo_local_root, 561 Some(PathBuf::from(".local/radroots")) 562 ); 563 assert_eq!( 564 selection.repo_local_root_source.as_deref(), 565 Some("env_file:RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT") 566 ); 567 } 568 569 #[test] 570 fn config_entry_selection_uses_default_profile_without_sources() { 571 let selection = RadrootsRuntimePathSelection::from_config_entries( 572 None, 573 None, 574 RadrootsPathProfile::InteractiveUser, 575 ) 576 .expect("default selection"); 577 578 assert_eq!(selection.profile, RadrootsPathProfile::InteractiveUser); 579 assert_eq!(selection.profile_source, "default"); 580 assert_eq!(selection.repo_local_root, None); 581 assert_eq!(selection.repo_local_root_source, None); 582 } 583 584 #[test] 585 fn contract_captures_selection_sources() { 586 let selection = RadrootsRuntimePathSelection::caller( 587 RadrootsPathProfile::RepoLocal, 588 Some(PathBuf::from("/repo/.local/radroots")), 589 ); 590 591 let contract = selection.contract( 592 &["interactive_user", "repo_local"], 593 "config_artifact", 594 &["config.service.logs_dir"], 595 ); 596 597 assert_eq!(contract.active_profile, "repo_local"); 598 assert_eq!( 599 contract.allowed_profiles, 600 vec!["interactive_user".to_owned(), "repo_local".to_owned()] 601 ); 602 assert_eq!(contract.path_overrides.profile_source, "caller"); 603 assert_eq!( 604 contract.path_overrides.repo_local_root, 605 Some(PathBuf::from("/repo/.local/radroots")) 606 ); 607 } 608 609 #[test] 610 fn path_policy_contract_preserves_policy_strings() { 611 let contract = RadrootsRuntimePathPolicyContract::new( 612 "profile_root_env_or_repo_wrapper", 613 "config_artifact", 614 "compatibility_break_glass", 615 &["MYC_PATHS_STATE_DIR"], 616 ); 617 618 assert_eq!( 619 contract.canonical_root_selection, 620 "profile_root_env_or_repo_wrapper" 621 ); 622 assert_eq!( 623 contract.compatibility_leaf_path_keys, 624 vec!["MYC_PATHS_STATE_DIR".to_owned()] 625 ); 626 } 627 628 #[test] 629 fn runtime_migration_contract_maps_detected_paths() { 630 let report = RadrootsMigrationReport { 631 posture: "explicit_operator_import_required", 632 state: "legacy_state_detected", 633 silent_startup_relocation: false, 634 compatibility_window: "detect_and_report_only", 635 detected_legacy_paths: vec![RadrootsLegacyPathDetection { 636 id: "legacy_path".to_owned(), 637 description: "legacy path".to_owned(), 638 path: PathBuf::from("/tmp/legacy"), 639 destination: Some(PathBuf::from("/tmp/new")), 640 import_hint: "copy it manually".to_owned(), 641 }], 642 }; 643 644 let contract = runtime_migration_contract(report); 645 646 assert_eq!(contract.posture, "explicit_operator_import_required"); 647 assert_eq!(contract.detected_legacy_paths.len(), 1); 648 assert_eq!(contract.detected_legacy_paths[0].id, "legacy_path"); 649 } 650 }