diff --git a/src-tauri/src/singbox_runtime/system.rs b/src-tauri/src/singbox_runtime/system.rs index df4400e..02675f2 100644 --- a/src-tauri/src/singbox_runtime/system.rs +++ b/src-tauri/src/singbox_runtime/system.rs @@ -1282,7 +1282,7 @@ impl SingBoxNativeHost for SystemSingBoxNativeHost { && verify_runtime_root_shape( self.root.path(), files, - false, + receipt_valid && marker_valid, self.promoted_has_runtime_config, ) .is_ok() @@ -1854,6 +1854,81 @@ impl SystemSingBoxNativeHost { mod tests { use super::*; + #[test] + #[ignore = "requires elevated Windows and PROXYWARDEN_TEST_APP_ROOT; no service mutations"] + fn promoted_inventory_survives_metadata_commit_on_windows() { + let parent = PathBuf::from( + std::env::var_os("PROXYWARDEN_TEST_APP_ROOT").expect("trusted test parent"), + ); + safe_fs::verify_path_under_trusted_program_files(&parent) + .expect("trusted Program Files parent"); + let app = parent.join(format!(".singbox-inventory-test-{}", uuid::Uuid::new_v4())); + let components = app.join("components"); + let root = CanonicalComponentRoot::from_current_exe_for_tests( + &app.join("ProxyWarden.exe"), + ManagedComponent::SingBox, + ) + .expect("isolated root"); + for path in [&app, &components, root.path()] { + safe_fs::create_directory_admin_owned_user_read_only(path) + .expect("sealed test directory"); + } + let files = vec![ + staged(SINGBOX_LICENSE_FILE, b"license"), + staged(WINSW_WRAPPER_FILE, b"wrapper"), + staged(WINSW_SERVICE_XML_FILE, singbox_service_xml().as_bytes()), + staged(SINGBOX_CRONET_FILE, b"cronet"), + staged(SINGBOX_RUNTIME_FILE, b"runtime"), + ]; + let runtime = PrivilegedPackageProof { + component_id: ComponentId::SingBox, + version: "1.13.19".to_owned(), + asset_name: "sing-box-1.13.19-windows-amd64.zip".to_owned(), + sha256: "a".repeat(64), + size: 100, + source: PackageSource::Bundled, + independent_proof: None, + }; + let wrapper = SingBoxMarkerPackage { + version: "2.12.0".to_owned(), + asset_name: "WinSW.NET461.exe".to_owned(), + sha256: "b".repeat(64), + size: 100, + source: PackageSource::Bundled, + }; + let metadata = + build_metadata(&runtime, wrapper, installed_file_inventory(&files)).expect("metadata"); + write_staged_files(root.path(), &files).expect("sealed runtime files"); + let mut host = SystemSingBoxNativeHost::new(root); + host.promoted_files = Some(installed_file_inventory(&files)); + host.promoted_runtime_version = Some("1.13.19".to_owned()); + host.promoted_wrapper_version = Some("2.12.0".to_owned()); + let before = host.inspect(); + let committed = host.commit_metadata(SingBoxNativeMode::Install, &metadata); + let after = host.inspect(); + // Exact receipt-owned cleanup only; this test never creates or controls a service. + host.rollback(SingBoxNativeMode::Install, false) + .expect("remove isolated runtime"); + fs::remove_dir(&components).expect("empty components directory"); + fs::remove_dir(&app).expect("empty test directory"); + let before = before.expect("pre-commit inventory"); + assert!(before.service_xml_matches && before.promoted_files_match); + assert!(!before.receipt_valid && !before.marker_valid); + committed.expect("metadata commit"); + let after = after.expect("post-commit inventory"); + assert!( + after.receipt_valid + && after.receipt_files_match + && after.marker_valid + && after.marker_files_match + ); + assert!(after.service_xml_matches); + assert!( + after.promoted_files_match, + "committed metadata must not invalidate promoted files" + ); + } + fn staged(name: &str, bytes: &[u8]) -> SingBoxStagedFile { SingBoxStagedFile { relative_path: name.to_owned(),