@@ -399,6 +399,89 @@ fn cache_gc_prunes_unused_tool_versions() -> anyhow::Result<()> {
399399 Ok ( ( ) )
400400}
401401
402+ #[ test]
403+ fn cache_gc_prunes_tool_versions_without_positive_identification ( ) -> anyhow:: Result < ( ) > {
404+ let context = TestContext :: new ( ) ;
405+
406+ context. write_pre_commit_config ( indoc:: indoc! { r#"
407+ repos:
408+ - repo: local
409+ hooks:
410+ - id: local-python
411+ name: Local Python Hook
412+ entry: "python -c \"print(1)\""
413+ language: python
414+ "# } ) ;
415+
416+ let home = context. home_dir ( ) ;
417+
418+ // Track the config so GC has something to mark from.
419+ let config_path = context. work_dir ( ) . child ( CONFIG_FILE ) ;
420+ write_config_tracking_file ( home, & [ config_path. path ( ) ] ) ?;
421+
422+ // Seed a matching installed hook env marker, but use a toolchain path that is *not* inside
423+ // PREK_HOME/tools. This means we cannot positively identify a used tool version, so all
424+ // tool versions under the bucket are unused and should be pruned.
425+ let env_py = home. child ( "hooks/python-keep" ) ;
426+ env_py. create_dir_all ( ) ?;
427+ let marker_py = json ! ( {
428+ "language" : "python" ,
429+ "language_version" : "3.12.0" ,
430+ "dependencies" : [ ] ,
431+ "env_path" : env_py. path( ) ,
432+ "toolchain" : "/usr/bin/python3" ,
433+ "extra" : { } ,
434+ } ) ;
435+ env_py
436+ . child ( ".prek-hook.json" )
437+ . write_str ( & serde_json:: to_string_pretty ( & marker_py) ?) ?;
438+
439+ // Seed tool versions that should be removed.
440+ let py_312 = home. child ( "tools/python/3.12.0" ) ;
441+ let py_311 = home. child ( "tools/python/3.11.0" ) ;
442+ py_312. create_dir_all ( ) ?;
443+ py_311. create_dir_all ( ) ?;
444+
445+ // Add a temp dir to ensure it is not removed.
446+ home. child ( "repos/.temp" ) . create_dir_all ( ) ?;
447+ home. child ( "tools/.temp" ) . create_dir_all ( ) ?;
448+
449+ cmd_snapshot ! (
450+ context. filters( ) ,
451+ context. command( ) . args( [ "cache" , "gc" , "--dry-run" , "-v" ] ) ,
452+ @r"
453+ success: true
454+ exit_code: 0
455+ ----- stdout -----
456+ Would remove 2 tools ([SIZE])
457+
458+ Would remove 2 tools:
459+ - python/3.11.0
460+ path: [HOME]/tools/python/3.11.0
461+ - python/3.12.0
462+ path: [HOME]/tools/python/3.12.0
463+
464+ ----- stderr -----
465+ "
466+ ) ;
467+
468+ cmd_snapshot ! ( context. filters( ) , context. command( ) . args( [ "cache" , "gc" ] ) , @r"
469+ success: true
470+ exit_code: 0
471+ ----- stdout -----
472+ Removed 2 tools ([SIZE])
473+
474+ ----- stderr -----
475+ " ) ;
476+
477+ py_312. assert ( predicates:: path:: missing ( ) ) ;
478+ py_311. assert ( predicates:: path:: missing ( ) ) ;
479+ home. child ( "tools/python" )
480+ . assert ( predicates:: path:: is_dir ( ) ) ;
481+
482+ Ok ( ( ) )
483+ }
484+
402485#[ test]
403486fn cache_gc_keeps_local_hook_env ( ) -> anyhow:: Result < ( ) > {
404487 let context = TestContext :: new ( ) ;
0 commit comments