@@ -292,11 +292,17 @@ python-versions = ">=3.9"
292292content-hash = "4b42a89b7ff7b26511b06acdc458dbd85312e5083db8f212b017482bc68cdd01"
293293"# ;
294294
295+ /// A requirements.txt project; returns its wiring files.
296+ fn stage_requirements ( root : & Path ) -> & ' static [ & ' static str ] {
297+ std:: fs:: write ( root. join ( "requirements.txt" ) , "idna==3.7\n six==1.16.0\n " ) . unwrap ( ) ;
298+ & [ "requirements.txt" ]
299+ }
300+
295301#[ tokio:: test]
296302async fn requirements_vendored_to_hosted ( ) {
297303 let ( _tmp, root) = project ( ) ;
298- std :: fs :: write ( root . join ( "requirements.txt" ) , "idna==3.7 \n six==1.16.0 \n " ) . unwrap ( ) ;
299- assert_vendored_to_hosted ( & root, & [ "requirements.txt" ] ) . await ;
304+ let files = stage_requirements ( & root ) ;
305+ assert_vendored_to_hosted ( & root, files ) . await ;
300306}
301307
302308#[ tokio:: test]
@@ -306,9 +312,8 @@ async fn requirements_sole_pin_vendored_to_hosted() {
306312 assert_vendored_to_hosted ( & root, & [ "requirements.txt" ] ) . await ;
307313}
308314
309- #[ tokio:: test]
310- async fn poetry_vendored_to_hosted ( ) {
311- let ( _tmp, root) = project ( ) ;
315+ /// A Poetry project; returns its wiring files.
316+ fn stage_poetry ( root : & Path ) -> & ' static [ & ' static str ] {
312317 std:: fs:: write (
313318 root. join ( "pyproject.toml" ) ,
314319 "[tool.poetry]\n name = \" demo\" \n version = \" 0.1.0\" \n description = \" \" \n authors = [\" x <x@x>\" ]\n package-mode = false\n \n [tool.poetry.dependencies]\n python = \" >=3.9\" \n six = \" 1.16.0\" \n " ,
@@ -321,14 +326,20 @@ async fn poetry_vendored_to_hosted() {
321326 . replace ( "SDIST_SHA" , SDIST_SHA ) ,
322327 )
323328 . unwrap ( ) ;
324- assert_vendored_to_hosted ( & root , & [ "poetry.lock" , "pyproject.toml" ] ) . await ;
329+ & [ "poetry.lock" , "pyproject.toml" ]
325330}
326331
327- const PIPFILE : & str = "[[source]]\n url = \" https://pypi.org/simple\" \n verify_ssl = true\n name = \" pypi\" \n \n [packages]\n six = \" ==1.16.0\" \n \n [requires]\n python_version = \" 3.11\" \n " ;
328-
329332#[ tokio:: test]
330- async fn pipenv_vendored_to_hosted ( ) {
333+ async fn poetry_vendored_to_hosted ( ) {
331334 let ( _tmp, root) = project ( ) ;
335+ let files = stage_poetry ( & root) ;
336+ assert_vendored_to_hosted ( & root, files) . await ;
337+ }
338+
339+ const PIPFILE : & str = "[[source]]\n url = \" https://pypi.org/simple\" \n verify_ssl = true\n name = \" pypi\" \n \n [packages]\n six = \" ==1.16.0\" \n \n [requires]\n python_version = \" 3.11\" \n " ;
340+
341+ /// A Pipenv project; returns its wiring files.
342+ fn stage_pipenv ( root : & Path ) -> & ' static [ & ' static str ] {
332343 std:: fs:: write ( root. join ( "Pipfile" ) , PIPFILE ) . unwrap ( ) ;
333344 let lock = json ! ( {
334345 "_meta" : {
@@ -350,7 +361,14 @@ async fn pipenv_vendored_to_hosted() {
350361 let mut text = serde_json:: to_string_pretty ( & lock) . unwrap ( ) ;
351362 text. push ( '\n' ) ;
352363 std:: fs:: write ( root. join ( "Pipfile.lock" ) , text) . unwrap ( ) ;
353- assert_vendored_to_hosted ( & root, & [ "Pipfile.lock" ] ) . await ;
364+ & [ "Pipfile.lock" ]
365+ }
366+
367+ #[ tokio:: test]
368+ async fn pipenv_vendored_to_hosted ( ) {
369+ let ( _tmp, root) = project ( ) ;
370+ let files = stage_pipenv ( & root) ;
371+ assert_vendored_to_hosted ( & root, files) . await ;
354372}
355373
356374const UV_LOCK : & str = r#"version = 1
@@ -378,9 +396,8 @@ wheels = [
378396]
379397"# ;
380398
381- #[ tokio:: test]
382- async fn uv_vendored_to_hosted ( ) {
383- let ( _tmp, root) = project ( ) ;
399+ /// A uv project; returns its wiring files.
400+ fn stage_uv ( root : & Path ) -> & ' static [ & ' static str ] {
384401 std:: fs:: write (
385402 root. join ( "pyproject.toml" ) ,
386403 "[project]\n name = \" demo\" \n version = \" 0.1.0\" \n requires-python = \" >=3.9\" \n dependencies = [\" six==1.16.0\" ]\n " ,
@@ -393,18 +410,31 @@ async fn uv_vendored_to_hosted() {
393410 . replace ( "SDIST_SHA" , SDIST_SHA ) ,
394411 )
395412 . unwrap ( ) ;
396- assert_vendored_to_hosted ( & root , & [ "uv.lock" , "pyproject.toml" ] ) . await ;
413+ & [ "uv.lock" , "pyproject.toml" ]
397414}
398415
399416#[ tokio:: test]
400- async fn hatch_vendored_to_hosted ( ) {
417+ async fn uv_vendored_to_hosted ( ) {
401418 let ( _tmp, root) = project ( ) ;
419+ let files = stage_uv ( & root) ;
420+ assert_vendored_to_hosted ( & root, files) . await ;
421+ }
422+
423+ /// A Hatch project; returns its wiring files.
424+ fn stage_hatch ( root : & Path ) -> & ' static [ & ' static str ] {
402425 std:: fs:: write (
403426 root. join ( "pyproject.toml" ) ,
404427 "[build-system]\n requires = [\" hatchling\" ]\n build-backend = \" hatchling.build\" \n \n [project]\n name = \" demo\" \n version = \" 0.1.0\" \n dependencies = [\" six==1.16.0\" ]\n " ,
405428 )
406429 . unwrap ( ) ;
407- assert_vendored_to_hosted ( & root, & [ "pyproject.toml" ] ) . await ;
430+ & [ "pyproject.toml" ]
431+ }
432+
433+ #[ tokio:: test]
434+ async fn hatch_vendored_to_hosted ( ) {
435+ let ( _tmp, root) = project ( ) ;
436+ let files = stage_hatch ( & root) ;
437+ assert_vendored_to_hosted ( & root, files) . await ;
408438}
409439
410440/// The uv lock rewrite needs the hosted wheel's METADATA, fetched only
@@ -597,3 +627,81 @@ async fn ledger_update_failure_after_revert_is_stranded() {
597627 assert_eq ! ( env[ "status" ] , "partial_failure" , "{env:#}" ) ;
598628 assert_eq ! ( code, 1 , "{env:#}" ) ;
599629}
630+
631+ // ── `vendor --check` wiring audit (#725) ─────────────────────────────────
632+
633+ /// Vendor the staged project, confirm `vendor --check` passes, then put
634+ /// the wiring files back to their pre-vendor bytes — what `pipenv lock`,
635+ /// `poetry lock`, `uv lock` or a hand-edited requirements.txt leave behind —
636+ /// and require `vendor --check` to fail: the committed wheel is intact, but
637+ /// nothing installs it any more, so a fresh install is unpatched.
638+ fn assert_check_catches_relock ( root : & Path , files : & [ & str ] ) {
639+ let pristine: Vec < Vec < u8 > > = files
640+ . iter ( )
641+ . map ( |f| std:: fs:: read ( root. join ( f) ) . unwrap ( ) )
642+ . collect ( ) ;
643+ vendor_project ( root, files) ;
644+
645+ let ( code, env) = run_cli ( root, & [ "vendor" , "--check" ] , & [ ] ) ;
646+ assert_eq ! ( code, 0 , "wired project passes: {env:#}" ) ;
647+ assert_eq ! ( env[ "events" ] [ 0 ] [ "errorCode" ] , "vendor_check_ok" , "{env:#}" ) ;
648+
649+ for ( f, bytes) in files. iter ( ) . zip ( & pristine) {
650+ std:: fs:: write ( root. join ( f) , bytes) . unwrap ( ) ;
651+ }
652+ let ( code, env) = run_cli ( root, & [ "vendor" , "--check" ] , & [ ] ) ;
653+ assert_eq ! ( code, 1 , "{files:?} no longer wire the artifact: {env:#}" ) ;
654+ let event = & env[ "events" ] [ 0 ] ;
655+ assert_eq ! ( event[ "action" ] , "failed" , "{env:#}" ) ;
656+ assert_eq ! ( event[ "errorCode" ] , "vendor_check_failed" , "{env:#}" ) ;
657+ assert ! (
658+ event[ "reason" ]
659+ . as_str( )
660+ . is_some_and( |r| r. contains( "wiring" ) ) ,
661+ "the failure names the missing wiring: {env:#}"
662+ ) ;
663+ assert_eq ! ( env[ "summary" ] [ "failed" ] , 1 , "{env:#}" ) ;
664+ }
665+
666+ #[ tokio:: test]
667+ async fn vendor_check_fails_after_pipenv_relock ( ) {
668+ let ( _tmp, root) = project ( ) ;
669+ let files = stage_pipenv ( & root) ;
670+ assert_check_catches_relock ( & root, files) ;
671+
672+ // Human mode must not claim the wiring was verified.
673+ let ( code, stdout, stderr) = run_raw ( & root, & [ "vendor" , "--check" ] , & [ ] ) ;
674+ assert_eq ! ( code, 1 , "stdout:\n {stdout}\n stderr:\n {stderr}" ) ;
675+ assert ! (
676+ !stdout. contains( "wiring verified" ) ,
677+ "stdout:\n {stdout}\n stderr:\n {stderr}"
678+ ) ;
679+ }
680+
681+ #[ tokio:: test]
682+ async fn vendor_check_fails_after_requirements_rewrite ( ) {
683+ let ( _tmp, root) = project ( ) ;
684+ let files = stage_requirements ( & root) ;
685+ assert_check_catches_relock ( & root, files) ;
686+ }
687+
688+ #[ tokio:: test]
689+ async fn vendor_check_fails_after_poetry_relock ( ) {
690+ let ( _tmp, root) = project ( ) ;
691+ let files = stage_poetry ( & root) ;
692+ assert_check_catches_relock ( & root, files) ;
693+ }
694+
695+ #[ tokio:: test]
696+ async fn vendor_check_fails_after_uv_relock ( ) {
697+ let ( _tmp, root) = project ( ) ;
698+ let files = stage_uv ( & root) ;
699+ assert_check_catches_relock ( & root, files) ;
700+ }
701+
702+ #[ tokio:: test]
703+ async fn vendor_check_fails_after_hatch_dependency_reset ( ) {
704+ let ( _tmp, root) = project ( ) ;
705+ let files = stage_hatch ( & root) ;
706+ assert_check_catches_relock ( & root, files) ;
707+ }
0 commit comments