use proxywarden_lib::component_detection::{ proxyfier_component_from_inventory, singbox_component_from_inventory, }; use proxywarden_lib::component_inventory::{ authorize_component_action, classify_component_candidates, component_inventory_fingerprint_for_cutover, legacy_proxifyre_topshelf_path_matches, prove_legacy_cutover, BinaryIdentityEvidence, CandidateRole, ComponentCandidateProbe, ComponentClassification, InventoryAction, InventoryIssue, LegacyCutoverEvidence, LegacyCutoverProof, LegacyProxifyreScmProfile, MarkerEvidence, ServiceEvidence, AMBIGUOUS_LEGACY, MANUAL_MIGRATION_REQUIRED, OWNERSHIP_MISMATCH, }; use proxywarden_lib::component_status::resolve_component_statuses_with_inventories; use proxywarden_lib::models::{ComponentId, ComponentState}; use std::path::{Path, PathBuf}; #[test] fn managed_current_requires_marker_files_and_exact_service_path() { let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre"); let inventory = classify_component_candidates( ComponentId::Proxyfier, vec![probe( ComponentId::Proxyfier, CandidateRole::Current, &root, true, MarkerEvidence::Valid, BinaryIdentityEvidence::Unknown, Some(service(&root.join("ProxiFyre.exe"), true)), )], ); assert_eq!( inventory.classification(), ComponentClassification::ManagedCurrent ); assert_eq!( inventory .selected_candidate() .expect("selected current") .binary_version, Some("2.4.0.0".to_string()) ); } #[test] fn same_service_name_with_foreign_path_is_ownership_mismatch() { let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre"); let inventory = classify_component_candidates( ComponentId::Proxyfier, vec![probe( ComponentId::Proxyfier, CandidateRole::Current, &root, true, MarkerEvidence::Valid, BinaryIdentityEvidence::KnownPackage, Some(service( PathBuf::from(r"C:\Foreign\ProxiFyre.exe").as_path(), false, )), )], ); let candidate = inventory .selected_candidate() .expect("foreign current candidate"); assert_eq!(candidate.classification, ComponentClassification::Foreign); assert_eq!(candidate.issues[0].code, OWNERSHIP_MISMATCH); } #[test] fn tools_proxifyre_is_legacy_and_never_current() { let root = PathBuf::from(r"C:\Tools\ProxiFyre"); let inventory = classify_component_candidates( ComponentId::Proxyfier, vec![probe( ComponentId::Proxyfier, CandidateRole::Legacy, &root, false, MarkerEvidence::NotRequired, BinaryIdentityEvidence::KnownPackage, Some(service(&root.join("ProxiFyre.exe"), true)), )], ); assert_eq!( inventory.classification(), ComponentClassification::ManagedLegacy ); let component = proxyfier_component_from_inventory(&inventory); assert_eq!(component.actions, vec!["Перенести ProxiFyre"]); assert!(component .problems .iter() .any(|problem| problem.contains("явного переноса"))); } #[test] fn bare_singbox_root_stays_foreign_without_complete_identity() { let root = PathBuf::from(r"C:\Program Files\sing-box"); let mut candidate = probe( ComponentId::Singbox, CandidateRole::ForeignByDefault, &root, false, MarkerEvidence::NotRequired, BinaryIdentityEvidence::Unknown, Some(service(&root.join("ProxyWardenSingBox.exe"), true)), ); candidate.legacy_identity_complete = false; let inventory = classify_component_candidates(ComponentId::Singbox, vec![candidate]); assert_eq!(inventory.classification(), ComponentClassification::Foreign); } #[test] fn current_root_without_required_marker_is_incomplete() { let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre"); let inventory = classify_component_candidates( ComponentId::Proxyfier, vec![probe( ComponentId::Proxyfier, CandidateRole::Current, &root, true, MarkerEvidence::Missing, BinaryIdentityEvidence::KnownPackage, Some(service(&root.join("ProxiFyre.exe"), true)), )], ); assert_eq!( inventory.classification(), ComponentClassification::Incomplete ); } #[test] fn startup_status_preserves_foreign_and_incomplete_inventory_errors() { let proxyfier_root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre"); let foreign_proxyfier = classify_component_candidates( ComponentId::Proxyfier, vec![probe( ComponentId::Proxyfier, CandidateRole::Current, &proxyfier_root, true, MarkerEvidence::Valid, BinaryIdentityEvidence::KnownPackage, Some(service( PathBuf::from(r"C:\Foreign\ProxiFyre.exe").as_path(), false, )), )], ); let singbox_root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\sing-box"); let mut incomplete_singbox_probe = probe( ComponentId::Singbox, CandidateRole::Current, &singbox_root, false, MarkerEvidence::NotRequired, BinaryIdentityEvidence::KnownPackage, Some(service(&singbox_root.join("ProxyWardenSingBox.exe"), true)), ); incomplete_singbox_probe .missing_files .push(singbox_root.join("ProxyWardenSingBox.xml")); let incomplete_singbox = classify_component_candidates(ComponentId::Singbox, vec![incomplete_singbox_probe]); let statuses = resolve_component_statuses_with_inventories(&foreign_proxyfier, &incomplete_singbox); let proxyfier = statuses .iter() .find(|status| status.id == ComponentId::Proxyfier) .expect("ProxiFyre status"); let singbox = statuses .iter() .find(|status| status.id == ComponentId::Singbox) .expect("sing-box status"); assert_eq!(proxyfier.state, ComponentState::Error); assert_eq!(singbox.state, ComponentState::Error); assert!(!proxyfier.problems.is_empty()); assert!(!singbox.problems.is_empty()); } #[test] fn current_candidate_wins_but_legacy_remains_visible() { let current_root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre"); let legacy_root = PathBuf::from(r"C:\Tools\ProxiFyre"); let inventory = classify_component_candidates( ComponentId::Proxyfier, vec![ probe( ComponentId::Proxyfier, CandidateRole::Legacy, &legacy_root, false, MarkerEvidence::NotRequired, BinaryIdentityEvidence::KnownPackage, Some(service(&legacy_root.join("ProxiFyre.exe"), true)), ), probe( ComponentId::Proxyfier, CandidateRole::Current, ¤t_root, true, MarkerEvidence::Valid, BinaryIdentityEvidence::KnownPackage, Some(service(¤t_root.join("ProxiFyre.exe"), true)), ), ], ); assert_eq!(inventory.candidates.len(), 2); assert_eq!( inventory.classification(), ComponentClassification::ManagedCurrent ); assert_eq!( inventory .selected_candidate() .expect("selected current") .root, current_root ); } #[test] fn multiple_managed_legacy_candidates_block_selection() { let roots = [ PathBuf::from(r"C:\Tools\ProxiFyre"), PathBuf::from(r"C:\Program Files\ProxiFyre"), ]; let probes = roots .iter() .map(|root| { probe( ComponentId::Proxyfier, CandidateRole::Legacy, root, false, MarkerEvidence::NotRequired, BinaryIdentityEvidence::KnownPackage, Some(service(&root.join("ProxiFyre.exe"), true)), ) }) .collect(); let inventory = classify_component_candidates(ComponentId::Proxyfier, probes); assert!(inventory.selected_candidate().is_none()); assert_eq!(inventory.issues[0].code, AMBIGUOUS_LEGACY); } #[test] fn reparse_point_is_never_managed() { let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\sing-box"); let mut candidate = probe( ComponentId::Singbox, CandidateRole::Current, &root, false, MarkerEvidence::NotRequired, BinaryIdentityEvidence::KnownPackage, Some(service(&root.join("ProxyWardenSingBox.exe"), true)), ); candidate.has_reparse_point = true; let inventory = classify_component_candidates(ComponentId::Singbox, vec![candidate]); assert_eq!(inventory.classification(), ComponentClassification::Foreign); assert_eq!( inventory.selected_candidate().unwrap().issues[0].code, OWNERSHIP_MISMATCH ); } #[test] fn exact_frozen_proxifyre_identity_is_the_only_automatic_cutover() { let root = PathBuf::from(r"C:\Tools\ProxiFyre"); let inventory = legacy_inventory( ComponentId::Proxyfier, &root, "ProxiFyreService", &topshelf_path(&root), "2.2.1.0", ); let proof = prove_legacy_cutover(&inventory, &exact_cutover_evidence()) .expect("exact identity must produce an opaque proof"); assert_eq!(proof.fingerprint().len(), 64); } #[test] fn discovery_evidence_never_substitutes_for_cutover_identity() { let auto_root = PathBuf::from(r"C:\Tools\ProxiFyre"); let cases = [ legacy_inventory( ComponentId::Proxyfier, Path::new(r"C:\Program Files\ProxiFyre"), "ProxiFyreService", &topshelf_path(Path::new(r"C:\Program Files\ProxiFyre")), "2.2.1.0", ), legacy_inventory( ComponentId::Proxyfier, &auto_root, "ProxiFyre", &topshelf_path(&auto_root), "2.2.1.0", ), legacy_inventory( ComponentId::Proxyfier, &auto_root, "ProxiFyreService", &format!( r#""{}" --service"#, auto_root.join("ProxiFyre.exe").display() ), "2.2.1.0", ), legacy_inventory( ComponentId::Proxyfier, &auto_root, "ProxiFyreService", &topshelf_path(&auto_root), "2.4.0.0", ), ]; for inventory in cases { assert_eq!( inventory.classification(), ComponentClassification::ManagedLegacy ); assert_manual_without_mutation(prove_legacy_cutover(&inventory, &exact_cutover_evidence())); } let inventory = legacy_inventory( ComponentId::Proxyfier, &auto_root, "ProxiFyreService", &topshelf_path(&auto_root), "2.2.1.0", ); let mut bad_manifest = exact_cutover_evidence(); bad_manifest.proxifyre_manifest_matches = false; assert_manual_without_mutation(prove_legacy_cutover(&inventory, &bad_manifest)); let mut bad_snapshot_fingerprint = exact_cutover_evidence(); bad_snapshot_fingerprint .proxifyre_scm_snapshot_fingerprint .clear(); assert_manual_without_mutation(prove_legacy_cutover(&inventory, &bad_snapshot_fingerprint)); let mut bad_profile = exact_cutover_evidence(); bad_profile .proxifyre_scm_profile .as_mut() .expect("profile") .delayed_auto_start = true; assert_manual_without_mutation(prove_legacy_cutover(&inventory, &bad_profile)); let mut extra_candidate = inventory.clone(); extra_candidate .candidates .push(extra_candidate.candidates[0].clone()); assert_manual_without_mutation(prove_legacy_cutover( &extra_candidate, &exact_cutover_evidence(), )); let mut alias_collision = exact_cutover_evidence(); alias_collision.additional_matching_service = true; assert_manual_without_mutation(prove_legacy_cutover(&inventory, &alias_collision)); } #[test] fn legacy_singbox_is_always_manual_and_has_zero_mutation_authority() { let root = PathBuf::from(r"C:\Program Files\ProxyWarden\sing-box"); let inventory = legacy_inventory( ComponentId::Singbox, &root, "ProxyWardenSingBox", &format!(r#""{}""#, root.join("ProxyWardenSingBox.exe").display()), "1.13.19", ); assert_eq!( inventory.classification(), ComponentClassification::ManagedLegacy ); let component = singbox_component_from_inventory(&inventory); assert!(component.actions.is_empty()); assert!(component .problems .iter() .any(|problem| problem.contains("ручного переноса"))); assert_manual_without_mutation(prove_legacy_cutover(&inventory, &exact_cutover_evidence())); } #[test] fn generic_inventory_authorization_never_grants_cutover() { let root = PathBuf::from(r"C:\Tools\ProxiFyre"); let inventory = legacy_inventory( ComponentId::Proxyfier, &root, "ProxiFyreService", &topshelf_path(&root), "2.2.1.0", ); let error = authorize_component_action(&inventory, InventoryAction::Cutover) .expect_err("generic lifecycle authorization must not grant cutover"); assert_eq!(error.code, "legacy_cutover_required"); let mut current = inventory.clone(); current.candidates[0].classification = ComponentClassification::ManagedCurrent; let missing = proxywarden_lib::component_inventory::ComponentInventory::missing(ComponentId::Proxyfier); for inventory in [¤t, &missing] { assert!(authorize_component_action(inventory, InventoryAction::Cutover).is_err()); } prove_legacy_cutover(&inventory, &exact_cutover_evidence()) .expect("strict gate remains the only proof constructor"); } #[test] fn generic_inventory_authorization_never_writes_legacy_runtime_config() { let root = PathBuf::from(r"C:\Tools\ProxiFyre"); let inventory = legacy_inventory( ComponentId::Proxyfier, &root, "ProxiFyreService", &topshelf_path(&root), "2.2.1.0", ); for action in [ InventoryAction::Apply, InventoryAction::Start, InventoryAction::Stop, ] { let error = authorize_component_action(&inventory, action) .expect_err("legacy runtime actions require explicit cutover"); assert_eq!(error.code, "legacy_cutover_required"); } } #[test] fn cutover_inventory_fingerprint_is_stable_and_binds_live_service_state() { let root = PathBuf::from(r"C:\Tools\ProxiFyre"); let inventory = legacy_inventory( ComponentId::Proxyfier, &root, "ProxiFyreService", &topshelf_path(&root), "2.2.1.0", ); let first = component_inventory_fingerprint_for_cutover(&inventory); assert_eq!( first, component_inventory_fingerprint_for_cutover(&inventory) ); let mut changed = inventory.clone(); changed.candidates[0] .service .as_mut() .expect("service") .status = "running".to_string(); assert_ne!(first, component_inventory_fingerprint_for_cutover(&changed)); } #[test] fn frozen_scm_profile_rejects_each_unsafe_or_unknown_field() { let mutations: [fn(&mut LegacyProxifyreScmProfile); 15] = [ |profile| profile.service_type = 0x20, |profile| profile.start_type = 3, |profile| profile.error_control = 0, |profile| profile.account_name = "NetworkService".to_string(), |profile| profile.display_name = "ProxiFyre".to_string(), |profile| profile.description.clear(), |profile| profile.dependencies.push("Tcpip".to_string()), |profile| profile.load_order_group = Some("Network".to_string()), |profile| profile.has_failure_actions = true, |profile| profile.failure_actions_on_non_crash = true, |profile| profile.delayed_auto_start = true, |profile| profile.sid_type = 1, |profile| { profile .required_privileges .push("SeDebugPrivilege".to_string()) }, |profile| profile.has_triggers = true, |profile| profile.untrusted_mutation_rights = true, ]; assert!(exact_scm_profile().matches_frozen_2_2_1_profile()); for mutate in mutations { let mut profile = exact_scm_profile(); mutate(&mut profile); assert!(!profile.matches_frozen_2_2_1_profile()); } } #[test] fn topshelf_cutover_path_is_token_exact_and_pair_order_independent() { let executable = Path::new(r"C:\Tools\ProxiFyre\ProxiFyre.exe"); for path_name in [ r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename "ProxiFyreService""#, r#"C:\Tools\ProxiFyre\ProxiFyre.exe -servicename ProxiFyreService -displayname "ProxiFyre Service""#, ] { assert!(legacy_proxifyre_topshelf_path_matches( path_name, executable )); } for path_name in [ r#""C:\Tools\ProxiFyre\ProxiFyre.exe" --service"#, r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename ProxiFyreService --run"#, r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "Foreign" -servicename ProxiFyreService"#, r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename ProxiFyre"#, r#""C:\Tools\ProxiFyre\ProxiFyre.exe -displayname "ProxiFyre Service" -servicename ProxiFyreService"#, ] { assert!(!legacy_proxifyre_topshelf_path_matches( path_name, executable )); } assert!(!legacy_proxifyre_topshelf_path_matches( r#""C:\Program Files\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename ProxiFyreService"#, Path::new(r"C:\Program Files\ProxiFyre\ProxiFyre.exe"), )); } fn assert_manual_without_mutation(result: Result) { assert_eq!( result .expect_err("manual identity must not yield a proof") .code, MANUAL_MIGRATION_REQUIRED ); } fn exact_cutover_evidence() -> LegacyCutoverEvidence { LegacyCutoverEvidence { proxifyre_manifest_matches: true, proxifyre_scm_profile: Some(exact_scm_profile()), proxifyre_scm_snapshot_fingerprint: "9".repeat(64), additional_matching_service: false, } } fn exact_scm_profile() -> LegacyProxifyreScmProfile { LegacyProxifyreScmProfile { service_type: 0x10, start_type: 2, error_control: 1, account_name: "LocalSystem".to_string(), display_name: "ProxiFyre Service".to_string(), description: "ProxiFyre - SOCKS5 ProxiFyre Service".to_string(), dependencies: Vec::new(), load_order_group: None, has_failure_actions: false, failure_actions_on_non_crash: false, delayed_auto_start: false, sid_type: 0, required_privileges: Vec::new(), has_triggers: false, untrusted_mutation_rights: false, } } fn legacy_inventory( component_id: ComponentId, root: &Path, service_name: &str, path_name: &str, version: &str, ) -> proxywarden_lib::component_inventory::ComponentInventory { let executable_name = match component_id { ComponentId::Proxyfier => "ProxiFyre.exe", ComponentId::Singbox => "sing-box.exe", ComponentId::ControlApp => "ProxyWarden.exe", }; let service_executable = match component_id { ComponentId::Singbox => root.join("ProxyWardenSingBox.exe"), ComponentId::Proxyfier | ComponentId::ControlApp => root.join(executable_name), }; classify_component_candidates( component_id.clone(), vec![ComponentCandidateProbe { component_id, role: CandidateRole::Legacy, root: root.to_path_buf(), root_exists: true, has_reparse_point: false, executable_path: Some(root.join(executable_name)), missing_files: Vec::new(), marker: MarkerEvidence::NotRequired, marker_required: false, binary_identity: BinaryIdentityEvidence::KnownPackage, binary_version: Some(version.to_string()), service: Some(ServiceEvidence { name: service_name.to_string(), status: "stopped".to_string(), path_name: Some(path_name.to_string()), executable_path: Some(service_executable), path_matches_candidate: true, binary_version: Some(version.to_string()), }), service_required: true, legacy_identity_complete: true, }], ) } fn topshelf_path(root: &Path) -> String { format!( r#""{}" -displayname "ProxiFyre Service" -servicename "ProxiFyreService""#, root.join("ProxiFyre.exe").display() ) } fn probe( component_id: ComponentId, role: CandidateRole, root: &Path, marker_required: bool, marker: MarkerEvidence, binary_identity: BinaryIdentityEvidence, service: Option, ) -> ComponentCandidateProbe { let executable_name = match component_id { ComponentId::Proxyfier => "ProxiFyre.exe", ComponentId::Singbox => "sing-box.exe", ComponentId::ControlApp => "ProxyWarden.exe", }; ComponentCandidateProbe { component_id, role, root: root.to_path_buf(), root_exists: true, has_reparse_point: false, executable_path: Some(root.join(executable_name)), missing_files: Vec::new(), marker, marker_required, binary_identity, binary_version: Some("2.4.0.0".to_string()), service, service_required: true, legacy_identity_complete: true, } } fn service(executable: &std::path::Path, matches: bool) -> ServiceEvidence { ServiceEvidence { name: "ProxiFyreService".to_string(), status: "stopped".to_string(), path_name: Some(format!(r#""{}" --service"#, executable.display())), executable_path: Some(executable.to_path_buf()), path_matches_candidate: matches, binary_version: Some("2.4.0.0".to_string()), } }