@@ -397,3 +397,96 @@ fn apply_and_rollback_reach_both_transitive_only_vlt_store_copies() {
397397 assert_eq ! ( v[ "alreadyOriginal" ] , 1 , "envelope={v}" ) ;
398398 assert_vlt_copies ( [ & primary, & twin] , false , "after rollback" ) ;
399399}
400+
401+ /// #626: a `node_modules/<name>` link to first-party source (an npm
402+ /// workspace member, which a `file:` directory dependency lays out the
403+ /// same way) that shares a patched package's `name@version` is the user's
404+ /// own code, not an installed copy. Agent-mode apply (dry run included)
405+ /// and rollback refuse it with a diagnostic and never overwrite the fork,
406+ /// even though the default mismatch policy would otherwise replace it.
407+ #[ cfg( unix) ]
408+ #[ test]
409+ fn apply_and_rollback_refuse_a_node_modules_link_to_first_party_source ( ) {
410+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
411+ let root = tmp. path ( ) ;
412+ let name = "dupvuln" ;
413+ let original = b"module.exports = function(){ return 'VULNERABLE'; };\n " ;
414+ let mut patched = original. to_vec ( ) ;
415+ patched. extend_from_slice ( b"// SOCKET-PATCHED-MULTICOPY\n " ) ;
416+ std:: fs:: write (
417+ root. join ( "package.json" ) ,
418+ r#"{ "name": "ws-root", "version": "0.0.0", "private": true, "workspaces": ["packages/*"] }"# ,
419+ )
420+ . unwrap ( ) ;
421+ let fork = b"module.exports = 'first-party fork';\n " ;
422+ let fork_index = write_copy ( & root. join ( "packages" ) . join ( name) , name, "1.0.0" , fork) ;
423+ std:: fs:: create_dir_all ( root. join ( "node_modules" ) ) . unwrap ( ) ;
424+ std:: os:: unix:: fs:: symlink (
425+ format ! ( "../packages/{name}" ) ,
426+ root. join ( "node_modules" ) . join ( name) ,
427+ )
428+ . unwrap ( ) ;
429+ stage_manifest_and_blob (
430+ root,
431+ "pkg:npm/dupvuln@1.0.0" ,
432+ & git_sha256 ( original) ,
433+ & git_sha256 ( & patched) ,
434+ & patched,
435+ ) ;
436+ std:: fs:: write (
437+ root. join ( ".socket" )
438+ . join ( "blobs" )
439+ . join ( git_sha256 ( original) ) ,
440+ original,
441+ )
442+ . unwrap ( ) ;
443+
444+ let assert_refused = |code : i32 , v : & serde_json:: Value , stage : & str | {
445+ assert_ne ! ( code, 0 , "{stage}: must fail closed; envelope={v}" ) ;
446+ assert ! (
447+ v. to_string( ) . contains( "outside every node_modules tree" ) ,
448+ "{stage}: the refusal must name the cause; envelope={v}"
449+ ) ;
450+ assert_eq ! (
451+ std:: fs:: read( & fork_index) . unwrap( ) ,
452+ fork,
453+ "{stage}: the first-party fork was overwritten"
454+ ) ;
455+ } ;
456+
457+ let out = Command :: new ( binary ( ) )
458+ . args ( [
459+ "apply" ,
460+ "--json" ,
461+ "--offline" ,
462+ "--dry-run" ,
463+ "--ecosystems" ,
464+ "npm" ,
465+ "--cwd" ,
466+ ] )
467+ . arg ( root)
468+ . output ( )
469+ . expect ( "run apply --dry-run" ) ;
470+ let stdout = String :: from_utf8_lossy ( & out. stdout ) . to_string ( ) ;
471+ let v: serde_json:: Value = serde_json:: from_str ( stdout. trim ( ) )
472+ . unwrap_or_else ( |e| panic ! ( "apply must emit JSON: {e}; stdout={stdout}" ) ) ;
473+ assert_refused ( out. status . code ( ) . unwrap_or ( -1 ) , & v, "apply --dry-run" ) ;
474+
475+ let ( code, v) = run_apply ( root) ;
476+ assert_refused ( code, & v, "apply" ) ;
477+
478+ // A fork left patched by an apply from before the guard: rollback must
479+ // not write the upstream original over it either.
480+ std:: fs:: write ( & fork_index, & patched) . unwrap ( ) ;
481+ let ( code, v) = run_rollback ( root) ;
482+ assert_ne ! ( code, 0 , "rollback must fail closed; envelope={v}" ) ;
483+ assert ! (
484+ v. to_string( ) . contains( "outside every node_modules tree" ) ,
485+ "rollback: envelope={v}"
486+ ) ;
487+ assert_eq ! (
488+ std:: fs:: read( & fork_index) . unwrap( ) ,
489+ patched,
490+ "rollback wrote through the link"
491+ ) ;
492+ }
0 commit comments