@@ -517,6 +517,120 @@ fn test_pypi_global_lifecycle() {
517517 ) ;
518518}
519519
520+ /// `get --save-only` should save the patch to the manifest without applying.
521+ #[ test]
522+ #[ ignore]
523+ fn test_pypi_save_only ( ) {
524+ if !has_python3 ( ) {
525+ eprintln ! ( "SKIP: python3 not found on PATH" ) ;
526+ return ;
527+ }
528+
529+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
530+ let cwd = dir. path ( ) ;
531+
532+ setup_venv ( cwd) ;
533+
534+ let site_packages = find_site_packages ( cwd) ;
535+ let messages_py = site_packages. join ( "pydantic_ai/messages.py" ) ;
536+ assert ! ( messages_py. exists( ) ) ;
537+ let original_hash = git_sha256_file ( & messages_py) ;
538+
539+ // Download with --save-only.
540+ assert_run_ok ( cwd, & [ "get" , PYPI_UUID , "--save-only" ] , "get --save-only" ) ;
541+
542+ // File should be unchanged.
543+ assert_eq ! (
544+ git_sha256_file( & messages_py) ,
545+ original_hash,
546+ "file should not change after get --save-only"
547+ ) ;
548+
549+ // Manifest should exist with the patch.
550+ let manifest_path = cwd. join ( ".socket/manifest.json" ) ;
551+ assert ! ( manifest_path. exists( ) , "manifest should exist after get --save-only" ) ;
552+
553+ let ( purl, _) = read_patch_files ( & manifest_path) ;
554+ assert ! (
555+ purl. starts_with( PYPI_PURL_PREFIX ) ,
556+ "manifest should contain a pydantic-ai patch"
557+ ) ;
558+
559+ // Real apply should work.
560+ assert_run_ok ( cwd, & [ "apply" ] , "apply" ) ;
561+
562+ let ( _, files_value) = read_patch_files ( & manifest_path) ;
563+ let files = files_value. as_object ( ) . unwrap ( ) ;
564+ let after_hash = files[ "pydantic_ai/messages.py" ] [ "afterHash" ]
565+ . as_str ( )
566+ . unwrap ( ) ;
567+ assert_eq ! (
568+ git_sha256_file( & messages_py) ,
569+ after_hash,
570+ "file should match afterHash after apply"
571+ ) ;
572+ }
573+
574+ /// `apply --force` should apply patches even when file hashes don't match.
575+ #[ test]
576+ #[ ignore]
577+ fn test_pypi_apply_force ( ) {
578+ if !has_python3 ( ) {
579+ eprintln ! ( "SKIP: python3 not found on PATH" ) ;
580+ return ;
581+ }
582+
583+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
584+ let cwd = dir. path ( ) ;
585+
586+ setup_venv ( cwd) ;
587+
588+ let site_packages = find_site_packages ( cwd) ;
589+
590+ // Save the patch without applying.
591+ assert_run_ok ( cwd, & [ "get" , PYPI_UUID , "--save-only" ] , "get --save-only" ) ;
592+
593+ let manifest_path = cwd. join ( ".socket/manifest.json" ) ;
594+ let ( _, files_value) = read_patch_files ( & manifest_path) ;
595+ let files = files_value. as_object ( ) . unwrap ( ) ;
596+
597+ // Corrupt one of the files to create a hash mismatch.
598+ let messages_py = site_packages. join ( "pydantic_ai/messages.py" ) ;
599+ let before_hash = files[ "pydantic_ai/messages.py" ] [ "beforeHash" ]
600+ . as_str ( )
601+ . unwrap ( ) ;
602+ assert_eq ! (
603+ git_sha256_file( & messages_py) ,
604+ before_hash,
605+ "file should match beforeHash before corruption"
606+ ) ;
607+
608+ std:: fs:: write ( & messages_py, b"# corrupted content\n " ) . unwrap ( ) ;
609+ assert_ne ! (
610+ git_sha256_file( & messages_py) ,
611+ before_hash,
612+ "file should have a different hash after corruption"
613+ ) ;
614+
615+ // Normal apply should fail due to hash mismatch.
616+ let ( code, _stdout, _stderr) = run ( cwd, & [ "apply" ] ) ;
617+ assert_ne ! ( code, 0 , "apply without --force should fail on hash mismatch" ) ;
618+
619+ // Apply with --force should succeed.
620+ assert_run_ok ( cwd, & [ "apply" , "--force" ] , "apply --force" ) ;
621+
622+ // Verify all files match afterHash.
623+ for ( rel_path, info) in files {
624+ let after_hash = info[ "afterHash" ] . as_str ( ) . expect ( "afterHash" ) ;
625+ let full_path = site_packages. join ( rel_path) ;
626+ assert_eq ! (
627+ git_sha256_file( & full_path) ,
628+ after_hash,
629+ "{rel_path} should match afterHash after apply --force"
630+ ) ;
631+ }
632+ }
633+
520634/// UUID shortcut: `socket-patch <UUID>` should behave like `socket-patch get <UUID>`.
521635#[ test]
522636#[ ignore]
0 commit comments